diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/proxy/handler.go b/cmd/proxy/handler.go similarity index 100% rename from proxy/handler.go rename to cmd/proxy/handler.go diff --git a/proxy/proxy.go b/cmd/proxy/proxy.go similarity index 100% rename from proxy/proxy.go rename to cmd/proxy/proxy.go diff --git a/commands.go b/commands.go index 1c75f32..ef95be2 100644 --- a/commands.go +++ b/commands.go @@ -2,7 +2,7 @@ package main import ( "github.com/spf13/cobra" - "github.com/tenderly/tenderly-cli/proxy" + "github.com/tenderly/tenderly-cli/cmd/proxy" ) var targetHost string diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..d9594e5 --- /dev/null +++ b/config/config.go @@ -0,0 +1,60 @@ +package config + +import ( + "flag" + "fmt" + + "github.com/spf13/viper" +) + +const ( + TargetHost = "targetHost" + TargetPort = "targetPort" + ProxyPort = "proxyPort" + Path = "path" + Network = "network" +) + +var defaults = map[string]interface{}{ + TargetHost: "8525", + TargetPort: "127.0.0.1", + ProxyPort: "9545", + Path: ".", + Network: "mainnet", +} + +var configName string + +func init() { + flag.StringVar(&configName, "config", "config", "Configuration file name (without the extension)") +} + +func Init() { + flag.Parse() + + viper.SetConfigName(configName) + viper.AddConfigPath("/etc/tenderly/") + viper.AddConfigPath("$HOME/.tenderly") + viper.AddConfigPath(".") + + for k, v := range defaults { + viper.SetDefault(k, v) + } + + err := viper.ReadInConfig() + if err != nil { + panic(fmt.Errorf("Fatal error config file: %s \n", err)) + } +} + +func GetString(key string) string { + check(key) + + return viper.GetString(key) +} + +func check(key string) { + if !viper.IsSet(key) { + panic(fmt.Errorf("missing config for key %s", key)) + } +} diff --git a/ethereum/README.md b/ethereum/README.md new file mode 100644 index 0000000..f799eaa --- /dev/null +++ b/ethereum/README.md @@ -0,0 +1,3 @@ +# Ethereum Client + +Ethereum client is supposed to be a general interface over any ethereum node. \ No newline at end of file diff --git a/ethereum/client/client.go b/ethereum/client/client.go new file mode 100644 index 0000000..5526919 --- /dev/null +++ b/ethereum/client/client.go @@ -0,0 +1,207 @@ +package client + +import ( + "encoding/json" + "fmt" + "log" + "time" + + "github.com/tenderly/tenderly-cli/ethereum" + "github.com/tenderly/tenderly-cli/ethereum/geth" + "github.com/tenderly/tenderly-cli/ethereum/parity" + + "github.com/tenderly/tenderly-cli/jsonrpc2" +) + +// Client represents an implementation agnostic interface to the Ethereum node. +// It is able connect to both different protocols (http, ws) and implementations (geth, parity). +type Client struct { + rpc *jsonrpc2.Client + schema ethereum.Schema + + openChannels []chan int64 +} + +func Dial(host string) (*Client, error) { + rpcClient, err := jsonrpc2.DiscoverAndDial(host) + if err != nil { + return nil, fmt.Errorf("dial ethereum rpc: %s", err) + } + + nodeType := "geth" + + req, resp := parity.DefaultSchema.Parity().VersionInfo() + if err = rpcClient.CallRequest(resp, req); err == nil { + nodeType = "parity" + } + + var schema ethereum.Schema + switch nodeType { + case "geth": + schema = &geth.DefaultSchema + case "parity": + schema = &parity.DefaultSchema + default: + return nil, fmt.Errorf("unsupported node type: %s", err) + } + + return &Client{ + rpc: rpcClient, + schema: schema, + }, nil +} + +func (c *Client) CurrentBlockNumber() (int64, error) { + req, resp := c.schema.Eth().BlockNumber() + + err := c.rpc.CallRequest(resp, req) + if err != nil { + return 0, fmt.Errorf("current block number: %s", err) + } + + return resp.Value(), nil +} + +func (c *Client) GetBlock(number int64) (ethereum.Block, error) { + req, resp := c.schema.Eth().GetBlockByNumber(ethereum.Number(number)) + + if err := c.rpc.CallRequest(resp, req); err != nil { + return nil, fmt.Errorf("get block by number [%d]: %s", number, err) + } + + return resp, nil +} + +func (c *Client) GetTransaction(hash string) (ethereum.Transaction, error) { + req, resp := c.schema.Eth().GetTransaction(hash) + + if err := c.rpc.CallRequest(resp, req); err != nil { + return nil, fmt.Errorf("get transaction [%s]: %s", hash, err) + } + + return resp, nil +} + +func (c *Client) GetTransactionReceipt(hash string) (ethereum.TransactionReceipt, error) { + req, resp := c.schema.Eth().GetTransactionReceipt(hash) + + if err := c.rpc.CallRequest(resp, req); err != nil { + return nil, fmt.Errorf("get transaction receipt [%s]: %s", hash, err) + } + + return resp, nil +} + +func (c *Client) GetNetworkID() (string, error) { + req, resp := c.schema.Net().Version() + + if err := c.rpc.CallRequest(resp, req); err != nil { + return "", fmt.Errorf("get network ID: %s", err) + } + + return *resp, nil +} + +func (c *Client) GetTransactionVMTrace(hash string) (ethereum.TransactionStates, error) { + req, resp := c.schema.Trace().VMTrace(hash) + + if err := c.rpc.CallRequest(resp, req); err != nil { + return nil, fmt.Errorf("get transaction trace [%s]: %s", hash, err) + } + + resp.ProcessTrace() + + return resp, nil +} + +func (c *Client) GetTransactionCallTrace(hash string) (ethereum.CallTraces, error) { + req, resp := c.schema.Trace().CallTrace(hash) + + if err := c.rpc.CallRequest(resp, req); err != nil { + return nil, fmt.Errorf("get transaction pretty trace [%s]: %s", hash, err) + } + + return resp, nil +} + +func (c *Client) Subscribe(forcePoll bool) (chan int64, error) { + if forcePoll { + log.Printf("Forcing polling subscription...") + return c.subscribeViaPoll() + } + + //@TODO: Manage closing of the subscription. + req, subscription := c.schema.PubSub().Subscribe() + err := c.rpc.CallRequest(subscription, req) + if err != nil { + //@TODO: Do specific check if subscription not supported. + log.Printf("Subscription not supported, falling back to polling") + return c.subscribeViaPoll() + } + + return c.subscribe(subscription) +} + +func (c *Client) subscribeViaPoll() (chan int64, error) { + outCh := make(chan int64) + + go func() { + var lastBlock int64 + + for { + blockNumber, err := c.CurrentBlockNumber() + if err != nil { + log.Printf("failed pollig for last block number: %s", err) + time.Sleep(1 * time.Second) + continue + } + + if lastBlock == 0 { + lastBlock = blockNumber + continue + } + + for lastBlock < blockNumber { + outCh <- blockNumber + + lastBlock++ + } + + time.Sleep(200 * time.Millisecond) + } + }() + + return outCh, nil +} + +func (c *Client) subscribe(id *ethereum.SubscriptionID) (chan int64, error) { + outCh := make(chan int64) + + inCh, err := c.rpc.Subscribe(id.String()) + if err != nil { + return nil, fmt.Errorf("listen for subscriptions: %s", err) + } + + go func() { + for msg := range inCh { + var resp geth.SubscriptionResult + err = json.Unmarshal(msg.Params, &resp) + if err != nil { + log.Printf("failed reading notification: %s", err) + continue + } + + outCh <- resp.Result.Number().Value() + } + + close(outCh) + }() + + return outCh, nil +} + +func (c *Client) Close() error { + c.rpc.Close() + + return nil +} diff --git a/ethereum/core/accounts/abi/abi.go b/ethereum/core/accounts/abi/abi.go new file mode 100644 index 0000000..254b1f7 --- /dev/null +++ b/ethereum/core/accounts/abi/abi.go @@ -0,0 +1,146 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package abi + +import ( + "bytes" + "encoding/json" + "fmt" + "io" +) + +// The ABI holds information about a contract's context and available +// invokable methods. It will allow you to type check function calls and +// packs data accordingly. +type ABI struct { + Constructor Method + Methods map[string]Method + Events map[string]Event +} + +// JSON returns a parsed ABI interface and error if it failed. +func JSON(reader io.Reader) (ABI, error) { + dec := json.NewDecoder(reader) + + var abi ABI + if err := dec.Decode(&abi); err != nil { + return ABI{}, err + } + + return abi, nil +} + +// Pack the given method name to conform the ABI. Method call's data +// will consist of method_id, args0, arg1, ... argN. Method id consists +// of 4 bytes and arguments are all 32 bytes. +// Method ids are created from the first 4 bytes of the hash of the +// methods string signature. (signature = baz(uint32,string32)) +func (abi ABI) Pack(name string, args ...interface{}) ([]byte, error) { + // Fetch the ABI of the requested method + if name == "" { + // constructor + arguments, err := abi.Constructor.Inputs.Pack(args...) + if err != nil { + return nil, err + } + return arguments, nil + + } + method, exist := abi.Methods[name] + if !exist { + return nil, fmt.Errorf("method '%s' not found", name) + } + + arguments, err := method.Inputs.Pack(args...) + if err != nil { + return nil, err + } + // Pack up the method ID too if not a constructor and return + return append(method.Id(), arguments...), nil +} + +// Unpack output in v according to the abi specification +func (abi ABI) Unpack(v interface{}, name string, output []byte) (err error) { + if len(output) == 0 { + return fmt.Errorf("abi: unmarshalling empty output") + } + // since there can't be naming collisions with contracts and events, + // we need to decide whether we're calling a method or an event + if method, ok := abi.Methods[name]; ok { + if len(output)%32 != 0 { + return fmt.Errorf("abi: improperly formatted output") + } + return method.Outputs.Unpack(v, output) + } else if event, ok := abi.Events[name]; ok { + return event.Inputs.Unpack(v, output) + } + return fmt.Errorf("abi: could not locate named method or event") +} + +// UnmarshalJSON implements json.Unmarshaler interface +func (abi *ABI) UnmarshalJSON(data []byte) error { + var fields []struct { + Type string + Name string + Constant bool + Anonymous bool + Inputs []Argument + Outputs []Argument + } + + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + + abi.Methods = make(map[string]Method) + abi.Events = make(map[string]Event) + for _, field := range fields { + switch field.Type { + case "constructor": + abi.Constructor = Method{ + Inputs: field.Inputs, + } + // empty defaults to function according to the abi spec + case "function", "": + abi.Methods[field.Name] = Method{ + Name: field.Name, + Const: field.Constant, + Inputs: field.Inputs, + Outputs: field.Outputs, + } + case "event": + abi.Events[field.Name] = Event{ + Name: field.Name, + Anonymous: field.Anonymous, + Inputs: field.Inputs, + } + } + } + + return nil +} + +// MethodById looks up a method by the 4-byte id +// returns nil if none found +func (abi *ABI) MethodById(sigdata []byte) (*Method, error) { + for _, method := range abi.Methods { + if bytes.Equal(method.Id(), sigdata[:4]) { + return &method, nil + } + } + return nil, fmt.Errorf("no method with id: %#x", sigdata[:4]) +} diff --git a/ethereum/core/accounts/abi/argument.go b/ethereum/core/accounts/abi/argument.go new file mode 100644 index 0000000..93b513c --- /dev/null +++ b/ethereum/core/accounts/abi/argument.go @@ -0,0 +1,290 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package abi + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" +) + +// Argument holds the name of the argument and the corresponding type. +// Types are used when packing and testing arguments. +type Argument struct { + Name string + Type Type + Indexed bool // indexed is only used by events +} + +type Arguments []Argument + +// UnmarshalJSON implements json.Unmarshaler interface +func (argument *Argument) UnmarshalJSON(data []byte) error { + var extarg struct { + Name string + Type string + Indexed bool + } + err := json.Unmarshal(data, &extarg) + if err != nil { + return fmt.Errorf("argument json err: %v", err) + } + + argument.Type, err = NewType(extarg.Type) + if err != nil { + return err + } + argument.Name = extarg.Name + argument.Indexed = extarg.Indexed + + return nil +} + +// LengthNonIndexed returns the number of arguments when not counting 'indexed' ones. Only events +// can ever have 'indexed' arguments, it should always be false on arguments for method input/output +func (arguments Arguments) LengthNonIndexed() int { + out := 0 + for _, arg := range arguments { + if !arg.Indexed { + out++ + } + } + return out +} + +// NonIndexed returns the arguments with indexed arguments filtered out +func (arguments Arguments) NonIndexed() Arguments { + var ret []Argument + for _, arg := range arguments { + if !arg.Indexed { + ret = append(ret, arg) + } + } + return ret +} + +// isTuple returns true for non-atomic constructs, like (uint,uint) or uint[] +func (arguments Arguments) isTuple() bool { + return len(arguments) > 1 +} + +// Unpack performs the operation hexdata -> Go format +func (arguments Arguments) Unpack(v interface{}, data []byte) error { + + // make sure the passed value is arguments pointer + if reflect.Ptr != reflect.ValueOf(v).Kind() { + return fmt.Errorf("abi: Unpack(non-pointer %T)", v) + } + marshalledValues, err := arguments.UnpackValues(data) + if err != nil { + return err + } + if arguments.isTuple() { + return arguments.unpackTuple(v, marshalledValues) + } + return arguments.unpackAtomic(v, marshalledValues) +} + +func (arguments Arguments) unpackTuple(v interface{}, marshalledValues []interface{}) error { + + var ( + value = reflect.ValueOf(v).Elem() + typ = value.Type() + kind = value.Kind() + ) + + if err := requireUnpackKind(value, typ, kind, arguments); err != nil { + return err + } + + // If the interface is a struct, get of abi->struct_field mapping + + var abi2struct map[string]string + if kind == reflect.Struct { + var err error + abi2struct, err = mapAbiToStructFields(arguments, value) + if err != nil { + return err + } + } + for i, arg := range arguments.NonIndexed() { + + reflectValue := reflect.ValueOf(marshalledValues[i]) + + switch kind { + case reflect.Struct: + if structField, ok := abi2struct[arg.Name]; ok { + if err := set(value.FieldByName(structField), reflectValue, arg); err != nil { + return err + } + } + case reflect.Slice, reflect.Array: + if value.Len() < i { + return fmt.Errorf("abi: insufficient number of arguments for unpack, want %d, got %d", len(arguments), value.Len()) + } + v := value.Index(i) + if err := requireAssignable(v, reflectValue); err != nil { + return err + } + + if err := set(v.Elem(), reflectValue, arg); err != nil { + return err + } + default: + return fmt.Errorf("abi:[2] cannot unmarshal tuple in to %v", typ) + } + } + return nil +} + +// unpackAtomic unpacks ( hexdata -> go ) a single value +func (arguments Arguments) unpackAtomic(v interface{}, marshalledValues []interface{}) error { + if len(marshalledValues) != 1 { + return fmt.Errorf("abi: wrong length, expected single value, got %d", len(marshalledValues)) + } + + elem := reflect.ValueOf(v).Elem() + kind := elem.Kind() + reflectValue := reflect.ValueOf(marshalledValues[0]) + + var abi2struct map[string]string + if kind == reflect.Struct { + var err error + if abi2struct, err = mapAbiToStructFields(arguments, elem); err != nil { + return err + } + arg := arguments.NonIndexed()[0] + if structField, ok := abi2struct[arg.Name]; ok { + return set(elem.FieldByName(structField), reflectValue, arg) + } + return nil + } + + return set(elem, reflectValue, arguments.NonIndexed()[0]) + +} + +// Computes the full size of an array; +// i.e. counting nested arrays, which count towards size for unpacking. +func getArraySize(arr *Type) int { + size := arr.Size + // Arrays can be nested, with each element being the same size + arr = arr.Elem + for arr.T == ArrayTy { + // Keep multiplying by elem.Size while the elem is an array. + size *= arr.Size + arr = arr.Elem + } + // Now we have the full array size, including its children. + return size +} + +// UnpackValues can be used to unpack ABI-encoded hexdata according to the ABI-specification, +// without supplying a struct to unpack into. Instead, this method returns a list containing the +// values. An atomic argument will be a list with one element. +func (arguments Arguments) UnpackValues(data []byte) ([]interface{}, error) { + retval := make([]interface{}, 0, arguments.LengthNonIndexed()) + virtualArgs := 0 + for index, arg := range arguments.NonIndexed() { + marshalledValue, err := toGoType((index+virtualArgs)*32, arg.Type, data) + if arg.Type.T == ArrayTy { + // If we have a static array, like [3]uint256, these are coded as + // just like uint256,uint256,uint256. + // This means that we need to add two 'virtual' arguments when + // we count the index from now on. + // + // Array values nested multiple levels deep are also encoded inline: + // [2][3]uint256: uint256,uint256,uint256,uint256,uint256,uint256 + // + // Calculate the full array size to get the correct offset for the next argument. + // Decrement it by 1, as the normal index increment is still applied. + virtualArgs += getArraySize(&arg.Type) - 1 + } + if err != nil { + return nil, err + } + retval = append(retval, marshalledValue) + } + return retval, nil +} + +// PackValues performs the operation Go format -> Hexdata +// It is the semantic opposite of UnpackValues +func (arguments Arguments) PackValues(args []interface{}) ([]byte, error) { + return arguments.Pack(args...) +} + +// Pack performs the operation Go format -> Hexdata +func (arguments Arguments) Pack(args ...interface{}) ([]byte, error) { + // Make sure arguments match up and pack them + abiArgs := arguments + if len(args) != len(abiArgs) { + return nil, fmt.Errorf("argument count mismatch: %d for %d", len(args), len(abiArgs)) + } + // variable input is the output appended at the end of packed + // output. This is used for strings and bytes types input. + var variableInput []byte + + // input offset is the bytes offset for packed output + inputOffset := 0 + for _, abiArg := range abiArgs { + if abiArg.Type.T == ArrayTy { + inputOffset += 32 * abiArg.Type.Size + } else { + inputOffset += 32 + } + } + var ret []byte + for i, a := range args { + input := abiArgs[i] + // pack the input + packed, err := input.Type.pack(reflect.ValueOf(a)) + if err != nil { + return nil, err + } + // check for a slice type (string, bytes, slice) + if input.Type.requiresLengthPrefix() { + // calculate the offset + offset := inputOffset + len(variableInput) + // set the offset + ret = append(ret, packNum(reflect.ValueOf(offset))...) + // Append the packed output to the variable input. The variable input + // will be appended at the end of the input. + variableInput = append(variableInput, packed...) + } else { + // append the packed value to the input + ret = append(ret, packed...) + } + } + // append the variable input at the end of the packed input + ret = append(ret, variableInput...) + + return ret, nil +} + +// capitalise makes the first character of a string upper case, also removing any +// prefixing underscores from the variable names. +func capitalise(input string) string { + for len(input) > 0 && input[0] == '_' { + input = input[1:] + } + if len(input) == 0 { + return "" + } + return strings.ToUpper(input[:1]) + input[1:] +} diff --git a/ethereum/core/accounts/abi/error.go b/ethereum/core/accounts/abi/error.go new file mode 100644 index 0000000..9d8674a --- /dev/null +++ b/ethereum/core/accounts/abi/error.go @@ -0,0 +1,84 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package abi + +import ( + "errors" + "fmt" + "reflect" +) + +var ( + errBadBool = errors.New("abi: improperly encoded boolean value") +) + +// formatSliceString formats the reflection kind with the given slice size +// and returns a formatted string representation. +func formatSliceString(kind reflect.Kind, sliceSize int) string { + if sliceSize == -1 { + return fmt.Sprintf("[]%v", kind) + } + return fmt.Sprintf("[%d]%v", sliceSize, kind) +} + +// sliceTypeCheck checks that the given slice can by assigned to the reflection +// type in t. +func sliceTypeCheck(t Type, val reflect.Value) error { + if val.Kind() != reflect.Slice && val.Kind() != reflect.Array { + return typeErr(formatSliceString(t.Kind, t.Size), val.Type()) + } + + if t.T == ArrayTy && val.Len() != t.Size { + return typeErr(formatSliceString(t.Elem.Kind, t.Size), formatSliceString(val.Type().Elem().Kind(), val.Len())) + } + + if t.Elem.T == SliceTy { + if val.Len() > 0 { + return sliceTypeCheck(*t.Elem, val.Index(0)) + } + } else if t.Elem.T == ArrayTy { + return sliceTypeCheck(*t.Elem, val.Index(0)) + } + + if elemKind := val.Type().Elem().Kind(); elemKind != t.Elem.Kind { + return typeErr(formatSliceString(t.Elem.Kind, t.Size), val.Type()) + } + return nil +} + +// typeCheck checks that the given reflection value can be assigned to the reflection +// type in t. +func typeCheck(t Type, value reflect.Value) error { + if t.T == SliceTy || t.T == ArrayTy { + return sliceTypeCheck(t, value) + } + + // Check base type validity. Element types will be checked later on. + if t.Kind != value.Kind() { + return typeErr(t.Kind, value.Kind()) + } else if t.T == FixedBytesTy && t.Size != value.Len() { + return typeErr(t.Type, value.Type()) + } else { + return nil + } + +} + +// typeErr returns a formatted type casting error. +func typeErr(expected, got interface{}) error { + return fmt.Errorf("abi: cannot use %v as type %v as argument", got, expected) +} diff --git a/ethereum/core/accounts/abi/event.go b/ethereum/core/accounts/abi/event.go new file mode 100644 index 0000000..b6e3ec9 --- /dev/null +++ b/ethereum/core/accounts/abi/event.go @@ -0,0 +1,60 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package abi + +import ( + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/common" + "golang.org/x/crypto/sha3" +) + +// Event is an event potentially triggered by the EVM's LOG mechanism. The Event +// holds type information (inputs) about the yielded output. Anonymous events +// don't get the signature canonical representation as the first LOG topic. +type Event struct { + Name string + Anonymous bool + Inputs Arguments +} + +func (e Event) String() string { + inputs := make([]string, len(e.Inputs)) + for i, input := range e.Inputs { + inputs[i] = fmt.Sprintf("%v %v", input.Name, input.Type) + if input.Indexed { + inputs[i] = fmt.Sprintf("%v indexed %v", input.Name, input.Type) + } + } + return fmt.Sprintf("e %v(%v)", e.Name, strings.Join(inputs, ", ")) +} + +// Id returns the canonical representation of the event's signature used by the +// abi definition to identify event names and types. +func (e Event) Id() common.Hash { + types := make([]string, len(e.Inputs)) + i := 0 + for _, input := range e.Inputs { + types[i] = input.Type.String() + i++ + } + + sha := sha3.NewLegacyKeccak256() + sha.Write([]byte(fmt.Sprintf("%v(%v)", e.Name, strings.Join(types, ",")))) + return common.BytesToHash(sha.Sum(nil)) +} diff --git a/ethereum/core/accounts/abi/method.go b/ethereum/core/accounts/abi/method.go new file mode 100644 index 0000000..192f089 --- /dev/null +++ b/ethereum/core/accounts/abi/method.go @@ -0,0 +1,78 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package abi + +import ( + "fmt" + "golang.org/x/crypto/sha3" + "strings" +) + +// Method represents a callable given a `Name` and whether the method is a constant. +// If the method is `Const` no transaction needs to be created for this +// particular Method call. It can easily be simulated using a local VM. +// For example a `Balance()` method only needs to retrieve something +// from the storage and therefor requires no Tx to be send to the +// network. A method such as `Transact` does require a Tx and thus will +// be flagged `true`. +// Input specifies the required input parameters for this gives method. +type Method struct { + Name string + Const bool + Inputs Arguments + Outputs Arguments +} + +// Sig returns the methods string signature according to the ABI spec. +// +// Example +// +// function foo(uint32 a, int b) = "foo(uint32,int256)" +// +// Please note that "int" is substitute for its canonical representation "int256" +func (method Method) Sig() string { + types := make([]string, len(method.Inputs)) + for i, input := range method.Inputs { + types[i] = input.Type.String() + } + return fmt.Sprintf("%v(%v)", method.Name, strings.Join(types, ",")) +} + +func (method Method) String() string { + inputs := make([]string, len(method.Inputs)) + for i, input := range method.Inputs { + inputs[i] = fmt.Sprintf("%v %v", input.Name, input.Type) + } + outputs := make([]string, len(method.Outputs)) + for i, output := range method.Outputs { + if len(output.Name) > 0 { + outputs[i] = fmt.Sprintf("%v ", output.Name) + } + outputs[i] += output.Type.String() + } + constant := "" + if method.Const { + constant = "constant " + } + return fmt.Sprintf("function %v(%v) %sreturns(%v)", method.Name, strings.Join(inputs, ", "), constant, strings.Join(outputs, ", ")) +} + +func (method Method) Id() []byte { + sha := sha3.NewLegacyKeccak256() + sha.Write([]byte(method.Sig())) + return sha.Sum(nil)[:4] +} diff --git a/ethereum/core/accounts/abi/numbers.go b/ethereum/core/accounts/abi/numbers.go new file mode 100644 index 0000000..4d70684 --- /dev/null +++ b/ethereum/core/accounts/abi/numbers.go @@ -0,0 +1,44 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package abi + +import ( + "math/big" + "reflect" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" +) + +var ( + bigT = reflect.TypeOf(&big.Int{}) + derefbigT = reflect.TypeOf(big.Int{}) + uint8T = reflect.TypeOf(uint8(0)) + uint16T = reflect.TypeOf(uint16(0)) + uint32T = reflect.TypeOf(uint32(0)) + uint64T = reflect.TypeOf(uint64(0)) + int8T = reflect.TypeOf(int8(0)) + int16T = reflect.TypeOf(int16(0)) + int32T = reflect.TypeOf(int32(0)) + int64T = reflect.TypeOf(int64(0)) + addressT = reflect.TypeOf(common.Address{}) +) + +// U256 converts a big Int into a 256bit EVM number. +func U256(n *big.Int) []byte { + return math.PaddedBigBytes(math.U256(n), 32) +} diff --git a/ethereum/core/accounts/abi/pack.go b/ethereum/core/accounts/abi/pack.go new file mode 100644 index 0000000..36c5826 --- /dev/null +++ b/ethereum/core/accounts/abi/pack.go @@ -0,0 +1,81 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package abi + +import ( + "math/big" + "reflect" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" +) + +// packBytesSlice packs the given bytes as [L, V] as the canonical representation +// bytes slice +func packBytesSlice(bytes []byte, l int) []byte { + len := packNum(reflect.ValueOf(l)) + return append(len, common.RightPadBytes(bytes, (l+31)/32*32)...) +} + +// packElement packs the given reflect value according to the abi specification in +// t. +func packElement(t Type, reflectValue reflect.Value) []byte { + switch t.T { + case IntTy, UintTy: + return packNum(reflectValue) + case StringTy: + return packBytesSlice([]byte(reflectValue.String()), reflectValue.Len()) + case AddressTy: + if reflectValue.Kind() == reflect.Array { + reflectValue = mustArrayToByteSlice(reflectValue) + } + + return common.LeftPadBytes(reflectValue.Bytes(), 32) + case BoolTy: + if reflectValue.Bool() { + return math.PaddedBigBytes(common.Big1, 32) + } + return math.PaddedBigBytes(common.Big0, 32) + case BytesTy: + if reflectValue.Kind() == reflect.Array { + reflectValue = mustArrayToByteSlice(reflectValue) + } + return packBytesSlice(reflectValue.Bytes(), reflectValue.Len()) + case FixedBytesTy, FunctionTy: + if reflectValue.Kind() == reflect.Array { + reflectValue = mustArrayToByteSlice(reflectValue) + } + return common.RightPadBytes(reflectValue.Bytes(), 32) + default: + panic("abi: fatal error") + } +} + +// packNum packs the given number (using the reflect value) and will cast it to appropriate number representation +func packNum(value reflect.Value) []byte { + switch kind := value.Kind(); kind { + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return U256(new(big.Int).SetUint64(value.Uint())) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return U256(big.NewInt(value.Int())) + case reflect.Ptr: + return U256(value.Interface().(*big.Int)) + default: + panic("abi: fatal error") + } + +} diff --git a/ethereum/core/accounts/abi/reflect.go b/ethereum/core/accounts/abi/reflect.go new file mode 100644 index 0000000..0193517 --- /dev/null +++ b/ethereum/core/accounts/abi/reflect.go @@ -0,0 +1,212 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package abi + +import ( + "fmt" + "reflect" + "strings" +) + +// indirect recursively dereferences the value until it either gets the value +// or finds a big.Int +func indirect(v reflect.Value) reflect.Value { + if v.Kind() == reflect.Ptr && v.Elem().Type() != derefbigT { + return indirect(v.Elem()) + } + return v +} + +// reflectIntKind returns the reflect using the given size and +// unsignedness. +func reflectIntKindAndType(unsigned bool, size int) (reflect.Kind, reflect.Type) { + switch size { + case 8: + if unsigned { + return reflect.Uint8, uint8T + } + return reflect.Int8, int8T + case 16: + if unsigned { + return reflect.Uint16, uint16T + } + return reflect.Int16, int16T + case 32: + if unsigned { + return reflect.Uint32, uint32T + } + return reflect.Int32, int32T + case 64: + if unsigned { + return reflect.Uint64, uint64T + } + return reflect.Int64, int64T + } + return reflect.Ptr, bigT +} + +// mustArrayToBytesSlice creates a new byte slice with the exact same size as value +// and copies the bytes in value to the new slice. +func mustArrayToByteSlice(value reflect.Value) reflect.Value { + slice := reflect.MakeSlice(reflect.TypeOf([]byte{}), value.Len(), value.Len()) + reflect.Copy(slice, value) + return slice +} + +// set attempts to assign src to dst by either setting, copying or otherwise. +// +// set is a bit more lenient when it comes to assignment and doesn't force an as +// strict ruleset as bare `reflect` does. +func set(dst, src reflect.Value, output Argument) error { + dstType := dst.Type() + srcType := src.Type() + switch { + case dstType.AssignableTo(srcType): + dst.Set(src) + case dstType.Kind() == reflect.Interface: + dst.Set(src) + case dstType.Kind() == reflect.Ptr: + return set(dst.Elem(), src, output) + default: + return fmt.Errorf("abi: cannot unmarshal %v in to %v", src.Type(), dst.Type()) + } + return nil +} + +// requireAssignable assures that `dest` is a pointer and it's not an interface. +func requireAssignable(dst, src reflect.Value) error { + if dst.Kind() != reflect.Ptr && dst.Kind() != reflect.Interface { + return fmt.Errorf("abi: cannot unmarshal %v into %v", src.Type(), dst.Type()) + } + return nil +} + +// requireUnpackKind verifies preconditions for unpacking `args` into `kind` +func requireUnpackKind(v reflect.Value, t reflect.Type, k reflect.Kind, + args Arguments) error { + + switch k { + case reflect.Struct: + case reflect.Slice, reflect.Array: + if minLen := args.LengthNonIndexed(); v.Len() < minLen { + return fmt.Errorf("abi: insufficient number of elements in the list/array for unpack, want %d, got %d", + minLen, v.Len()) + } + default: + return fmt.Errorf("abi: cannot unmarshal tuple into %v", t) + } + return nil +} + +// mapAbiToStringField maps abi to struct fields. +// first round: for each Exportable field that contains a `abi:""` tag +// and this field name exists in the arguments, pair them together. +// second round: for each argument field that has not been already linked, +// find what variable is expected to be mapped into, if it exists and has not been +// used, pair them. +func mapAbiToStructFields(args Arguments, value reflect.Value) (map[string]string, error) { + + typ := value.Type() + + abi2struct := make(map[string]string) + struct2abi := make(map[string]string) + + // first round ~~~ + for i := 0; i < typ.NumField(); i++ { + structFieldName := typ.Field(i).Name + + // skip private struct fields. + if structFieldName[:1] != strings.ToUpper(structFieldName[:1]) { + continue + } + + // skip fields that have no abi:"" tag. + var ok bool + var tagName string + if tagName, ok = typ.Field(i).Tag.Lookup("abi"); !ok { + continue + } + + // check if tag is empty. + if tagName == "" { + return nil, fmt.Errorf("struct: abi tag in '%s' is empty", structFieldName) + } + + // check which argument field matches with the abi tag. + found := false + for _, abiField := range args.NonIndexed() { + if abiField.Name == tagName { + if abi2struct[abiField.Name] != "" { + return nil, fmt.Errorf("struct: abi tag in '%s' already mapped", structFieldName) + } + // pair them + abi2struct[abiField.Name] = structFieldName + struct2abi[structFieldName] = abiField.Name + found = true + } + } + + // check if this tag has been mapped. + if !found { + return nil, fmt.Errorf("struct: abi tag '%s' defined but not found in abi", tagName) + } + + } + + // second round ~~~ + for _, arg := range args { + + abiFieldName := arg.Name + structFieldName := capitalise(abiFieldName) + + if structFieldName == "" { + return nil, fmt.Errorf("abi: purely underscored output cannot unpack to struct") + } + + // this abi has already been paired, skip it... unless there exists another, yet unassigned + // struct field with the same field name. If so, raise an error: + // abi: [ { "name": "value" } ] + // struct { Value *big.Int , Value1 *big.Int `abi:"value"`} + if abi2struct[abiFieldName] != "" { + if abi2struct[abiFieldName] != structFieldName && + struct2abi[structFieldName] == "" && + value.FieldByName(structFieldName).IsValid() { + return nil, fmt.Errorf("abi: multiple variables maps to the same abi field '%s'", abiFieldName) + } + continue + } + + // return an error if this struct field has already been paired. + if struct2abi[structFieldName] != "" { + return nil, fmt.Errorf("abi: multiple outputs mapping to the same struct field '%s'", structFieldName) + } + + if value.FieldByName(structFieldName).IsValid() { + // pair them + abi2struct[abiFieldName] = structFieldName + struct2abi[structFieldName] = abiFieldName + } else { + // not paired, but annotate as used, to detect cases like + // abi : [ { "name": "value" }, { "name": "_value" } ] + // struct { Value *big.Int } + struct2abi[structFieldName] = abiFieldName + } + + } + + return abi2struct, nil +} diff --git a/ethereum/core/accounts/abi/type.go b/ethereum/core/accounts/abi/type.go new file mode 100644 index 0000000..9de36da --- /dev/null +++ b/ethereum/core/accounts/abi/type.go @@ -0,0 +1,204 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package abi + +import ( + "fmt" + "reflect" + "regexp" + "strconv" + "strings" +) + +// Type enumerator +const ( + IntTy byte = iota + UintTy + BoolTy + StringTy + SliceTy + ArrayTy + AddressTy + FixedBytesTy + BytesTy + HashTy + FixedPointTy + FunctionTy +) + +// Type is the reflection of the supported argument type +type Type struct { + Elem *Type + + Kind reflect.Kind + Type reflect.Type + Size int + T byte // Our own type checking + + stringKind string // holds the unparsed string for deriving signatures +} + +var ( + // typeRegex parses the abi sub types + typeRegex = regexp.MustCompile("([a-zA-Z]+)(([0-9]+)(x([0-9]+))?)?") +) + +// NewType creates a new reflection type of abi type given in t. +func NewType(t string) (typ Type, err error) { + // check that array brackets are equal if they exist + if strings.Count(t, "[") != strings.Count(t, "]") { + return Type{}, fmt.Errorf("invalid arg type in abi") + } + + typ.stringKind = t + + // if there are brackets, get ready to go into slice/array mode and + // recursively create the type + if strings.Count(t, "[") != 0 { + i := strings.LastIndex(t, "[") + // recursively embed the type + embeddedType, err := NewType(t[:i]) + if err != nil { + return Type{}, err + } + // grab the last cell and create a type from there + sliced := t[i:] + // grab the slice size with regexp + re := regexp.MustCompile("[0-9]+") + intz := re.FindAllString(sliced, -1) + + if len(intz) == 0 { + // is a slice + typ.T = SliceTy + typ.Kind = reflect.Slice + typ.Elem = &embeddedType + typ.Type = reflect.SliceOf(embeddedType.Type) + } else if len(intz) == 1 { + // is a array + typ.T = ArrayTy + typ.Kind = reflect.Array + typ.Elem = &embeddedType + typ.Size, err = strconv.Atoi(intz[0]) + if err != nil { + return Type{}, fmt.Errorf("abi: error parsing variable size: %v", err) + } + typ.Type = reflect.ArrayOf(typ.Size, embeddedType.Type) + } else { + return Type{}, fmt.Errorf("invalid formatting of array type") + } + return typ, err + } + // parse the type and size of the abi-type. + parsedType := typeRegex.FindAllStringSubmatch(t, -1)[0] + // varSize is the size of the variable + var varSize int + if len(parsedType[3]) > 0 { + var err error + varSize, err = strconv.Atoi(parsedType[2]) + if err != nil { + return Type{}, fmt.Errorf("abi: error parsing variable size: %v", err) + } + } else { + if parsedType[0] == "uint" || parsedType[0] == "int" { + // this should fail because it means that there's something wrong with + // the abi type (the compiler should always format it to the size...always) + return Type{}, fmt.Errorf("unsupported arg type: %s", t) + } + } + // varType is the parsed abi type + switch varType := parsedType[1]; varType { + case "int": + typ.Kind, typ.Type = reflectIntKindAndType(false, varSize) + typ.Size = varSize + typ.T = IntTy + case "uint": + typ.Kind, typ.Type = reflectIntKindAndType(true, varSize) + typ.Size = varSize + typ.T = UintTy + case "bool": + typ.Kind = reflect.Bool + typ.T = BoolTy + typ.Type = reflect.TypeOf(bool(false)) + case "address": + typ.Kind = reflect.Array + typ.Type = addressT + typ.Size = 20 + typ.T = AddressTy + case "string": + typ.Kind = reflect.String + typ.Type = reflect.TypeOf("") + typ.T = StringTy + case "bytes": + if varSize == 0 { + typ.T = BytesTy + typ.Kind = reflect.Slice + typ.Type = reflect.SliceOf(reflect.TypeOf(byte(0))) + } else { + typ.T = FixedBytesTy + typ.Kind = reflect.Array + typ.Size = varSize + typ.Type = reflect.ArrayOf(varSize, reflect.TypeOf(byte(0))) + } + case "function": + typ.Kind = reflect.Array + typ.T = FunctionTy + typ.Size = 24 + typ.Type = reflect.ArrayOf(24, reflect.TypeOf(byte(0))) + default: + return Type{}, fmt.Errorf("unsupported arg type: %s", t) + } + + return +} + +// String implements Stringer +func (t Type) String() (out string) { + return t.stringKind +} + +func (t Type) pack(v reflect.Value) ([]byte, error) { + // dereference pointer first if it's a pointer + v = indirect(v) + + if err := typeCheck(t, v); err != nil { + return nil, err + } + + if t.T == SliceTy || t.T == ArrayTy { + var packed []byte + + for i := 0; i < v.Len(); i++ { + val, err := t.Elem.pack(v.Index(i)) + if err != nil { + return nil, err + } + packed = append(packed, val...) + } + if t.T == SliceTy { + return packBytesSlice(packed, v.Len()), nil + } else if t.T == ArrayTy { + return packed, nil + } + } + return packElement(t, v), nil +} + +// requireLengthPrefix returns whether the type requires any sort of length +// prefixing. +func (t Type) requiresLengthPrefix() bool { + return t.T == StringTy || t.T == BytesTy || t.T == SliceTy +} diff --git a/ethereum/core/accounts/abi/unpack.go b/ethereum/core/accounts/abi/unpack.go new file mode 100644 index 0000000..793d515 --- /dev/null +++ b/ethereum/core/accounts/abi/unpack.go @@ -0,0 +1,230 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package abi + +import ( + "encoding/binary" + "fmt" + "math/big" + "reflect" + + "github.com/ethereum/go-ethereum/common" +) + +// reads the integer based on its kind +func readInteger(kind reflect.Kind, b []byte) interface{} { + switch kind { + case reflect.Uint8: + return b[len(b)-1] + case reflect.Uint16: + return binary.BigEndian.Uint16(b[len(b)-2:]) + case reflect.Uint32: + return binary.BigEndian.Uint32(b[len(b)-4:]) + case reflect.Uint64: + return binary.BigEndian.Uint64(b[len(b)-8:]) + case reflect.Int8: + return int8(b[len(b)-1]) + case reflect.Int16: + return int16(binary.BigEndian.Uint16(b[len(b)-2:])) + case reflect.Int32: + return int32(binary.BigEndian.Uint32(b[len(b)-4:])) + case reflect.Int64: + return int64(binary.BigEndian.Uint64(b[len(b)-8:])) + default: + return new(big.Int).SetBytes(b) + } +} + +// reads a bool +func readBool(word []byte) (bool, error) { + for _, b := range word[:31] { + if b != 0 { + return false, errBadBool + } + } + switch word[31] { + case 0: + return false, nil + case 1: + return true, nil + default: + return false, errBadBool + } +} + +// A function type is simply the address with the function selection signature at the end. +// This enforces that standard by always presenting it as a 24-array (address + sig = 24 bytes) +func readFunctionType(t Type, word []byte) (funcTy [24]byte, err error) { + if t.T != FunctionTy { + return [24]byte{}, fmt.Errorf("abi: invalid type in call to make function type byte array") + } + if garbage := binary.BigEndian.Uint64(word[24:32]); garbage != 0 { + err = fmt.Errorf("abi: got improperly encoded function type, got %v", word) + } else { + copy(funcTy[:], word[0:24]) + } + return +} + +// through reflection, creates a fixed array to be read from +func readFixedBytes(t Type, word []byte) (interface{}, error) { + if t.T != FixedBytesTy { + return nil, fmt.Errorf("abi: invalid type in call to make fixed byte array") + } + // convert + array := reflect.New(t.Type).Elem() + + reflect.Copy(array, reflect.ValueOf(word[0:t.Size])) + return array.Interface(), nil + +} + +func getFullElemSize(elem *Type) int { + //all other should be counted as 32 (slices have pointers to respective elements) + size := 32 + //arrays wrap it, each element being the same size + for elem.T == ArrayTy { + size *= elem.Size + elem = elem.Elem + } + return size +} + +// iteratively unpack elements +func forEachUnpack(t Type, output []byte, start, size int) (interface{}, error) { + if size < 0 { + return nil, fmt.Errorf("cannot marshal input to array, size is negative (%d)", size) + } + if start+32*size > len(output) { + return nil, fmt.Errorf("abi: cannot marshal in to go array: offset %d would go over slice boundary (len=%d)", len(output), start+32*size) + } + + // this value will become our slice or our array, depending on the type + var refSlice reflect.Value + + if t.T == SliceTy { + // declare our slice + refSlice = reflect.MakeSlice(t.Type, size, size) + } else if t.T == ArrayTy { + // declare our array + refSlice = reflect.New(t.Type).Elem() + } else { + return nil, fmt.Errorf("abi: invalid type in array/slice unpacking stage") + } + + // Arrays have packed elements, resulting in longer unpack steps. + // Slices have just 32 bytes per element (pointing to the contents). + elemSize := 32 + if t.T == ArrayTy { + elemSize = getFullElemSize(t.Elem) + } + + for i, j := start, 0; j < size; i, j = i+elemSize, j+1 { + + inter, err := toGoType(i, *t.Elem, output) + if err != nil { + return nil, err + } + + // append the item to our reflect slice + refSlice.Index(j).Set(reflect.ValueOf(inter)) + } + + // return the interface + return refSlice.Interface(), nil +} + +// toGoType parses the output bytes and recursively assigns the value of these bytes +// into a go type with accordance with the ABI spec. +func toGoType(index int, t Type, output []byte) (interface{}, error) { + if index+32 > len(output) { + return nil, fmt.Errorf("abi: cannot marshal in to go type: length insufficient %d require %d", len(output), index+32) + } + + var ( + returnOutput []byte + begin, end int + err error + ) + + // if we require a length prefix, find the beginning word and size returned. + if t.requiresLengthPrefix() { + begin, end, err = lengthPrefixPointsTo(index, output) + if err != nil { + return nil, err + } + } else { + returnOutput = output[index : index+32] + } + + switch t.T { + case SliceTy: + return forEachUnpack(t, output, begin, end) + case ArrayTy: + return forEachUnpack(t, output, index, t.Size) + case StringTy: // variable arrays are written at the end of the return bytes + return string(output[begin : begin+end]), nil + case IntTy, UintTy: + return readInteger(t.Kind, returnOutput), nil + case BoolTy: + return readBool(returnOutput) + case AddressTy: + return common.BytesToAddress(returnOutput), nil + case HashTy: + return common.BytesToHash(returnOutput), nil + case BytesTy: + return output[begin : begin+end], nil + case FixedBytesTy: + return readFixedBytes(t, returnOutput) + case FunctionTy: + return readFunctionType(t, returnOutput) + default: + return nil, fmt.Errorf("abi: unknown type %v", t.T) + } +} + +// interprets a 32 byte slice as an offset and then determines which indice to look to decode the type. +func lengthPrefixPointsTo(index int, output []byte) (start int, length int, err error) { + bigOffsetEnd := big.NewInt(0).SetBytes(output[index : index+32]) + bigOffsetEnd.Add(bigOffsetEnd, common.Big32) + outputLength := big.NewInt(int64(len(output))) + + if bigOffsetEnd.Cmp(outputLength) > 0 { + return 0, 0, fmt.Errorf("abi: cannot marshal in to go slice: offset %v would go over slice boundary (len=%v)", bigOffsetEnd, outputLength) + } + + if bigOffsetEnd.BitLen() > 63 { + return 0, 0, fmt.Errorf("abi offset larger than int64: %v", bigOffsetEnd) + } + + offsetEnd := int(bigOffsetEnd.Uint64()) + lengthBig := big.NewInt(0).SetBytes(output[offsetEnd-32 : offsetEnd]) + + totalSize := big.NewInt(0) + totalSize.Add(totalSize, bigOffsetEnd) + totalSize.Add(totalSize, lengthBig) + if totalSize.BitLen() > 63 { + return 0, 0, fmt.Errorf("abi length larger than int64: %v", totalSize) + } + + if totalSize.Cmp(outputLength) > 0 { + return 0, 0, fmt.Errorf("abi: cannot marshal in to go type: length insufficient %v require %v", outputLength, totalSize) + } + start = int(bigOffsetEnd.Uint64()) + length = int(lengthBig.Uint64()) + return +} diff --git a/ethereum/geth/schema.go b/ethereum/geth/schema.go new file mode 100644 index 0000000..16a14b4 --- /dev/null +++ b/ethereum/geth/schema.go @@ -0,0 +1,114 @@ +package geth + +import ( + "github.com/tenderly/tenderly-cli/ethereum" + "github.com/tenderly/tenderly-cli/jsonrpc2" +) + +var DefaultSchema = Schema{ + ValueEth: ethSchema{}, + ValueNet: netSchema{}, + ValueTrace: trace{}, + ValuePubSub: pubSubSchema{}, +} + +type Schema struct { + ValueEth ethereum.EthSchema + ValueNet ethereum.NetSchema + ValueTrace ethereum.TraceSchema + ValuePubSub ethereum.PubSubSchema +} + +func (s *Schema) Eth() ethereum.EthSchema { + return s.ValueEth +} + +func (s *Schema) Net() ethereum.NetSchema { + return s.ValueNet +} + +func (s *Schema) Trace() ethereum.TraceSchema { + return s.ValueTrace +} + +func (s *Schema) PubSub() ethereum.PubSubSchema { + return s.ValuePubSub +} + +// Eth + +type ethSchema struct { +} + +func (ethSchema) BlockNumber() (*jsonrpc2.Request, *ethereum.Number) { + var num ethereum.Number + + return jsonrpc2.NewRequest("eth_blockNumber"), &num +} + +func (ethSchema) GetBlockByNumber(num ethereum.Number) (*jsonrpc2.Request, ethereum.Block) { + var block Block + + return jsonrpc2.NewRequest("eth_getBlockByNumber", num.Hex(), true), &block +} + +func (ethSchema) GetTransaction(hash string) (*jsonrpc2.Request, ethereum.Transaction) { + var t Transaction + + return jsonrpc2.NewRequest("eth_getTransactionByHash", hash), &t +} + +func (ethSchema) GetTransactionReceipt(hash string) (*jsonrpc2.Request, ethereum.TransactionReceipt) { + var receipt TransactionReceipt + + return jsonrpc2.NewRequest("eth_getTransactionReceipt", hash), &receipt +} + +// Net + +type netSchema struct { +} + +func (netSchema) Version() (*jsonrpc2.Request, *string) { + var v string + + return jsonrpc2.NewRequest("net_version"), &v +} + +// States + +type trace struct { +} + +func (trace) VMTrace(hash string) (*jsonrpc2.Request, ethereum.TransactionStates) { + var trace TraceResult + + return jsonrpc2.NewRequest("debug_traceTransaction", hash, struct{}{}), &trace +} +func (trace) CallTrace(hash string) (*jsonrpc2.Request, ethereum.CallTraces) { + var trace CallTrace + + return jsonrpc2.NewRequest("debug_traceTransaction", hash, map[string]string{"tracer": "callTracer"}), &trace +} + +// PubSub + +type PubSubSchema interface { + Subscribe() (*jsonrpc2.Request, *ethereum.SubscriptionID) + Unsubscribe(id ethereum.SubscriptionID) (*jsonrpc2.Request, *ethereum.UnsubscribeSuccess) +} + +type pubSubSchema struct { +} + +func (pubSubSchema) Subscribe() (*jsonrpc2.Request, *ethereum.SubscriptionID) { + id := ethereum.NewNilSubscriptionID() + + return jsonrpc2.NewRequest("eth_subscribe", "newHeads"), &id +} + +func (pubSubSchema) Unsubscribe(id ethereum.SubscriptionID) (*jsonrpc2.Request, *ethereum.UnsubscribeSuccess) { + var success ethereum.UnsubscribeSuccess + + return jsonrpc2.NewRequest("eth_unsubscribe", id.String()), &success +} diff --git a/ethereum/geth/types.go b/ethereum/geth/types.go new file mode 100644 index 0000000..62583f1 --- /dev/null +++ b/ethereum/geth/types.go @@ -0,0 +1,279 @@ +package geth + +import ( + "encoding/json" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/tenderly/tenderly-cli/ethereum" +) + +// Core Types + +type Header struct { + HNumber *ethereum.Number `json:"number"` +} + +func (h *Header) Number() *ethereum.Number { + return h.HNumber +} + +type Block struct { + ValuesTransactions []*Transaction `json:"transactions"` +} + +func (b Block) Transactions() []ethereum.Transaction { + transactions := make([]ethereum.Transaction, len(b.ValuesTransactions)) + for k, v := range b.ValuesTransactions { + transactions[k] = v + } + + return transactions +} + +type Transaction struct { + ValueHash *common.Hash `json:"hash"` + ValueFrom *common.Address `json:"from"` + ValueTo *common.Address `json:"to"` + ValueInput hexutil.Bytes `json:"input"` + ValueValue *hexutil.Big `json:"value"` + ValueGas *hexutil.Big `json:"gas"` + ValueGasPrice *hexutil.Big `json:"gasPrice"` + ValueBlockNumber string `json:"blockNumber"` +} + +func (t *Transaction) Hash() *common.Hash { + return t.ValueHash +} + +func (t *Transaction) From() *common.Address { + return t.ValueFrom +} + +func (t *Transaction) To() *common.Address { + return t.ValueTo +} + +func (t *Transaction) Input() hexutil.Bytes { + return t.ValueInput +} + +func (t *Transaction) Value() *hexutil.Big { + return t.ValueValue +} + +func (t *Transaction) Gas() *hexutil.Big { + return t.ValueGas +} + +func (t *Transaction) GasPrice() *hexutil.Big { + return t.ValueGasPrice +} + +type Log struct { + ValueAddress string `json:"address"` + ValueBlockHash string `json:"blockHash"` + ValueBlockNumber string `json:"blockNumber"` + ValueData string `json:"data"` + ValueLogIndex string `json:"logIndex"` + ValueRemoved bool `json:"removed"` + ValueTopics []string `json:"topics"` + ValueTransactionHash string `json:"transactionHash"` + ValueTransactionIndex string `json:"transactionIndex"` + ValueTransactionLogIndex string `json:"transactionLogIndex"` + ValueType string `json:"type"` +} + +func (l *Log) Data() string { + return l.ValueData +} + +func (l *Log) Topics() []string { + return l.ValueTopics +} + +type TransactionReceipt struct { + TFrom string `json:"from"` + TTo string `json:"to"` + + TGasUsed *hexutil.Big `json:"gasUsed"` + TCumulativeGasUsed *hexutil.Big `json:"cumulativeGasUsed"` + TContractAddress *common.Address `json:"contractAddress"` + + TStatus *ethereum.Number `json:"status"` // Can be null, if null do a check anyways. 0x0 fail, 0x1 success + TLogs []*Log `json:"logs"` +} + +func (t *TransactionReceipt) From() string { + return t.TFrom +} + +func (t *TransactionReceipt) To() string { + return t.TTo +} + +func (t *TransactionReceipt) GasUsed() *hexutil.Big { + return t.TGasUsed +} + +func (t *TransactionReceipt) CumulativeGasUsed() *hexutil.Big { + return t.TCumulativeGasUsed +} + +func (t *TransactionReceipt) ContractAddress() *common.Address { + return t.TContractAddress +} + +func (t *TransactionReceipt) Status() *ethereum.Number { + return t.TStatus +} + +func (t *TransactionReceipt) Logs() []ethereum.Log { + var logs []ethereum.Log + + for _, log := range t.TLogs { + logs = append(logs, log) + } + + return logs +} + +// States Types + +type EvmState struct { + ValuePc uint64 `json:"pc"` + ValueOp string `json:"op"` + ValueGas uint64 `json:"gas"` + ValueGasCost int64 `json:"gasCost"` + ValueDepth int `json:"depth"` + ValueError json.RawMessage `json:"error,omitempty"` + ValueStack *[]string `json:"stack,omitempty"` + ValueMemory *[]string `json:"memory,omitempty"` + ValueStorage *map[string]string `json:"storage,omitempty"` +} + +func (s *EvmState) Pc() uint64 { + return s.ValuePc +} + +func (s *EvmState) Depth() int { + return s.ValueDepth +} + +func (s *EvmState) Op() string { + return s.ValueOp +} + +func (s *EvmState) Stack() []string { + return *s.ValueStack +} + +type TraceResult struct { + Gas uint64 `json:"gas"` + Failed bool `json:"failed"` + ReturnValue string `json:"returnValue"` + StructLogs []*EvmState `json:"structLogs"` +} + +type CallTrace struct { + ValueHash *common.Hash `json:"hash"` + ValueParentHash *common.Hash `json:"parentHash"` + ValueTransactionHash *common.Hash `json:"transactionHash"` + ValueType string `json:"type"` + ValueFrom common.Address `json:"from"` + ValueTo common.Address `json:"to"` + ValueInput hexutil.Bytes `json:"input"` + ValueOutput hexutil.Bytes `json:"output"` + ValueGas *hexutil.Uint64 `json:"gas,omitempty"` + ValueGasUsed *hexutil.Uint64 `json:"gasUsed,omitempty"` + ValueValue *hexutil.Big `json:"value,omitempty"` + ValueError string `json:"error,omitempty"` + ValueCalls []CallTrace `json:"calls,omitempty"` +} + +func (c *CallTrace) Hash() *common.Hash { + return c.ValueHash +} + +func (c *CallTrace) ParentHash() *common.Hash { + return c.ValueParentHash +} + +func (c *CallTrace) TransactionHash() *common.Hash { + return c.ValueTransactionHash +} + +func (c *CallTrace) Type() string { + return c.ValueType +} + +func (c *CallTrace) From() common.Address { + return c.ValueFrom +} + +func (c *CallTrace) To() common.Address { + return c.ValueTo +} + +func (c *CallTrace) Input() hexutil.Bytes { + return c.ValueInput +} + +func (c *CallTrace) Output() hexutil.Bytes { + return c.ValueOutput +} + +func (c *CallTrace) Gas() *hexutil.Uint64 { + return c.ValueGas +} + +func (c *CallTrace) GasUsed() *hexutil.Uint64 { + return c.ValueGasUsed +} + +func (c *CallTrace) Value() *hexutil.Big { + return c.ValueValue +} + +func (c *CallTrace) Error() string { + return c.ValueError +} + +func (c *CallTrace) Traces() []ethereum.Trace { + ch := make(chan *CallTrace) + Walk(c, ch) + + var traces []ethereum.Trace + for callTrace := range ch { + traces = append(traces, callTrace) + } + + return traces +} + +func Walk(c *CallTrace, ch chan *CallTrace) { + if c == nil { + return + } + ch <- c + for _, callTrace := range c.ValueCalls { + Walk(&callTrace, ch) + } +} + +func (gtr *TraceResult) States() []ethereum.EvmState { + traces := make([]ethereum.EvmState, len(gtr.StructLogs)) + for k, v := range gtr.StructLogs { + traces[k] = v + } + + return traces +} + +func (gtr *TraceResult) ProcessTrace() { +} + +type SubscriptionResult struct { + Subscription ethereum.SubscriptionID `json:"subscription"` + Result Header `json:"result"` +} diff --git a/ethereum/opcodes.go b/ethereum/opcodes.go new file mode 100644 index 0000000..5cd6ac7 --- /dev/null +++ b/ethereum/opcodes.go @@ -0,0 +1,379 @@ +package ethereum + +import ( + "fmt" +) + +// OpCode is an EVM opcode +type OpCode byte + +// IsPush specifies if an opcode is a PUSH opcode. +func (op OpCode) IsPush() bool { + switch op { + case PUSH1, PUSH2, PUSH3, PUSH4, PUSH5, PUSH6, PUSH7, PUSH8, PUSH9, PUSH10, PUSH11, PUSH12, PUSH13, PUSH14, PUSH15, PUSH16, PUSH17, PUSH18, PUSH19, PUSH20, PUSH21, PUSH22, PUSH23, PUSH24, PUSH25, PUSH26, PUSH27, PUSH28, PUSH29, PUSH30, PUSH31, PUSH32: + return true + } + return false +} + +func (op OpCode) PushSize() uint64 { + return uint64(op - PUSH1 + 1) +} + +// IsStaticJump specifies if an opcode is JUMP. +func (op OpCode) IsStaticJump() bool { + return op == JUMP +} + +// 0x0 range - arithmetic ops. +const ( + STOP OpCode = iota + ADD + MUL + SUB + DIV + SDIV + MOD + SMOD + ADDMOD + MULMOD + EXP + SIGNEXTEND +) + +// 0x10 range - comparison ops. +const ( + LT OpCode = iota + 0x10 + GT + SLT + SGT + EQ + ISZERO + AND + OR + XOR + NOT + BYTE + SHL + SHR + SAR + + SHA3 = 0x20 +) + +// 0x30 range - closure state. +const ( + ADDRESS OpCode = 0x30 + iota + BALANCE + ORIGIN + CALLER + CALLVALUE + CALLDATALOAD + CALLDATASIZE + CALLDATACOPY + CODESIZE + CODECOPY + GASPRICE + EXTCODESIZE + EXTCODECOPY + RETURNDATASIZE + RETURNDATACOPY +) + +// 0x40 range - block operations. +const ( + BLOCKHASH OpCode = 0x40 + iota + COINBASE + TIMESTAMP + NUMBER + DIFFICULTY + GASLIMIT +) + +// 0x50 range - 'storage' and execution. +const ( + POP OpCode = 0x50 + iota + MLOAD + MSTORE + MSTORE8 + SLOAD + SSTORE + JUMP + JUMPI + PC + MSIZE + GAS + JUMPDEST +) + +// 0x60 range. +const ( + PUSH1 OpCode = 0x60 + iota + PUSH2 + PUSH3 + PUSH4 + PUSH5 + PUSH6 + PUSH7 + PUSH8 + PUSH9 + PUSH10 + PUSH11 + PUSH12 + PUSH13 + PUSH14 + PUSH15 + PUSH16 + PUSH17 + PUSH18 + PUSH19 + PUSH20 + PUSH21 + PUSH22 + PUSH23 + PUSH24 + PUSH25 + PUSH26 + PUSH27 + PUSH28 + PUSH29 + PUSH30 + PUSH31 + PUSH32 + DUP1 + DUP2 + DUP3 + DUP4 + DUP5 + DUP6 + DUP7 + DUP8 + DUP9 + DUP10 + DUP11 + DUP12 + DUP13 + DUP14 + DUP15 + DUP16 + SWAP1 + SWAP2 + SWAP3 + SWAP4 + SWAP5 + SWAP6 + SWAP7 + SWAP8 + SWAP9 + SWAP10 + SWAP11 + SWAP12 + SWAP13 + SWAP14 + SWAP15 + SWAP16 +) + +// 0xa0 range - logging ops. +const ( + LOG0 OpCode = 0xa0 + iota + LOG1 + LOG2 + LOG3 + LOG4 +) + +// unofficial opcodes used for parsing. +const ( + PUSH OpCode = 0xb0 + iota + DUP + SWAP +) + +// 0xf0 range - closures. +const ( + CREATE OpCode = 0xf0 + iota + CALL + CALLCODE + RETURN + DELEGATECALL + STATICCALL = 0xfa + + REVERT = 0xfd + INVALID_OPCODE = 0xfe + SELFDESTRUCT = 0xff +) + +// Since the opcodes aren't all in order we can't use a regular slice. +var opCodeToString = map[OpCode]string{ + // 0x0 range - arithmetic ops. + STOP: "STOP", + ADD: "ADD", + MUL: "MUL", + SUB: "SUB", + DIV: "DIV", + SDIV: "SDIV", + MOD: "MOD", + SMOD: "SMOD", + EXP: "EXP", + NOT: "NOT", + LT: "LT", + GT: "GT", + SLT: "SLT", + SGT: "SGT", + EQ: "EQ", + ISZERO: "ISZERO", + SIGNEXTEND: "SIGNEXTEND", + + // 0x10 range - bit ops. + AND: "AND", + OR: "OR", + XOR: "XOR", + BYTE: "BYTE", + SHL: "SHL", + SHR: "SHR", + SAR: "SAR", + ADDMOD: "ADDMOD", + MULMOD: "MULMOD", + + // 0x20 range - crypto. + SHA3: "SHA3", + + // 0x30 range - closure state. + ADDRESS: "ADDRESS", + BALANCE: "BALANCE", + ORIGIN: "ORIGIN", + CALLER: "CALLER", + CALLVALUE: "CALLVALUE", + CALLDATALOAD: "CALLDATALOAD", + CALLDATASIZE: "CALLDATASIZE", + CALLDATACOPY: "CALLDATACOPY", + CODESIZE: "CODESIZE", + CODECOPY: "CODECOPY", + GASPRICE: "GASPRICE", + EXTCODESIZE: "EXTCODESIZE", + EXTCODECOPY: "EXTCODECOPY", + RETURNDATASIZE: "RETURNDATASIZE", + RETURNDATACOPY: "RETURNDATACOPY", + + // 0x40 range - block operations. + BLOCKHASH: "BLOCKHASH", + COINBASE: "COINBASE", + TIMESTAMP: "TIMESTAMP", + NUMBER: "NUMBER", + DIFFICULTY: "DIFFICULTY", + GASLIMIT: "GASLIMIT", + + // 0x50 range - 'storage' and execution. + POP: "POP", + //DUP: "DUP", + //SWAP: "SWAP", + MLOAD: "MLOAD", + MSTORE: "MSTORE", + MSTORE8: "MSTORE8", + SLOAD: "SLOAD", + SSTORE: "SSTORE", + JUMP: "JUMP", + JUMPI: "JUMPI", + PC: "PC", + MSIZE: "MSIZE", + GAS: "GAS", + JUMPDEST: "JUMPDEST", + + // 0x60 range - push. + PUSH1: "PUSH1", + PUSH2: "PUSH2", + PUSH3: "PUSH3", + PUSH4: "PUSH4", + PUSH5: "PUSH5", + PUSH6: "PUSH6", + PUSH7: "PUSH7", + PUSH8: "PUSH8", + PUSH9: "PUSH9", + PUSH10: "PUSH10", + PUSH11: "PUSH11", + PUSH12: "PUSH12", + PUSH13: "PUSH13", + PUSH14: "PUSH14", + PUSH15: "PUSH15", + PUSH16: "PUSH16", + PUSH17: "PUSH17", + PUSH18: "PUSH18", + PUSH19: "PUSH19", + PUSH20: "PUSH20", + PUSH21: "PUSH21", + PUSH22: "PUSH22", + PUSH23: "PUSH23", + PUSH24: "PUSH24", + PUSH25: "PUSH25", + PUSH26: "PUSH26", + PUSH27: "PUSH27", + PUSH28: "PUSH28", + PUSH29: "PUSH29", + PUSH30: "PUSH30", + PUSH31: "PUSH31", + PUSH32: "PUSH32", + + DUP1: "DUP1", + DUP2: "DUP2", + DUP3: "DUP3", + DUP4: "DUP4", + DUP5: "DUP5", + DUP6: "DUP6", + DUP7: "DUP7", + DUP8: "DUP8", + DUP9: "DUP9", + DUP10: "DUP10", + DUP11: "DUP11", + DUP12: "DUP12", + DUP13: "DUP13", + DUP14: "DUP14", + DUP15: "DUP15", + DUP16: "DUP16", + + SWAP1: "SWAP1", + SWAP2: "SWAP2", + SWAP3: "SWAP3", + SWAP4: "SWAP4", + SWAP5: "SWAP5", + SWAP6: "SWAP6", + SWAP7: "SWAP7", + SWAP8: "SWAP8", + SWAP9: "SWAP9", + SWAP10: "SWAP10", + SWAP11: "SWAP11", + SWAP12: "SWAP12", + SWAP13: "SWAP13", + SWAP14: "SWAP14", + SWAP15: "SWAP15", + SWAP16: "SWAP16", + LOG0: "LOG0", + LOG1: "LOG1", + LOG2: "LOG2", + LOG3: "LOG3", + LOG4: "LOG4", + + // 0xf0 range. + CREATE: "CREATE", + CALL: "CALL", + RETURN: "RETURN", + CALLCODE: "CALLCODE", + DELEGATECALL: "DELEGATECALL", + STATICCALL: "STATICCALL", + REVERT: "REVERT", + INVALID_OPCODE: "INVALID OPCODE", + SELFDESTRUCT: "SELFDESTRUCT", + + PUSH: "PUSH", + DUP: "DUP", + SWAP: "SWAP", +} + +func (op OpCode) String() string { + str := opCodeToString[op] + if len(str) == 0 { + return fmt.Sprintf("Missing opcode 0x%x", int(op)) + } + + return str +} diff --git a/ethereum/parity/schema.go b/ethereum/parity/schema.go new file mode 100644 index 0000000..a32fc41 --- /dev/null +++ b/ethereum/parity/schema.go @@ -0,0 +1,135 @@ +package parity + +import ( + "github.com/tenderly/tenderly-cli/ethereum" + "github.com/tenderly/tenderly-cli/jsonrpc2" +) + +var DefaultSchema = Schema{ + ValueEth: ethSchema{}, + ValueNet: netSchema{}, + ValueTrace: trace{}, + ValuePubSub: pubSubSchema{}, + ValueParity: ParitySchema{}, +} + +type Schema struct { + ValueEth ethereum.EthSchema + ValueNet ethereum.NetSchema + ValueTrace ethereum.TraceSchema + ValuePubSub ethereum.PubSubSchema + ValueParity ParitySchema +} + +func (s *Schema) Eth() ethereum.EthSchema { + return s.ValueEth +} + +func (s *Schema) Net() ethereum.NetSchema { + return s.ValueNet +} + +func (s *Schema) Trace() ethereum.TraceSchema { + return s.ValueTrace +} + +func (s *Schema) PubSub() ethereum.PubSubSchema { + return s.ValuePubSub +} + +func (s *Schema) Parity() ParitySchema { + return s.ValueParity +} + +// Eth + +type ethSchema struct { +} + +func (ethSchema) BlockNumber() (*jsonrpc2.Request, *ethereum.Number) { + var num ethereum.Number + + return jsonrpc2.NewRequest("eth_blockNumber"), &num +} + +func (ethSchema) GetBlockByNumber(num ethereum.Number) (*jsonrpc2.Request, ethereum.Block) { + var block Block + + return jsonrpc2.NewRequest("eth_getBlockByNumber", num.Hex(), true), &block +} + +func (ethSchema) GetTransaction(hash string) (*jsonrpc2.Request, ethereum.Transaction) { + var t Transaction + + return jsonrpc2.NewRequest("eth_getTransactionByHash", hash), &t +} + +func (ethSchema) GetTransactionReceipt(hash string) (*jsonrpc2.Request, ethereum.TransactionReceipt) { + var receipt TransactionReceipt + + return jsonrpc2.NewRequest("eth_getTransactionReceipt", hash), &receipt +} + +// Net + +type netSchema struct { +} + +func (netSchema) Version() (*jsonrpc2.Request, *string) { + var v string + + return jsonrpc2.NewRequest("net_version"), &v +} + +// States + +type trace struct { +} + +func (trace) VMTrace(hash string) (*jsonrpc2.Request, ethereum.TransactionStates) { + var trace TraceResult + + return jsonrpc2.NewRequest("trace_replayTransaction", hash, []string{"vmTrace"}), &trace +} + +func (trace) CallTrace(hash string) (*jsonrpc2.Request, ethereum.CallTraces) { + var trace TraceResult + + return jsonrpc2.NewRequest("trace_replayTransaction", hash, []string{"trace"}), &trace +} + +// PubSub + +type PubSubSchema interface { + Subscribe() (*jsonrpc2.Request, *ethereum.SubscriptionID) + Unsubscribe(id ethereum.SubscriptionID) (*jsonrpc2.Request, *ethereum.UnsubscribeSuccess) +} + +type pubSubSchema struct { +} + +func (pubSubSchema) Subscribe() (*jsonrpc2.Request, *ethereum.SubscriptionID) { + id := ethereum.NewNilSubscriptionID() + + return jsonrpc2.NewRequest("eth_subscribe", "newHeads"), &id +} + +func (pubSubSchema) Unsubscribe(id ethereum.SubscriptionID) (*jsonrpc2.Request, *ethereum.UnsubscribeSuccess) { + var success ethereum.UnsubscribeSuccess + + return jsonrpc2.NewRequest("eth_unsubscribe", id.String()), &success +} + +// Parity Schema + +type ParityVersionInfo struct { +} + +type ParitySchema struct { +} + +func (ParitySchema) VersionInfo() (*jsonrpc2.Request, *ParityVersionInfo) { + var info ParityVersionInfo + + return jsonrpc2.NewRequest("parity_versionInfo"), &info +} diff --git a/ethereum/parity/types.go b/ethereum/parity/types.go new file mode 100644 index 0000000..e4ce8d7 --- /dev/null +++ b/ethereum/parity/types.go @@ -0,0 +1,363 @@ +package parity + +import ( + "encoding/json" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/tenderly/tenderly-cli/ethereum" +) + +// Core Types + +type Header struct { + HNumber *ethereum.Number `json:"number"` +} + +func (h *Header) Number() *ethereum.Number { + return h.HNumber +} + +type Block struct { + ValuesTransactions []*Transaction `json:"transactions"` +} + +func (b *Block) Transactions() []ethereum.Transaction { + if b.ValuesTransactions == nil { + return []ethereum.Transaction{} + } + + traces := make([]ethereum.Transaction, len(b.ValuesTransactions)) + for k, v := range b.ValuesTransactions { + traces[k] = v + } + + return traces +} + +type Transaction struct { + ValueHash *common.Hash `json:"hash"` + ValueFrom *common.Address `json:"from"` + ValueTo *common.Address `json:"to"` + ValueInput hexutil.Bytes `json:"input"` + ValueOutput hexutil.Bytes `json:"output"` + ValueValue *hexutil.Big `json:"value"` + ValueGas *hexutil.Big `json:"gas"` + ValueGasPrice *hexutil.Big `json:"gasPrice"` + ValueBlockNumber string `json:"blockNumber"` +} + +func (t *Transaction) Hash() *common.Hash { + return t.ValueHash +} + +func (t *Transaction) From() *common.Address { + return t.ValueFrom +} + +func (t *Transaction) To() *common.Address { + return t.ValueTo +} + +func (t *Transaction) Input() hexutil.Bytes { + return t.ValueInput +} + +func (t *Transaction) Value() *hexutil.Big { + return t.ValueValue +} + +func (t *Transaction) Gas() *hexutil.Big { + return t.ValueGas +} + +func (t *Transaction) GasPrice() *hexutil.Big { + return t.ValueGasPrice +} + +type Log struct { + ValueAddress string `json:"address"` + ValueBlockHash string `json:"blockHash"` + ValueBlockNumber string `json:"blockNumber"` + ValueData string `json:"data"` + ValueLogIndex string `json:"logIndex"` + ValueRemoved bool `json:"removed"` + ValueTopics []string `json:"topics"` + ValueTransactionHash string `json:"transactionHash"` + ValueTransactionIndex string `json:"transactionIndex"` + ValueTransactionLogIndex string `json:"transactionLogIndex"` + ValueType string `json:"type"` +} + +func (l *Log) Data() string { + return l.ValueData +} + +func (l *Log) Topics() []string { + return l.ValueTopics +} + +type TransactionReceipt struct { + TFrom string `json:"from"` + TTo string `json:"to"` + + TGasUsed *hexutil.Big `json:"gasUsed"` + TCumulativeGasUsed *hexutil.Big `json:"cumulativeGasUsed"` + TContractAddress *common.Address `json:"contractAddress"` + + TStatus *ethereum.Number `json:"status"` // Can be null, if null do a check anyways. 0x0 fail, 0x1 success + TLogs []*Log `json:"logs"` +} + +func (t *TransactionReceipt) From() string { + return t.TFrom +} + +func (t *TransactionReceipt) To() string { + return t.TTo +} + +func (t *TransactionReceipt) GasUsed() *hexutil.Big { + return t.TGasUsed +} + +func (t *TransactionReceipt) CumulativeGasUsed() *hexutil.Big { + return t.TCumulativeGasUsed +} + +func (t *TransactionReceipt) ContractAddress() *common.Address { + return t.TContractAddress +} + +func (t *TransactionReceipt) Status() *ethereum.Number { + return t.TStatus +} + +func (t *TransactionReceipt) Logs() []ethereum.Log { + var logs []ethereum.Log + + for _, log := range t.TLogs { + logs = append(logs, log) + } + + return logs +} + +type Version struct { + Major int `json:"major"` + Minor int `json:"minor"` + Patch int `json:"patch"` +} + +type VersionInfo struct { + Hash string `json:"hash"` + Track string `json:"track"` + Version Version `json:"version"` +} + +// States Types + +type Mem struct { + Data hexutil.Bytes `json:"data"` + Off int64 `json:"off"` +} + +type Ex struct { + Mem Mem `json:"mem"` + Push []string `json:"push"` + Used uint64 `json:"used"` +} + +type VmState struct { + ValuePc uint64 `json:"pc"` + ValueOp string `json:"op"` + ValueEx Ex `json:"ex"` + ValueSub *VmTrace `json:"sub"` + ValueGas uint64 `json:"gas"` + ValueGasCost int64 `json:"cost"` + ValueDepth int `json:"depth"` + ValueError json.RawMessage `json:"error,omitempty"` + ValueStack *[]string `json:"stack,omitempty"` + ValueMemory *[]string `json:"memory,omitempty"` + ValueStorage *map[string]string `json:"storage,omitempty"` + Terminating bool +} + +func (pvs *VmState) Pc() uint64 { + return pvs.ValuePc +} + +func (pvs *VmState) Depth() int { + return pvs.ValueDepth + 1 +} + +func (pvs *VmState) Op() string { + return "Not implemented" +} + +func (pvs *VmState) Stack() []string { + return *pvs.ValueStack +} + +type TraceResult struct { + VmTrace *VmTrace `json:"vmTrace"` + CallTrace []*Trace `json:"trace"` +} + +type VmTrace struct { + Logs []*VmState `json:"ops"` + Code hexutil.Bytes `json:"code"` +} + +func (tr *TraceResult) States() []ethereum.EvmState { + if tr.VmTrace == nil { + return []ethereum.EvmState{} + } + + traces := make([]ethereum.EvmState, len(tr.VmTrace.Logs)) + for k, v := range tr.VmTrace.Logs { + traces[k] = v + } + + return traces +} + +func (tr *TraceResult) Traces() []ethereum.Trace { + if tr.VmTrace == nil { + return []ethereum.Trace{} + } + + traces := make([]ethereum.Trace, len(tr.CallTrace)) + for k, v := range tr.CallTrace { + traces[k] = v + } + + return traces +} + +func (tr *TraceResult) ProcessTrace() { + if tr.VmTrace == nil { + return + } + + tr.VmTrace.Logs = Walk(tr.VmTrace) +} + +func Walk(vmt *VmTrace) []*VmState { + var traces []*VmState + + vmt.Logs[0].ValueOp = ethereum.OpCode(vmt.Code[vmt.Logs[0].ValuePc]).String() + for i := 0; i < len(vmt.Logs); i++ { + if i > 0 { + vmt.Logs[i].ValueStack = vmt.Logs[i-1].ValueStack + + if vmt.Logs[i-1].ValueOp == "CALL" { + vmt.Logs[i].ValueStack = nil + } + } + + if i < len(vmt.Logs)-1 { + opCode := ethereum.OpCode(vmt.Code[vmt.Logs[i+1].ValuePc]) + vmt.Logs[i+1].ValueOp = opCode.String() + + if vmt.Logs[i+1].ValueOp == "EXTCODESIZE" { + vmt.Logs[i].ValueStack = &[]string{} + for j := 0; j < len(vmt.Logs[i].ValueEx.Push); j++ { + vmt.Logs[i].ValueEx.Push[j] = "000000000000000000000000" + vmt.Logs[i].ValueEx.Push[j][2:] + for len(vmt.Logs[i].ValueEx.Push[j]) < 64 { + vmt.Logs[i].ValueEx.Push[j] = "0" + vmt.Logs[i].ValueEx.Push[j] + } + } + + *vmt.Logs[i].ValueStack = append(*vmt.Logs[i].ValueStack, vmt.Logs[i].ValueEx.Push...) + } + } + + traces = append(traces, vmt.Logs[i]) + if vmt.Logs[i].ValueSub != nil { + subTraces := Walk(vmt.Logs[i].ValueSub) + subTraces[len(subTraces)-1].Terminating = true + + traces = append(traces, subTraces...) + } + } + + traces[len(traces)-1].Terminating = true + + return traces +} + +type Action struct { + CallType string `json:"callType"` + Hash *common.Hash `json:"hash"` + ParentHash *common.Hash `json:"hash"` + TransactionHash *common.Hash `json:"hash"` + From common.Address `json:"from"` + To common.Address `json:"to"` + Input hexutil.Bytes `json:"input"` + Gas *hexutil.Uint64 `json:"gas,omitempty"` + Value *hexutil.Big `json:"value,omitempty"` +} + +type Result struct { + GasUsed *hexutil.Uint64 `json:"gasUsed,omitempty"` + Output hexutil.Bytes `json:"output"` +} + +type Trace struct { + ValueAction Action `json:"action"` + ValueResult Result `json:"result"` + ValueLogs []Log `json:"logs"` + ValueSubtraces int `json:"subtraces"` + ValueError string `json:"error"` + ValueTraceAddress []int `json:"traceAddress"` + ValueType string `json:"type"` +} + +func (t *Trace) Hash() *common.Hash { + return t.ValueAction.Hash +} + +func (t *Trace) ParentHash() *common.Hash { + return t.ValueAction.ParentHash +} + +func (t *Trace) TransactionHash() *common.Hash { + return t.ValueAction.TransactionHash +} + +func (t *Trace) Type() string { + return t.ValueType +} + +func (t *Trace) From() common.Address { + return t.ValueAction.From +} + +func (t *Trace) To() common.Address { + return t.ValueAction.To +} + +func (t *Trace) Input() hexutil.Bytes { + return t.ValueAction.Input +} + +func (t *Trace) Output() hexutil.Bytes { + return t.ValueResult.Output +} + +func (t *Trace) Gas() *hexutil.Uint64 { + return t.ValueAction.Gas +} + +func (t *Trace) GasUsed() *hexutil.Uint64 { + return t.ValueResult.GasUsed +} + +func (t *Trace) Value() *hexutil.Big { + return t.ValueAction.Value +} + +func (t *Trace) Error() string { + return t.ValueError +} diff --git a/ethereum/schema.go b/ethereum/schema.go new file mode 100644 index 0000000..110c70f --- /dev/null +++ b/ethereum/schema.go @@ -0,0 +1,56 @@ +package ethereum + +import ( + "github.com/tenderly/tenderly-cli/jsonrpc2" +) + +type Schema interface { + Eth() EthSchema + Net() NetSchema + Trace() TraceSchema + PubSub() PubSubSchema +} + +// Eth + +type EthSchema interface { + BlockNumber() (*jsonrpc2.Request, *Number) + GetBlockByNumber(num Number) (*jsonrpc2.Request, Block) + GetTransaction(hash string) (*jsonrpc2.Request, Transaction) + GetTransactionReceipt(hash string) (*jsonrpc2.Request, TransactionReceipt) +} + +// Net + +type NetSchema interface { + Version() (*jsonrpc2.Request, *string) +} + +// States + +type TraceSchema interface { + VMTrace(hash string) (*jsonrpc2.Request, TransactionStates) + CallTrace(hash string) (*jsonrpc2.Request, CallTraces) +} + +// PubSub + +type PubSubSchema interface { + Subscribe() (*jsonrpc2.Request, *SubscriptionID) + Unsubscribe(id SubscriptionID) (*jsonrpc2.Request, *UnsubscribeSuccess) +} + +type pubSubSchema struct { +} + +func (pubSubSchema) Subscribe() (*jsonrpc2.Request, *SubscriptionID) { + id := NewNilSubscriptionID() + + return jsonrpc2.NewRequest("eth_subscribe", "newHeads"), &id +} + +func (pubSubSchema) Unsubscribe(id SubscriptionID) (*jsonrpc2.Request, *UnsubscribeSuccess) { + var success UnsubscribeSuccess + + return jsonrpc2.NewRequest("eth_unsubscribe", id.String()), &success +} diff --git a/ethereum/types.go b/ethereum/types.go new file mode 100644 index 0000000..7bc53af --- /dev/null +++ b/ethereum/types.go @@ -0,0 +1,127 @@ +package ethereum + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// Core Types + +type Number int64 + +func (n *Number) Value() int64 { + return int64(*n) +} + +func (n *Number) Hex() string { + return fmt.Sprintf("%#x", int64(*n)) +} + +func (n *Number) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + + num, err := strconv.ParseInt(s, 0, 64) + if err != nil { + return err + } + + *n = Number(num) + + return nil +} + +type Header interface { + Number() *Number +} + +type Block interface { + Transactions() []Transaction +} + +type Transaction interface { + Hash() *common.Hash + + From() *common.Address + To() *common.Address + + Input() hexutil.Bytes + Value() *hexutil.Big + Gas() *hexutil.Big + GasPrice() *hexutil.Big +} + +type Log interface { + Topics() []string + Data() string +} + +type TransactionReceipt interface { + From() string + To() string + + GasUsed() *hexutil.Big + CumulativeGasUsed() *hexutil.Big + ContractAddress() *common.Address + + Status() *Number + Logs() []Log +} + +// States Types + +type TransactionStates interface { + States() []EvmState + ProcessTrace() +} + +type EvmState interface { + Pc() uint64 + Depth() int + Op() string + Stack() []string +} + +type CallTraces interface { + Traces() []Trace +} + +type Trace interface { + Hash() *common.Hash + ParentHash() *common.Hash + TransactionHash() *common.Hash + Type() string + From() common.Address + To() common.Address + Input() hexutil.Bytes + Output() hexutil.Bytes + Gas() *hexutil.Uint64 + GasUsed() *hexutil.Uint64 + Value() *hexutil.Big + Error() string +} + +// Subscription Types + +type SubscriptionID string + +func NewNilSubscriptionID() SubscriptionID { + return "" +} + +func (id SubscriptionID) String() string { + return string(id) +} + +type SubscriptionResult struct { + Subscription SubscriptionID `json:"subscription"` + Result Header `json:"result"` +} + +type UnsubscribeSuccess bool diff --git a/jsonrpc2/client.go b/jsonrpc2/client.go new file mode 100644 index 0000000..1ba509a --- /dev/null +++ b/jsonrpc2/client.go @@ -0,0 +1,269 @@ +package jsonrpc2 + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "strings" + "sync" + "sync/atomic" + "time" +) + +var id int64 + +func nextID() int64 { + atomic.AddInt64(&id, 1) + + return id +} + +type Request struct { + ID int64 `json:"id,omitempty"` + Version string `json:"jsonrpc"` + + Method string `json:"method"` + Params []interface{} `json:"params,omitempty"` +} + +func NewRequest(method string, params ...interface{}) *Request { + return &Request{ + ID: nextID(), + Version: "2.0", + + Method: method, + Params: params, + } +} + +type Message struct { + ID int64 `json:"id,omitempty"` + Version string `json:"jsonrpc"` + + Method string `json:"method,omitempty"` + Params json.RawMessage `json:"params,omitempty"` + + Result json.RawMessage `json:"result,omitempty"` + Error *Error `json:"error,omitempty"` +} + +type Error struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +func (msg *Message) Reset() { + msg.ID = 0 + msg.Version = "2.0" + msg.Method = "" + msg.Params = nil + msg.Result = nil + msg.Error = nil +} + +type Connection interface { + Write(msg *Request) error + Read() (*Message, error) + Close() error +} + +type Client struct { + conn Connection + + flying sync.Map + subscribers sync.Map +} + +func DiscoverAndDial(host string) (client *Client, err error) { + protocols := []string{"ws", "wss", "http", "https"} + + for _, protocol := range protocols { + addr := fmt.Sprintf("%s://%s", protocol, host) + + client, err = Dial(addr) + if err != nil { + continue + } + + return client, nil + } + + return nil, fmt.Errorf("could not determine protocol") +} + +func Dial(addr string) (*Client, error) { + conn, err := dialConn(addr) + if err != nil { + return nil, fmt.Errorf("dial connection: %s", err) + } + + client := &Client{ + conn: conn, + } + + go client.listen() + + return client, nil +} + +func dialConn(addr string) (Connection, error) { + if strings.HasPrefix(addr, "ws") { + return DialWebsocketConnection(addr) + } + + if strings.HasPrefix(addr, "http") { + return DialHttpConnection(addr) + } + + return nil, fmt.Errorf("unrecognized protocol") +} + +func (c *Client) Call(res interface{}, method string, params ...interface{}) error { + req := NewRequest(method, params...) + + return c.CallRequest(res, req) +} + +func (c *Client) CallRequest(res interface{}, req *Request) error { + resCh := make(chan *Message, 1) + c.setFlying(req.ID, resCh) + defer func() { + c.deleteFlying(req.ID) + }() + + err := c.conn.Write(req) + if err != nil { + return fmt.Errorf("write message to socket: %s", err) + } + + ctx, _ := context.WithTimeout(context.TODO(), 5*time.Second) + select { + case <-ctx.Done(): + return fmt.Errorf("request timed out") + case r := <-resCh: + if r.Error != nil { + return fmt.Errorf("request failed: [ %d ] %s", r.Error.Code, r.Error.Message) + } + + err = json.Unmarshal(r.Result, res) + if err != nil { + return fmt.Errorf("read result: %s", err) + } + } + + return nil +} + +func (c *Client) Subscribe(id string) (chan *Message, error) { + subCh := make(chan *Message, 256) + + if c.hasSubscription(id) { + return nil, fmt.Errorf("subscription %s already exists", id) + } + + c.setSubscription(id, subCh) + + return subCh, nil +} + +func (c *Client) Unsubscribe(id string) error { + subCh, ok := c.getSubscription(id) + if !ok { + return fmt.Errorf("subscription %s does not exist", id) + } + + c.deleteSubscription(id) + close(subCh) + + return nil +} + +func (c *Client) listen() { + for { + msg, err := c.conn.Read() + if err == io.EOF { + break + } + if err != nil { + log.Fatalf("failed reading message from connection: %s", err) + } + + c.processMsg(msg) + } +} + +func (c *Client) processMsg(msg *Message) error { + if msg.ID == 0 { + // notification + c.subscribers.Range(func(_, val interface{}) bool { + subCh := val.(chan *Message) + + subCh <- msg + + return true + }) + + return nil + } + + // response + //@TODO: We don't have to send ID and JSONRPC version back to caller. + + resCh, ok := c.getFlying(msg.ID) + + if !ok { + log.Printf("dropped message: %d", msg.ID) + return nil + } + + resCh <- msg + + return nil +} + +func (c *Client) setFlying(id int64, msg chan *Message) { + c.flying.Store(id, msg) +} + +func (c *Client) getFlying(id int64) (chan *Message, bool) { + msg, ok := c.flying.Load(id) + if !ok { + return nil, ok + } + + return msg.(chan *Message), ok +} + +func (c *Client) deleteFlying(id int64) { + c.flying.Delete(id) +} + +func (c *Client) hasSubscription(id string) bool { + _, ok := c.subscribers.Load(id) + + return ok +} + +func (c *Client) setSubscription(id string, msg chan *Message) { + c.subscribers.Store(id, msg) +} + +func (c *Client) getSubscription(id string) (chan *Message, bool) { + msg, ok := c.subscribers.Load(id) + if !ok { + return nil, ok + } + + return msg.(chan *Message), ok +} + +func (c *Client) deleteSubscription(id string) { + c.subscribers.Delete(id) +} + +func (c *Client) Close() error { + return c.conn.Close() +} diff --git a/jsonrpc2/http.go b/jsonrpc2/http.go new file mode 100644 index 0000000..dcbeb0c --- /dev/null +++ b/jsonrpc2/http.go @@ -0,0 +1,109 @@ +package jsonrpc2 + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "sync" +) + +type httpConnection struct { + addr string + + respCh chan *Message + + once sync.Once + ctx context.Context + cancel context.CancelFunc +} + +func DialHttpConnection(addr string) (Connection, error) { + ctx, cancel := context.WithCancel(context.Background()) + + conn := &httpConnection{ + addr: addr, + + respCh: make(chan *Message, 256), + + ctx: ctx, + cancel: cancel, + } + + msg := NewRequest("ping") + + err := conn.Write(msg) + if err != nil { + return nil, fmt.Errorf("connect via http: %s", err) + } + + _, err = conn.Read() + if err != nil { + return nil, fmt.Errorf("connect via http: %s", err) + } + + return conn, nil +} + +func (conn *httpConnection) Write(msg *Request) error { + // Setup request, possibly wasteful. + req, err := http.NewRequest(http.MethodPost, conn.addr, nil) + if err != nil { + return fmt.Errorf("write request setup: %s", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + data, err := json.Marshal(msg) + if err != nil { + return fmt.Errorf("write request body: %s", err) + } + + req.Body = ioutil.NopCloser(bytes.NewReader(data)) + req.ContentLength = int64(len(data)) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("write request send: %s", err) + } + if resp.StatusCode < 200 && resp.StatusCode >= 300 { + return fmt.Errorf("write request unsuccessful: %s", err) + } + + var respMsg Message + + err = json.NewDecoder(resp.Body).Decode(&respMsg) + if err != nil { + return fmt.Errorf("write request read: %s", err) + } + + conn.respCh <- &respMsg + + err = resp.Body.Close() + if err != nil { + return fmt.Errorf("write request cleanup: %s", err) + } + + return nil +} + +func (conn *httpConnection) Read() (*Message, error) { + select { + case resp := <-conn.respCh: + return resp, nil + case <-conn.ctx.Done(): + return nil, io.EOF + } +} + +func (conn *httpConnection) Close() error { + conn.once.Do(func() { + conn.cancel() + }) + + return nil +} diff --git a/jsonrpc2/websocket.go b/jsonrpc2/websocket.go new file mode 100644 index 0000000..b5c9efd --- /dev/null +++ b/jsonrpc2/websocket.go @@ -0,0 +1,50 @@ +package jsonrpc2 + +import ( + "fmt" + "io" + + "github.com/gorilla/websocket" +) + +type websocketConnection struct { + ws *websocket.Conn +} + +func DialWebsocketConnection(host string) (Connection, error) { + ws, _, err := websocket.DefaultDialer.Dial(host, nil) + if err != nil { + return nil, fmt.Errorf("open websocket connection: %s", err) + } + + return &websocketConnection{ + ws: ws, + }, nil +} + +func (conn *websocketConnection) Write(r *Request) error { + err := conn.ws.WriteJSON(r) + if err != nil { + return fmt.Errorf("write websocket: %s", err) + } + + return nil +} + +func (conn *websocketConnection) Read() (*Message, error) { + var msg Message + + err := conn.ws.ReadJSON(&msg) + if err == io.ErrUnexpectedEOF { + return nil, io.EOF + } + if err != nil { + return nil, fmt.Errorf("read websocket: %s", err) + } + + return &msg, nil +} + +func (conn *websocketConnection) Close() error { + return conn.ws.Close() +}