├── .github └── workflows │ └── test.yml ├── LICENSE ├── README.md ├── abi ├── abi.go ├── abi_test.go ├── constructor.go ├── constructor_test.go ├── contract.go ├── contract_test.go ├── decode.go ├── encode.go ├── error.go ├── error_test.go ├── event.go ├── event_test.go ├── fourbytes.go ├── fourbytes_test.go ├── method.go ├── method_test.go ├── num.go ├── num_test.go ├── panic.go ├── panic_test.go ├── revert.go ├── revert_test.go ├── sigparser.go ├── sigparser_test.go ├── testdata │ ├── abi.json │ └── abi.sol ├── type.go ├── type_test.go ├── value.go ├── value_test.go ├── word.go └── word_test.go ├── crypto ├── crypto.go ├── ecdsa.go ├── ecdsa_test.go ├── keccak.go ├── keccak_test.go ├── transaction.go └── transaction_test.go ├── examples ├── abi-enc-dec-prog │ └── main.go ├── abi-enc-dec-struct │ └── main.go ├── abi-enc-dec-vars │ └── main.go ├── call-abi │ └── main.go ├── call │ └── main.go ├── connect │ └── main.go ├── contract-hra-abi │ └── main.go ├── contract-json-abi │ ├── erc20.json │ └── main.go ├── custom-type-advenced │ └── main.go ├── custom-type-simple │ └── main.go ├── events │ └── main.go ├── key-mnemonic │ └── key-mnemonic.go ├── send-tx │ ├── key.json │ └── main.go └── subscription │ └── main.go ├── go.mod ├── go.sum ├── hexutil ├── hexutil.go └── hexutil_test.go ├── rpc ├── base.go ├── base_test.go ├── client.go ├── client_test.go ├── mocks_test.go ├── rpc.go ├── transport │ ├── combined.go │ ├── error.go │ ├── error_test.go │ ├── http.go │ ├── http_test.go │ ├── ipc.go │ ├── retry.go │ ├── retry_test.go │ ├── rpc.go │ ├── stream.go │ ├── transport.go │ ├── websocket.go │ └── websocket_test.go └── util.go ├── txmodifier ├── chainid.go ├── chainid_test.go ├── gasfee.go ├── gasfee_test.go ├── gaslimit.go ├── gaslimit_test.go ├── nonce.go ├── nonce_test.go └── txmodifier_test.go ├── types ├── structs.go ├── structs_test.go ├── types.go ├── types_test.go └── util.go └── wallet ├── key.go ├── key_hd.go ├── key_hd_test.go ├── key_json.go ├── key_json_test.go ├── key_json_v3.go ├── key_priv.go ├── key_rpc.go └── testdata ├── pbkdf2.json └── scrypt.json /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | pull_request: 9 | branches: 10 | - master 11 | - main 12 | 13 | jobs: 14 | test: 15 | name: Code Linting & Unit Tests 16 | strategy: 17 | matrix: 18 | go-version: [ 1.21.x ] 19 | os: [ ubuntu-latest ] 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | with: 25 | fetch-depth: '0' 26 | - name: Setup Go 27 | uses: actions/setup-go@v4 28 | with: 29 | go-version: ${{ matrix.go-version }} 30 | - name: Lint 31 | uses: golangci/golangci-lint-action@v3 32 | with: 33 | version: v1.55 34 | - name: Build 35 | run: go build -v ./... 36 | - name: Test 37 | run: go test -v ./... 38 | 39 | analyze: 40 | needs: test 41 | name: Analyze with CodeQL 42 | runs-on: ubuntu-latest 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | language: [ 'go' ] 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@v2 50 | with: 51 | fetch-depth: 0 52 | - name: Init 53 | uses: github/codeql-action/init@v2 54 | with: 55 | languages: ${{ matrix.language }} 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v2 58 | - name: Analyze 59 | uses: github/codeql-action/analyze@v2 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /abi/constructor.go: -------------------------------------------------------------------------------- 1 | package abi 2 | 3 | // Constructor represents a constructor in an Contract. The constructor can be used to 4 | // encode arguments for a constructor call. 5 | type Constructor struct { 6 | inputs *TupleType 7 | abi *ABI 8 | } 9 | 10 | // NewConstructor creates a new Constructor instance. 11 | // 12 | // This method is rarely used, see ParseConstructor for a more convenient way 13 | // to create a new Constructor. 14 | func NewConstructor(inputs *TupleType) *Constructor { 15 | return Default.NewConstructor(inputs) 16 | } 17 | 18 | // ParseConstructor parses a constructor signature and returns a new Constructor. 19 | // 20 | // A constructor signature is similar to a method signature, but it does not 21 | // have a name and returns no values. It can be optionally prefixed with the 22 | // "constructor" keyword. 23 | // 24 | // The following examples are valid signatures: 25 | // 26 | // ((uint256,bytes32)[]) 27 | // ((uint256 a, bytes32 b)[] c) 28 | // constructor(tuple(uint256 a, bytes32 b)[] memory c) 29 | // 30 | // This function is equivalent to calling Parser.ParseConstructor with the 31 | // default configuration. 32 | func ParseConstructor(signature string) (*Constructor, error) { 33 | return Default.ParseConstructor(signature) 34 | } 35 | 36 | // MustParseConstructor is like ParseConstructor but panics on error. 37 | func MustParseConstructor(signature string) *Constructor { 38 | return Default.MustParseConstructor(signature) 39 | } 40 | 41 | // NewConstructor creates a new Constructor instance. 42 | func (a *ABI) NewConstructor(inputs *TupleType) *Constructor { 43 | if inputs == nil { 44 | inputs = NewTupleType() 45 | } 46 | return &Constructor{ 47 | inputs: inputs, 48 | abi: a, 49 | } 50 | } 51 | 52 | // ParseConstructor parses a constructor signature and returns a new Constructor. 53 | // 54 | // See ParseConstructor for more information. 55 | func (a *ABI) ParseConstructor(signature string) (*Constructor, error) { 56 | return parseConstructor(a, nil, signature) 57 | } 58 | 59 | // MustParseConstructor is like ParseConstructor but panics on error. 60 | func (a *ABI) MustParseConstructor(signature string) *Constructor { 61 | c, err := a.ParseConstructor(signature) 62 | if err != nil { 63 | panic(err) 64 | } 65 | return c 66 | } 67 | 68 | // Inputs returns the input arguments of the constructor as a tuple type. 69 | func (m *Constructor) Inputs() *TupleType { 70 | return m.inputs 71 | } 72 | 73 | // EncodeArg encodes an argument for a contract deployment. 74 | // The map or structure must have fields with the same names as the 75 | // constructor arguments. 76 | func (m *Constructor) EncodeArg(code []byte, arg any) ([]byte, error) { 77 | encoded, err := m.abi.EncodeValue(m.inputs, arg) 78 | if err != nil { 79 | return nil, err 80 | } 81 | input := make([]byte, len(code)+len(encoded)) 82 | copy(input, code) 83 | copy(input[len(code):], encoded) 84 | return input, nil 85 | } 86 | 87 | // MustEncodeArg is like EncodeArg but panics on error. 88 | func (m *Constructor) MustEncodeArg(code []byte, arg any) []byte { 89 | encoded, err := m.EncodeArg(code, arg) 90 | if err != nil { 91 | panic(err) 92 | } 93 | return encoded 94 | } 95 | 96 | // EncodeArgs encodes arguments for a contract deployment. 97 | // The map or structure must have fields with the same names as the 98 | // constructor arguments. 99 | func (m *Constructor) EncodeArgs(code []byte, args ...any) ([]byte, error) { 100 | encoded, err := m.abi.EncodeValues(m.inputs, args...) 101 | if err != nil { 102 | return nil, err 103 | } 104 | input := make([]byte, len(code)+len(encoded)) 105 | copy(input, code) 106 | copy(input[len(code):], encoded) 107 | return input, nil 108 | } 109 | 110 | // MustEncodeArgs is like EncodeArgs but panics on error. 111 | func (m *Constructor) MustEncodeArgs(code []byte, args ...any) []byte { 112 | encoded, err := m.EncodeArgs(code, args...) 113 | if err != nil { 114 | panic(err) 115 | } 116 | return encoded 117 | } 118 | 119 | // String returns the human-readable signature of the constructor. 120 | func (m *Constructor) String() string { 121 | return "constructor" + m.inputs.String() 122 | } 123 | -------------------------------------------------------------------------------- /abi/constructor_test.go: -------------------------------------------------------------------------------- 1 | package abi 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestParseConstructor(t *testing.T) { 13 | tests := []struct { 14 | signature string 15 | expected string 16 | wantErr bool 17 | }{ 18 | {signature: "constructor()", expected: "constructor()"}, 19 | {signature: "constructor(uint256)", expected: "constructor(uint256)"}, 20 | {signature: "((uint256, bytes32)[])", expected: "constructor((uint256, bytes32)[])"}, 21 | {signature: "((uint256 a,bytes32 b)[] a)", expected: "constructor((uint256 a, bytes32 b)[] a)"}, 22 | {signature: "constructor(tuple(uint256 a, bytes32 b)[] memory c)", expected: "constructor((uint256 a, bytes32 b)[] c)"}, 23 | {signature: "foo(uint256)(uint256)", wantErr: true}, 24 | {signature: "event foo(uint256)", wantErr: true}, 25 | {signature: "error foo(uint256)", wantErr: true}, 26 | {signature: "function foo(uint256)", wantErr: true}, 27 | } 28 | for n, tt := range tests { 29 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 30 | c, err := ParseConstructor(tt.signature) 31 | if tt.wantErr { 32 | require.Error(t, err) 33 | } else { 34 | require.NoError(t, err) 35 | assert.Equal(t, tt.expected, c.String()) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestConstructor_EncodeArgs(t *testing.T) { 42 | tests := []struct { 43 | signature string 44 | arg []any 45 | expected string 46 | }{ 47 | {signature: "constructor()", arg: nil, expected: "aabb"}, 48 | {signature: "constructor(uint256)", arg: []any{1}, expected: "aabb0000000000000000000000000000000000000000000000000000000000000001"}, 49 | } 50 | for n, tt := range tests { 51 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 52 | c, err := ParseConstructor(tt.signature) 53 | require.NoError(t, err) 54 | enc, err := c.EncodeArgs([]byte{0xAA, 0xBB}, tt.arg...) 55 | require.NoError(t, err) 56 | assert.Equal(t, tt.expected, hex.EncodeToString(enc)) 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /abi/error.go: -------------------------------------------------------------------------------- 1 | package abi 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/defiweb/go-eth/crypto" 8 | ) 9 | 10 | // CustomError represents a custom error returned by a contract call. 11 | type CustomError struct { 12 | Type *Error // The error type. 13 | Data []byte // The error data returned by the contract call. 14 | } 15 | 16 | // Error implements the error interface. 17 | func (e CustomError) Error() string { 18 | return fmt.Sprintf("error: %s", e.Type.Name()) 19 | } 20 | 21 | // Error represents an error in an ABI. The error can be used to decode errors 22 | // returned by a contract call. 23 | type Error struct { 24 | name string 25 | inputs *TupleType 26 | abi *ABI 27 | 28 | fourBytes FourBytes 29 | signature string 30 | } 31 | 32 | // NewError creates a new Error instance. 33 | func NewError(name string, inputs *TupleType) *Error { 34 | if inputs == nil { 35 | inputs = NewTupleType() 36 | } 37 | return Default.NewError(name, inputs) 38 | } 39 | 40 | // ParseError parses an error signature and returns a new Error. 41 | // 42 | // An error signature is similar to a method signature, but returns no values. 43 | // It can be optionally prefixed with the "error" keyword. 44 | // 45 | // The following examples are valid signatures: 46 | // 47 | // foo((uint256,bytes32)[]) 48 | // foo((uint256 a, bytes32 b)[] c) 49 | // error foo(tuple(uint256 a, bytes32 b)[] c) 50 | // 51 | // This function is equivalent to calling Parser.ParseError with the default 52 | // configuration. 53 | func ParseError(signature string) (*Error, error) { 54 | return Default.ParseError(signature) 55 | } 56 | 57 | // MustParseError is like ParseError but panics on error. 58 | func MustParseError(signature string) *Error { 59 | return Default.MustParseError(signature) 60 | } 61 | 62 | // NewError creates a new Error instance. 63 | // 64 | // This method is rarely used, see ParseError for a more convenient way to 65 | // create a new Error. 66 | func (a *ABI) NewError(name string, inputs *TupleType) *Error { 67 | e := &Error{ 68 | name: name, 69 | inputs: inputs, 70 | abi: a, 71 | } 72 | e.generateSignature() 73 | e.calculateFourBytes() 74 | return e 75 | } 76 | 77 | // ParseError parses an error signature and returns a new Error. 78 | // 79 | // See ParseError for more information. 80 | func (a *ABI) ParseError(signature string) (*Error, error) { 81 | return parseError(a, nil, signature) 82 | } 83 | 84 | // MustParseError is like ParseError but panics on error. 85 | func (a *ABI) MustParseError(signature string) *Error { 86 | m, err := a.ParseError(signature) 87 | if err != nil { 88 | panic(err) 89 | } 90 | return m 91 | } 92 | 93 | // Name returns the name of the error. 94 | func (e *Error) Name() string { 95 | return e.name 96 | } 97 | 98 | // Inputs returns the input arguments of the error as a tuple type. 99 | func (e *Error) Inputs() *TupleType { 100 | return e.inputs 101 | } 102 | 103 | // FourBytes is the first four bytes of the Keccak256 hash of the error 104 | // signature. 105 | func (e *Error) FourBytes() FourBytes { 106 | return e.fourBytes 107 | } 108 | 109 | // Signature returns the error signature, that is, the error name and the 110 | // canonical type of error arguments. 111 | func (e *Error) Signature() string { 112 | return e.signature 113 | } 114 | 115 | // Is returns true if the ABI encoded data is an error of this type. 116 | func (e *Error) Is(data []byte) bool { 117 | return e.fourBytes.Match(data) && (len(data)-4)%WordLength == 0 118 | } 119 | 120 | // DecodeValue decodes the error into a map or structure. If a structure is 121 | // given, it must have fields with the same names as error arguments. 122 | func (e *Error) DecodeValue(data []byte, val any) error { 123 | if e.fourBytes.Match(data) { 124 | return fmt.Errorf("abi: selector mismatch for error %s", e.name) 125 | } 126 | return e.abi.DecodeValue(e.inputs, data[4:], val) 127 | } 128 | 129 | // MustDecodeValue is like DecodeValue but panics on error. 130 | func (e *Error) MustDecodeValue(data []byte, val any) { 131 | err := e.DecodeValue(data, val) 132 | if err != nil { 133 | panic(err) 134 | } 135 | } 136 | 137 | // DecodeValues decodes the error into a map or structure. If a structure is 138 | // given, it must have fields with the same names as error arguments. 139 | func (e *Error) DecodeValues(data []byte, vals ...any) error { 140 | if e.fourBytes.Match(data) { 141 | return fmt.Errorf("abi: selector mismatch for error %s", e.name) 142 | } 143 | return e.abi.DecodeValues(e.inputs, data[4:], vals...) 144 | } 145 | 146 | // MustDecodeValues is like DecodeValues but panics on error. 147 | func (e *Error) MustDecodeValues(data []byte, vals ...any) { 148 | err := e.DecodeValues(data, vals...) 149 | if err != nil { 150 | panic(err) 151 | } 152 | } 153 | 154 | // ToError converts the error data returned by contract calls into a CustomError. 155 | // If the data does not contain a valid error message, it returns nil. 156 | func (e *Error) ToError(data []byte) error { 157 | if !e.fourBytes.Match(data) { 158 | return nil 159 | } 160 | return CustomError{ 161 | Type: e, 162 | Data: data[4:], 163 | } 164 | } 165 | 166 | // HandleError converts an error returned by a contract call to a custom error 167 | // if possible. If provider error is nil, it returns nil. 168 | func (e *Error) HandleError(err error) error { 169 | if err == nil { 170 | return nil 171 | } 172 | var dataErr interface{ RPCErrorData() any } 173 | if !errors.As(err, &dataErr) { 174 | return err 175 | } 176 | data, ok := dataErr.RPCErrorData().([]byte) 177 | if !ok { 178 | return err 179 | } 180 | if err := e.ToError(data); err != nil { 181 | return err 182 | } 183 | return err 184 | } 185 | 186 | // String returns the human-readable signature of the error. 187 | func (e *Error) String() string { 188 | return "error " + e.name + e.inputs.String() 189 | } 190 | 191 | func (e *Error) generateSignature() { 192 | e.signature = e.name + e.inputs.CanonicalType() 193 | } 194 | 195 | func (e *Error) calculateFourBytes() { 196 | id := crypto.Keccak256([]byte(e.Signature())) 197 | copy(e.fourBytes[:], id[:4]) 198 | } 199 | -------------------------------------------------------------------------------- /abi/error_test.go: -------------------------------------------------------------------------------- 1 | package abi 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/defiweb/go-eth/hexutil" 12 | ) 13 | 14 | type mockError struct { 15 | data any 16 | } 17 | 18 | func (m *mockError) Error() string { 19 | return "mock error" 20 | } 21 | 22 | func (m *mockError) RPCErrorData() any { 23 | return m.data 24 | } 25 | 26 | func TestParseError(t *testing.T) { 27 | tests := []struct { 28 | signature string 29 | expected string 30 | wantErr bool 31 | }{ 32 | {signature: "foo((uint256,bytes32)[])", expected: "error foo((uint256, bytes32)[])"}, 33 | {signature: "foo((uint256 a, bytes32 b)[] c)", expected: "error foo((uint256 a, bytes32 b)[] c)"}, 34 | {signature: "error foo(tuple(uint256 a, bytes32 b)[] c)", expected: "error foo((uint256 a, bytes32 b)[] c)"}, 35 | {signature: "foo(uint256)(uint256)", wantErr: true}, 36 | {signature: "event foo(uint256)", wantErr: true}, 37 | {signature: "function foo(uint256)", wantErr: true}, 38 | {signature: "constructor(uint256)", wantErr: true}, 39 | } 40 | for n, tt := range tests { 41 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 42 | e, err := ParseError(tt.signature) 43 | if tt.wantErr { 44 | require.Error(t, err) 45 | } else { 46 | require.NoError(t, err) 47 | assert.Equal(t, tt.expected, e.String()) 48 | } 49 | }) 50 | } 51 | } 52 | 53 | func TestError_Is(t *testing.T) { 54 | e, err := ParseError("error foo(uint256)") 55 | require.NoError(t, err) 56 | 57 | assert.True(t, e.Is(hexutil.MustHexToBytes("0x2fbebd38000000000000000000000000000000000000000000000000000000000000012c"))) 58 | assert.False(t, e.Is(hexutil.MustHexToBytes("0xaabbccdd000000000000000000000000000000000000000000000000000000000000012c"))) 59 | } 60 | 61 | func TestError_ToError(t *testing.T) { 62 | e, err := ParseError("error foo(uint256)") 63 | require.NoError(t, err) 64 | 65 | // Custom error 66 | t.Run("custom error", func(t *testing.T) { 67 | customErr := e.ToError(hexutil.MustHexToBytes("0x2fbebd38000000000000000000000000000000000000000000000000000000000000012c")) 68 | require.NotNil(t, customErr) 69 | assert.Equal(t, "error: foo", customErr.Error()) 70 | }) 71 | 72 | // Unknown error 73 | t.Run("unknown error", func(t *testing.T) { 74 | unkErr := e.ToError(hexutil.MustHexToBytes("0x112233440000000000000000000000000000000000000000000000000000000000000000")) 75 | require.Nil(t, unkErr) 76 | }) 77 | } 78 | 79 | func TestError_HandleError(t *testing.T) { 80 | e, err := ParseError("error foo(uint256)") 81 | require.NoError(t, err) 82 | 83 | // Custom error 84 | t.Run("custom error", func(t *testing.T) { 85 | callErr := &mockError{data: hexutil.MustHexToBytes("0x2fbebd38000000000000000000000000000000000000000000000000000000000000012c")} 86 | customErr := e.HandleError(callErr) 87 | require.NotNil(t, customErr) 88 | assert.Equal(t, "error: foo", customErr.Error()) 89 | }) 90 | 91 | // Unknown error 92 | t.Run("unknown error", func(t *testing.T) { 93 | callErr := &mockError{data: hexutil.MustHexToBytes("0x112233440000000000000000000000000000000000000000000000000000000000000000")} 94 | unkErr := e.HandleError(callErr) 95 | require.NotNil(t, unkErr) 96 | assert.Equal(t, callErr, unkErr) 97 | }) 98 | 99 | // Nil 100 | t.Run("nil", func(t *testing.T) { 101 | require.Nil(t, e.HandleError(nil)) 102 | }) 103 | 104 | // Not a byte slice 105 | t.Run("not a byte slice", func(t *testing.T) { 106 | callErr := &mockError{data: "not a byte slice"} 107 | require.Equal(t, callErr, e.HandleError(callErr)) 108 | }) 109 | 110 | // Not a RPC call error 111 | t.Run("not a RPC call error", func(t *testing.T) { 112 | require.Equal(t, errors.New("not a RPC call error"), e.HandleError(errors.New("not a RPC call error"))) 113 | }) 114 | } 115 | -------------------------------------------------------------------------------- /abi/event.go: -------------------------------------------------------------------------------- 1 | package abi 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/defiweb/go-eth/crypto" 8 | "github.com/defiweb/go-eth/types" 9 | ) 10 | 11 | // Event represents an event in an ABI. The event can be used to decode events 12 | // emitted by a contract. 13 | type Event struct { 14 | name string 15 | inputs *EventTupleType 16 | anonymous bool 17 | abi *ABI 18 | 19 | topic0 types.Hash 20 | signature string 21 | } 22 | 23 | // NewEvent creates a new Event instance. 24 | // 25 | // This method is rarely used, see ParseEvent for a more convenient way to 26 | // create a new Event. 27 | func NewEvent(name string, inputs *EventTupleType, anonymous bool) *Event { 28 | return Default.NewEvent(name, inputs, anonymous) 29 | } 30 | 31 | // ParseEvent parses an event signature and returns a new Event. 32 | // 33 | // An event signature is similar to a method signature, but returns no values. 34 | // It can be optionally prefixed with the "event" keyword. 35 | // 36 | // The following examples are valid signatures: 37 | // 38 | // foo(int indexed,(uint256,bytes32)[]) 39 | // foo(int indexed a, (uint256 b, bytes32 c)[] d) 40 | // event foo(int indexed a tuple(uint256 b, bytes32 c)[] d) 41 | // 42 | // This function is equivalent to calling Parser.ParseEvent with the default 43 | // configuration. 44 | func ParseEvent(signature string) (*Event, error) { 45 | return Default.ParseEvent(signature) 46 | } 47 | 48 | // MustParseEvent is like ParseEvent but panics on error. 49 | func MustParseEvent(signature string) *Event { 50 | return Default.MustParseEvent(signature) 51 | } 52 | 53 | // NewEvent creates a new Event instance. 54 | func (a *ABI) NewEvent(name string, inputs *EventTupleType, anonymous bool) *Event { 55 | if inputs == nil { 56 | inputs = NewEventTupleType() 57 | } 58 | e := &Event{ 59 | name: name, 60 | inputs: inputs, 61 | anonymous: anonymous, 62 | abi: a, 63 | } 64 | e.generateSignature() 65 | e.calculateTopic0() 66 | return e 67 | } 68 | 69 | // ParseEvent parses an event signature and returns a new Event. 70 | // 71 | // See ParseEvent for more information. 72 | func (a *ABI) ParseEvent(signature string) (*Event, error) { 73 | return parseEvent(a, nil, signature) 74 | } 75 | 76 | // MustParseEvent is like ParseEvent but panics on error. 77 | func (a *ABI) MustParseEvent(signature string) *Event { 78 | e, err := a.ParseEvent(signature) 79 | if err != nil { 80 | panic(err) 81 | } 82 | return e 83 | } 84 | 85 | // Name returns the name of the event. 86 | func (e *Event) Name() string { 87 | return e.name 88 | } 89 | 90 | // Inputs returns the input arguments of the event as a tuple type. 91 | func (e *Event) Inputs() *EventTupleType { 92 | return e.inputs 93 | } 94 | 95 | // Topic0 returns the first topic of the event, that is, the Keccak256 hash of 96 | // the event signature. 97 | func (e *Event) Topic0() types.Hash { 98 | return e.topic0 99 | } 100 | 101 | // Signature returns the event signature, that is, the event name and the 102 | // canonical type of the input arguments. 103 | func (e *Event) Signature() string { 104 | return e.signature 105 | } 106 | 107 | // DecodeValue decodes the event into a map or structure. If a structure is 108 | // given, it must have fields with the same names as the event arguments. 109 | func (e *Event) DecodeValue(topics []types.Hash, data []byte, val any) error { 110 | if e.anonymous { 111 | return e.abi.DecodeValue(e.inputs, data, val) 112 | } 113 | if len(topics) != e.inputs.IndexedSize()+1 { 114 | return fmt.Errorf("abi: wrong number of topics for event %s", e.name) 115 | } 116 | if topics[0] != e.topic0 { 117 | return fmt.Errorf("abi: topic0 mismatch for event %s", e.name) 118 | } 119 | // The anymapper package does not zero out values before decoding into 120 | // it, therefore we can decode topics and data into the same value. 121 | if len(topics) > 1 { 122 | if err := e.abi.DecodeValue(e.inputs.TopicsTuple(), hashSliceToBytes(topics[1:]), val); err != nil { 123 | return err 124 | } 125 | } 126 | if len(data) > 0 { 127 | if err := e.abi.DecodeValue(e.inputs.DataTuple(), data, val); err != nil { 128 | return err 129 | } 130 | } 131 | return nil 132 | } 133 | 134 | // MustDecodeValue is like DecodeValue but panics on error. 135 | func (e *Event) MustDecodeValue(topics []types.Hash, data []byte, val any) { 136 | err := e.DecodeValue(topics, data, val) 137 | if err != nil { 138 | panic(err) 139 | } 140 | } 141 | 142 | // DecodeValues decodes the event into a map or structure. If a structure is 143 | // given, it must have fields with the same names as the event arguments. 144 | func (e *Event) DecodeValues(topics []types.Hash, data []byte, vals ...any) error { 145 | if e.anonymous { 146 | return e.abi.DecodeValues(e.inputs, data, vals...) 147 | } 148 | if len(topics) != e.inputs.IndexedSize()+1 { 149 | return fmt.Errorf("abi: wrong number of topics for event %s", e.name) 150 | } 151 | if topics[0] != e.topic0 { 152 | return fmt.Errorf("abi: topic0 mismatch for event %s", e.name) 153 | } 154 | indexedVals := make([]any, 0, e.inputs.IndexedSize()) 155 | dataVals := make([]any, 0, e.inputs.DataSize()) 156 | for i := range e.inputs.Elements() { 157 | if i >= len(vals) { 158 | break 159 | } 160 | if e.inputs.Elements()[i].Indexed { 161 | indexedVals = append(indexedVals, vals[i]) 162 | } else { 163 | dataVals = append(dataVals, vals[i]) 164 | } 165 | } 166 | // The anymapper package does not zero out values before decoding into 167 | // it, therefore we can decode topics and data into the same value. 168 | if len(topics) > 1 { 169 | if err := e.abi.DecodeValues(e.inputs.TopicsTuple(), hashSliceToBytes(topics[1:]), indexedVals...); err != nil { 170 | return err 171 | } 172 | } 173 | if len(data) > 0 { 174 | if err := e.abi.DecodeValues(e.inputs.DataTuple(), data, dataVals...); err != nil { 175 | return err 176 | } 177 | } 178 | return nil 179 | } 180 | 181 | // MustDecodeValues is like DecodeValues but panics on error. 182 | func (e *Event) MustDecodeValues(topics []types.Hash, data []byte, vals ...any) { 183 | err := e.DecodeValues(topics, data, vals...) 184 | if err != nil { 185 | panic(err) 186 | } 187 | } 188 | 189 | // String returns the human-readable signature of the event. 190 | func (e *Event) String() string { 191 | var buf strings.Builder 192 | buf.WriteString("event ") 193 | buf.WriteString(e.name) 194 | buf.WriteString(e.inputs.String()) 195 | if e.anonymous { 196 | buf.WriteString(" anonymous") 197 | } 198 | return buf.String() 199 | } 200 | 201 | func (e *Event) calculateTopic0() { 202 | e.topic0 = crypto.Keccak256([]byte(e.signature)) 203 | } 204 | 205 | func (e *Event) generateSignature() { 206 | e.signature = fmt.Sprintf("%s%s", e.name, e.inputs.CanonicalType()) 207 | } 208 | 209 | func hashSliceToBytes(hashes []types.Hash) []byte { 210 | buf := make([]byte, len(hashes)*types.HashLength) 211 | for i, hash := range hashes { 212 | copy(buf[i*types.HashLength:], hash[:]) 213 | } 214 | return buf 215 | } 216 | -------------------------------------------------------------------------------- /abi/fourbytes.go: -------------------------------------------------------------------------------- 1 | package abi 2 | 3 | import "github.com/defiweb/go-eth/hexutil" 4 | 5 | // FourBytes is a 4-byte method selector. 6 | type FourBytes [4]byte 7 | 8 | // Bytes returns the four bytes as a byte slice. 9 | func (f FourBytes) Bytes() []byte { 10 | return f[:] 11 | } 12 | 13 | // Hex returns the four bytes as a hex string. 14 | func (f FourBytes) Hex() string { 15 | return hexutil.BytesToHex(f[:]) 16 | } 17 | 18 | // String returns the four bytes as a hex string. 19 | func (f FourBytes) String() string { 20 | return f.Hex() 21 | } 22 | 23 | // Match returns true if the given ABI data matches the four byte selector. 24 | func (f FourBytes) Match(data []byte) bool { 25 | if len(data) < 4 { 26 | return false 27 | } 28 | return f == FourBytes{data[0], data[1], data[2], data[3]} 29 | } 30 | -------------------------------------------------------------------------------- /abi/fourbytes_test.go: -------------------------------------------------------------------------------- 1 | package abi 2 | 3 | import ( 4 | "crypto/rand" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkFourBytes_Match(b *testing.B) { 9 | data := make([]byte, 32*6+4) 10 | _, _ = rand.Read(data) 11 | f := FourBytes{0x01, 0x02, 0x03, 0x04} 12 | b.ResetTimer() 13 | for i := 0; i < b.N; i++ { 14 | f.Match(data) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /abi/method_test.go: -------------------------------------------------------------------------------- 1 | package abi 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "math/big" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/defiweb/go-eth/hexutil" 13 | ) 14 | 15 | func TestParseMethod(t *testing.T) { 16 | tests := []struct { 17 | signature string 18 | expected string 19 | wantErr bool 20 | }{ 21 | {signature: "foo((uint256,bytes32)[])(uint256)", expected: "function foo((uint256, bytes32)[]) returns (uint256)"}, 22 | {signature: "foo((uint256 a, bytes32 b)[] c)(uint256 d)", expected: "function foo((uint256 a, bytes32 b)[] c) returns (uint256 d)"}, 23 | {signature: "function foo(tuple(uint256 a, bytes32 b)[] memory c) pure returns (uint256 d)", expected: "function foo((uint256 a, bytes32 b)[] c) pure returns (uint256 d)"}, 24 | {signature: "event foo(uint256)", wantErr: true}, 25 | {signature: "error foo(uint256)", wantErr: true}, 26 | {signature: "constructor(uint256)", wantErr: true}, 27 | } 28 | for n, tt := range tests { 29 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 30 | m, err := ParseMethod(tt.signature) 31 | if tt.wantErr { 32 | require.Error(t, err) 33 | } else { 34 | require.NoError(t, err) 35 | assert.Equal(t, tt.expected, m.String()) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestMethod_EncodeArgs(t *testing.T) { 42 | tests := []struct { 43 | signature string 44 | arg []any 45 | expected string 46 | }{ 47 | {signature: "foo()", arg: nil, expected: "c2985578"}, 48 | {signature: "foo(uint256)", arg: []any{1}, expected: "2fbebd380000000000000000000000000000000000000000000000000000000000000001"}, 49 | } 50 | for n, tt := range tests { 51 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 52 | c, err := ParseMethod(tt.signature) 53 | require.NoError(t, err) 54 | enc, err := c.EncodeArgs(tt.arg...) 55 | require.NoError(t, err) 56 | assert.Equal(t, tt.expected, hex.EncodeToString(enc)) 57 | }) 58 | } 59 | } 60 | 61 | func TestMethod_DecodeArg(t *testing.T) { 62 | tests := []struct { 63 | signature string 64 | arg any 65 | data string 66 | expected any 67 | wantErr bool 68 | }{ 69 | {signature: "foo(uint256)", arg: map[string]any{}, data: "2fbebd380000000000000000000000000000000000000000000000000000000000000001", expected: map[string]any{"arg0": big.NewInt(1)}}, 70 | {signature: "foo(uint256)", arg: map[string]any{}, data: "aabbccdd0000000000000000000000000000000000000000000000000000000000000001", wantErr: true}, 71 | } 72 | for n, tt := range tests { 73 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 74 | c, err := ParseMethod(tt.signature) 75 | require.NoError(t, err) 76 | err = c.DecodeArg(hexutil.MustHexToBytes(tt.data), &tt.arg) 77 | if tt.wantErr { 78 | require.Error(t, err) 79 | } else { 80 | require.NoError(t, err) 81 | assert.Equal(t, tt.expected, tt.arg) 82 | } 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /abi/num_test.go: -------------------------------------------------------------------------------- 1 | package abi 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/big" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestIntX_Bytes(t *testing.T) { 14 | tests := []struct { 15 | val *intX 16 | set *big.Int 17 | want []byte 18 | }{ 19 | { 20 | val: newIntX(8), 21 | set: big.NewInt(0), 22 | want: []byte{0x00}, 23 | }, 24 | { 25 | val: newIntX(8), 26 | set: big.NewInt(1), 27 | want: []byte{0x01}, 28 | }, 29 | { 30 | val: newIntX(8), 31 | set: big.NewInt(-1), 32 | want: []byte{0xff}, 33 | }, 34 | { 35 | val: newIntX(8), 36 | set: big.NewInt(127), 37 | want: []byte{0x7f}, 38 | }, 39 | { 40 | val: newIntX(8), 41 | set: big.NewInt(-128), 42 | want: []byte{0x80}, 43 | }, 44 | { 45 | val: newIntX(32), 46 | set: big.NewInt(0), 47 | want: []byte{0x00, 0x00, 0x00, 0x00}, 48 | }, 49 | { 50 | val: newIntX(32), 51 | set: big.NewInt(1), 52 | want: []byte{0x00, 0x00, 0x00, 0x01}, 53 | }, 54 | { 55 | val: newIntX(32), 56 | set: big.NewInt(-1), 57 | want: []byte{0xff, 0xff, 0xff, 0xff}, 58 | }, 59 | { 60 | val: newIntX(32), 61 | set: MaxInt[32], 62 | want: []byte{0x7f, 0xff, 0xff, 0xff}, 63 | }, 64 | { 65 | val: newIntX(32), 66 | set: MinInt[32], 67 | want: []byte{0x80, 0x00, 0x00, 0x00}, 68 | }, 69 | } 70 | for n, tt := range tests { 71 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 72 | require.NoError(t, tt.val.SetBigInt(tt.set)) 73 | assert.Equal(t, tt.want, tt.val.Bytes()) 74 | }) 75 | } 76 | } 77 | 78 | func TestIntX_SetBytes(t *testing.T) { 79 | tests := []struct { 80 | val *intX 81 | bytes []byte 82 | want *big.Int 83 | wantErr bool 84 | }{ 85 | { 86 | val: newIntX(8), 87 | bytes: []byte{0x00}, 88 | want: big.NewInt(0), 89 | }, 90 | { 91 | val: newIntX(8), 92 | bytes: []byte{0x01}, 93 | want: big.NewInt(1), 94 | }, 95 | { 96 | val: newIntX(8), 97 | bytes: []byte{0xff}, 98 | want: big.NewInt(-1), 99 | }, 100 | { 101 | val: newIntX(8), 102 | bytes: []byte{0x7f}, 103 | want: big.NewInt(127), 104 | }, 105 | { 106 | val: newIntX(8), 107 | bytes: []byte{0x80}, 108 | want: big.NewInt(-128), 109 | }, 110 | { 111 | val: newIntX(32), 112 | bytes: []byte{0x00, 0x00, 0x00, 0x00}, 113 | want: big.NewInt(0), 114 | }, 115 | { 116 | val: newIntX(32), 117 | bytes: []byte{0x00, 0x00, 0x00, 0x01}, 118 | want: big.NewInt(1), 119 | }, 120 | { 121 | val: newIntX(32), 122 | bytes: []byte{0xff, 0xff, 0xff, 0xff}, 123 | want: big.NewInt(-1), 124 | }, 125 | { 126 | val: newIntX(32), 127 | bytes: []byte{0x7f, 0xff, 0xff, 0xff}, 128 | want: MaxInt[32], 129 | }, 130 | { 131 | val: newIntX(32), 132 | bytes: []byte{0x80, 0x00, 0x00, 0x00}, 133 | want: MinInt[32], 134 | }, 135 | } 136 | for n, tt := range tests { 137 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 138 | err := tt.val.SetBytes(tt.bytes) 139 | if tt.wantErr { 140 | assert.Error(t, err) 141 | } else { 142 | assert.NoError(t, err) 143 | assert.Equal(t, tt.want, tt.val.val) 144 | } 145 | }) 146 | } 147 | } 148 | 149 | func Test_signedBitLen(t *testing.T) { 150 | tests := []struct { 151 | arg *big.Int 152 | want int 153 | }{ 154 | {arg: big.NewInt(0), want: 0}, 155 | {arg: MaxInt[256], want: 256}, 156 | {arg: MinInt[256], want: 256}, 157 | {arg: MaxUint[256], want: 257}, 158 | {arg: bigIntStr("-0x010000000000000000"), want: 65}, 159 | {arg: bigIntStr("-0x020000000000000000"), want: 66}, 160 | {arg: bigIntStr("-0x030000000000000000"), want: 67}, 161 | } 162 | for n, tt := range tests { 163 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 164 | assert.Equal(t, tt.want, signedBitLen(tt.arg)) 165 | }) 166 | } 167 | } 168 | 169 | func Test_canSetInt(t *testing.T) { 170 | tests := []struct { 171 | x int64 172 | bitLen int 173 | want bool 174 | }{ 175 | {x: 0, bitLen: 8, want: true}, 176 | {x: 1, bitLen: 8, want: true}, 177 | {x: -1, bitLen: 8, want: true}, 178 | {x: 127, bitLen: 8, want: true}, 179 | {x: -128, bitLen: 8, want: true}, 180 | {x: 128, bitLen: 8, want: false}, 181 | {x: -129, bitLen: 8, want: false}, 182 | {x: 0, bitLen: 32, want: true}, 183 | {x: 1, bitLen: 32, want: true}, 184 | {x: -1, bitLen: 32, want: true}, 185 | {x: math.MaxInt32, bitLen: 32, want: true}, 186 | {x: math.MinInt32, bitLen: 32, want: true}, 187 | {x: math.MaxInt32 + 1, bitLen: 32, want: false}, 188 | {x: math.MinInt32 - 1, bitLen: 32, want: false}, 189 | {x: math.MaxInt64, bitLen: 64, want: true}, 190 | {x: math.MinInt64, bitLen: 64, want: true}, 191 | } 192 | for n, tt := range tests { 193 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 194 | assert.Equal(t, tt.want, canSetInt(tt.x, tt.bitLen)) 195 | }) 196 | } 197 | } 198 | 199 | func TestIntX_SetIntUint(t *testing.T) { 200 | tests := []struct { 201 | x uint64 202 | bitLen int 203 | want bool 204 | }{ 205 | {x: 0, bitLen: 8, want: true}, 206 | {x: 1, bitLen: 8, want: true}, 207 | {x: 255, bitLen: 8, want: true}, 208 | {x: 256, bitLen: 8, want: false}, 209 | {x: 0, bitLen: 32, want: true}, 210 | {x: 1, bitLen: 32, want: true}, 211 | {x: math.MaxUint32, bitLen: 32, want: true}, 212 | {x: math.MaxUint32 + 1, bitLen: 32, want: false}, 213 | {x: math.MaxUint64, bitLen: 64, want: true}, 214 | } 215 | for n, tt := range tests { 216 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 217 | assert.Equal(t, tt.want, canSetUint(tt.x, tt.bitLen)) 218 | }) 219 | } 220 | } 221 | 222 | func bigIntStr(s string) *big.Int { 223 | i, ok := new(big.Int).SetString(s, 0) 224 | if !ok { 225 | panic("invalid big.Int string") 226 | } 227 | return i 228 | } 229 | -------------------------------------------------------------------------------- /abi/panic.go: -------------------------------------------------------------------------------- 1 | package abi 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | ) 7 | 8 | // Panic is the Error instance for panic responses. 9 | var Panic = NewError("Panic", NewTupleType(TupleTypeElem{Name: "error", Type: NewUintType(256)})) 10 | 11 | // panicPrefix is the prefix of panic messages. It is the first 4 bytes of the 12 | // keccak256 hash of the string "Panic(uint256)". 13 | var panicPrefix = FourBytes{0x4e, 0x48, 0x7b, 0x71} 14 | 15 | // PanicError represents an error returned by contract calls when the call 16 | // panics. 17 | type PanicError struct { 18 | Code *big.Int 19 | } 20 | 21 | // Error implements the error interface. 22 | func (e PanicError) Error() string { 23 | return fmt.Sprintf("panic: %s", e.Code.String()) 24 | } 25 | 26 | // IsPanic returns true if the data has the panic prefix. 27 | func IsPanic(data []byte) bool { 28 | return panicPrefix.Match(data) && len(data) == 36 29 | } 30 | 31 | // DecodePanic decodes the panic data returned by contract calls. 32 | // If the data is not a valid panic message, it returns nil. 33 | func DecodePanic(data []byte) *big.Int { 34 | // The code below is a slightly optimized version of 35 | // Panic.DecodeValues(data). 36 | if !IsPanic(data) { 37 | return nil 38 | } 39 | s := &UintValue{Size: 256} 40 | t := TupleValue{TupleValueElem{Value: s}} 41 | if _, err := t.DecodeABI(BytesToWords(data[4:])); err != nil { 42 | return nil 43 | } 44 | return &s.Int 45 | } 46 | 47 | // ToPanicError converts the panic data returned by contract calls into a PanicError. 48 | // If the data does not contain a valid panic message, it returns nil. 49 | func ToPanicError(data []byte) error { 50 | if !IsPanic(data) { 51 | return nil 52 | } 53 | return PanicError{Code: DecodePanic(data)} 54 | } 55 | -------------------------------------------------------------------------------- /abi/panic_test.go: -------------------------------------------------------------------------------- 1 | package abi 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/defiweb/go-eth/hexutil" 11 | ) 12 | 13 | func TestPanicPrefix(t *testing.T) { 14 | assert.Equal(t, panicPrefix, Panic.FourBytes()) 15 | } 16 | 17 | func TestDecodePanic(t *testing.T) { 18 | tests := []struct { 19 | data []byte 20 | want uint64 21 | wantNil bool 22 | }{ 23 | { 24 | data: hexutil.MustHexToBytes("0x4e487b710000000000000000000000000000000000000000000000000000000000000000"), 25 | want: 0, 26 | }, 27 | { 28 | data: hexutil.MustHexToBytes("0x4e487b71000000000000000000000000000000000000000000000000000000000000002a"), 29 | want: 42, 30 | }, 31 | { 32 | // Invalid panic prefix. 33 | data: hexutil.MustHexToBytes("0xaaaaaaaa00000000000000000000000000000000000000000000000000000000000000"), 34 | wantNil: true, 35 | }, 36 | { 37 | // Empty panic data. 38 | data: hexutil.MustHexToBytes("0x4e487b71"), 39 | wantNil: true, 40 | }, 41 | } 42 | for n, tt := range tests { 43 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 44 | got := DecodePanic(tt.data) 45 | if tt.wantNil { 46 | assert.Nil(t, got) 47 | } else { 48 | assert.Equal(t, tt.want, got.Uint64()) 49 | } 50 | }) 51 | } 52 | } 53 | 54 | func TestToPanicError(t *testing.T) { 55 | panicErr := ToPanicError(hexutil.MustHexToBytes("0x4e487b710000000000000000000000000000000000000000000000000000000000000020")) 56 | require.NotNil(t, panicErr) 57 | assert.Equal(t, "panic: 32", panicErr.Error()) 58 | } 59 | -------------------------------------------------------------------------------- /abi/revert.go: -------------------------------------------------------------------------------- 1 | package abi 2 | 3 | import "fmt" 4 | 5 | // Revert is the Error instance for revert responses. 6 | var Revert = NewError("Error", NewTupleType(TupleTypeElem{Name: "error", Type: NewStringType()})) 7 | 8 | // revertPrefix is the prefix of revert messages. It is the first 4 bytes of the 9 | // keccak256 hash of the string "Error(string)". 10 | var revertPrefix = FourBytes{0x08, 0xc3, 0x79, 0xa0} 11 | 12 | // RevertError represents an error returned by contract calls when the call 13 | // reverts. 14 | type RevertError struct { 15 | Reason string 16 | } 17 | 18 | // Error implements the error interface. 19 | func (e RevertError) Error() string { 20 | return fmt.Sprintf("revert: %s", e.Reason) 21 | } 22 | 23 | // IsRevert returns true if the data has the revert prefix. 24 | func IsRevert(data []byte) bool { 25 | return revertPrefix.Match(data) && (len(data)-4)%WordLength == 0 26 | } 27 | 28 | // DecodeRevert decodes the revert data returned by contract calls. 29 | // If the data is not a valid revert message, it returns an empty string. 30 | func DecodeRevert(data []byte) string { 31 | // The code below is a slightly optimized version of 32 | // Revert.DecodeValues(data). 33 | if !IsRevert(data) { 34 | return "" 35 | } 36 | s := new(StringValue) 37 | t := TupleValue{TupleValueElem{Value: s}} 38 | if _, err := t.DecodeABI(BytesToWords(data[4:])); err != nil { 39 | return "" 40 | } 41 | return string(*s) 42 | } 43 | 44 | // ToRevertError converts the revert data returned by contract calls into a RevertError. 45 | // If the data does not contain a valid revert message, it returns nil. 46 | func ToRevertError(data []byte) error { 47 | if !IsRevert(data) { 48 | return nil 49 | } 50 | return RevertError{Reason: DecodeRevert(data)} 51 | } 52 | -------------------------------------------------------------------------------- /abi/revert_test.go: -------------------------------------------------------------------------------- 1 | package abi 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/defiweb/go-eth/hexutil" 11 | ) 12 | 13 | func TestRevertPrefix(t *testing.T) { 14 | assert.Equal(t, revertPrefix, Revert.FourBytes()) 15 | } 16 | 17 | func TestDecodeRevert(t *testing.T) { 18 | tests := []struct { 19 | data []byte 20 | want string 21 | }{ 22 | { 23 | data: hexutil.MustHexToBytes("0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e726576657274206d657373616765000000000000000000000000000000000000"), 24 | want: "revert message", 25 | }, 26 | { 27 | data: hexutil.MustHexToBytes("0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004061616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161"), 28 | want: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 29 | }, 30 | { 31 | // Invalid revert prefix. 32 | data: hexutil.MustHexToBytes("0xaaaaaaaa0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e726576657274206d657373616765000000000000000000000000000000000000"), 33 | want: "", 34 | }, 35 | { 36 | // Empty revert data. 37 | data: hexutil.MustHexToBytes("0x08c379a0"), 38 | want: "", 39 | }, 40 | { 41 | // Invalid revert data. 42 | data: hexutil.MustHexToBytes("0x08c379a0726576657274206d657373616765000000000000000000000000000000000000"), 43 | want: "", 44 | }, 45 | } 46 | for n, tt := range tests { 47 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 48 | assert.Equal(t, tt.want, DecodeRevert(tt.data)) 49 | }) 50 | } 51 | } 52 | 53 | func TestToRevertError(t *testing.T) { 54 | revertErr := ToRevertError(hexutil.MustHexToBytes("0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003666f6f0000000000000000000000000000000000000000000000000000000000")) 55 | require.NotNil(t, revertErr) 56 | assert.Equal(t, "revert: foo", revertErr.Error()) 57 | } 58 | -------------------------------------------------------------------------------- /abi/sigparser_test.go: -------------------------------------------------------------------------------- 1 | package abi 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestParseType(t *testing.T) { 11 | tests := []struct { 12 | sig string 13 | want string 14 | wantErr bool 15 | }{ 16 | {sig: "uint256", want: "uint256"}, 17 | {sig: "uint256[]", want: "uint256[]"}, 18 | {sig: "(uint256 a, uint256 b)", want: "(uint256 a, uint256 b)"}, 19 | } 20 | for n, tt := range tests { 21 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 22 | got, err := ParseType(tt.sig) 23 | if tt.wantErr { 24 | assert.Error(t, err) 25 | } else { 26 | assert.NoError(t, err) 27 | assert.Equal(t, tt.want, got.String()) 28 | } 29 | }) 30 | } 31 | } 32 | 33 | func TestParseStruct(t *testing.T) { 34 | tests := []struct { 35 | sig string 36 | want string 37 | wantErr bool 38 | }{ 39 | {sig: "struct { uint256 a; }", want: "(uint256 a)"}, 40 | {sig: "struct test { uint256 a; }", want: "(uint256 a)"}, // name is ignored 41 | {sig: "struct { uint256[] a; }", want: "(uint256[] a)"}, 42 | {sig: "struct { uint256 a; uint256 b; }", want: "(uint256 a, uint256 b)"}, 43 | } 44 | for n, tt := range tests { 45 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 46 | got, err := ParseStruct(tt.sig) 47 | if tt.wantErr { 48 | assert.Error(t, err) 49 | } else { 50 | assert.NoError(t, err) 51 | assert.Equal(t, tt.want, got.String()) 52 | } 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /abi/testdata/abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "Test.CustomUint", 6 | "name": "a", 7 | "type": "uint256" 8 | } 9 | ], 10 | "stateMutability": "nonpayable", 11 | "type": "constructor" 12 | }, 13 | { 14 | "inputs": [ 15 | { 16 | "internalType": "uint256", 17 | "name": "a", 18 | "type": "uint256" 19 | }, 20 | { 21 | "internalType": "uint256", 22 | "name": "b", 23 | "type": "uint256" 24 | } 25 | ], 26 | "name": "ErrorA", 27 | "type": "error" 28 | }, 29 | { 30 | "anonymous": false, 31 | "inputs": [ 32 | { 33 | "indexed": true, 34 | "internalType": "uint256", 35 | "name": "a", 36 | "type": "uint256" 37 | }, 38 | { 39 | "indexed": false, 40 | "internalType": "string", 41 | "name": "b", 42 | "type": "string" 43 | } 44 | ], 45 | "name": "EventA", 46 | "type": "event" 47 | }, 48 | { 49 | "anonymous": false, 50 | "inputs": [ 51 | { 52 | "indexed": true, 53 | "internalType": "uint256", 54 | "name": "a", 55 | "type": "uint256" 56 | }, 57 | { 58 | "indexed": true, 59 | "internalType": "string", 60 | "name": "b", 61 | "type": "string" 62 | } 63 | ], 64 | "name": "EventB", 65 | "type": "event" 66 | }, 67 | { 68 | "anonymous": true, 69 | "inputs": [ 70 | { 71 | "indexed": true, 72 | "internalType": "uint256", 73 | "name": "a", 74 | "type": "uint256" 75 | }, 76 | { 77 | "indexed": false, 78 | "internalType": "string", 79 | "name": "b", 80 | "type": "string" 81 | } 82 | ], 83 | "name": "EventC", 84 | "type": "event" 85 | }, 86 | { 87 | "stateMutability": "nonpayable", 88 | "type": "fallback" 89 | }, 90 | { 91 | "inputs": [ 92 | { 93 | "components": [ 94 | { 95 | "internalType": "bytes32", 96 | "name": "A", 97 | "type": "bytes32" 98 | }, 99 | { 100 | "internalType": "bytes32", 101 | "name": "B", 102 | "type": "bytes32" 103 | }, 104 | { 105 | "internalType": "enum Test.Status", 106 | "name": "status", 107 | "type": "uint8" 108 | } 109 | ], 110 | "internalType": "struct Test.Struct[2][2]", 111 | "name": "a", 112 | "type": "tuple[2][2]" 113 | } 114 | ], 115 | "name": "Bar", 116 | "outputs": [ 117 | { 118 | "internalType": "uint8[2][2]", 119 | "name": "", 120 | "type": "uint8[2][2]" 121 | } 122 | ], 123 | "stateMutability": "nonpayable", 124 | "type": "function" 125 | }, 126 | { 127 | "inputs": [ 128 | { 129 | "internalType": "Test.CustomUint", 130 | "name": "a", 131 | "type": "uint256" 132 | } 133 | ], 134 | "name": "Foo", 135 | "outputs": [ 136 | { 137 | "internalType": "Test.CustomUint", 138 | "name": "", 139 | "type": "uint256" 140 | } 141 | ], 142 | "stateMutability": "nonpayable", 143 | "type": "function" 144 | }, 145 | { 146 | "inputs": [], 147 | "name": "structField", 148 | "outputs": [ 149 | { 150 | "internalType": "bytes32", 151 | "name": "A", 152 | "type": "bytes32" 153 | }, 154 | { 155 | "internalType": "bytes32", 156 | "name": "B", 157 | "type": "bytes32" 158 | }, 159 | { 160 | "internalType": "enum Test.Status", 161 | "name": "status", 162 | "type": "uint8" 163 | } 164 | ], 165 | "stateMutability": "view", 166 | "type": "function" 167 | }, 168 | { 169 | "inputs": [ 170 | { 171 | "internalType": "uint256", 172 | "name": "", 173 | "type": "uint256" 174 | } 175 | ], 176 | "name": "structsArray", 177 | "outputs": [ 178 | { 179 | "internalType": "bytes32", 180 | "name": "A", 181 | "type": "bytes32" 182 | }, 183 | { 184 | "internalType": "bytes32", 185 | "name": "B", 186 | "type": "bytes32" 187 | }, 188 | { 189 | "internalType": "enum Test.Status", 190 | "name": "status", 191 | "type": "uint8" 192 | } 193 | ], 194 | "stateMutability": "view", 195 | "type": "function" 196 | }, 197 | { 198 | "inputs": [ 199 | { 200 | "internalType": "address", 201 | "name": "", 202 | "type": "address" 203 | } 204 | ], 205 | "name": "structsMapping", 206 | "outputs": [ 207 | { 208 | "internalType": "bytes32", 209 | "name": "A", 210 | "type": "bytes32" 211 | }, 212 | { 213 | "internalType": "bytes32", 214 | "name": "B", 215 | "type": "bytes32" 216 | }, 217 | { 218 | "internalType": "enum Test.Status", 219 | "name": "status", 220 | "type": "uint8" 221 | } 222 | ], 223 | "stateMutability": "view", 224 | "type": "function" 225 | }, 226 | { 227 | "stateMutability": "payable", 228 | "type": "receive" 229 | } 230 | ] 231 | -------------------------------------------------------------------------------- /abi/testdata/abi.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.7.0 <0.9.0; 2 | 3 | contract Test { 4 | // Enum 5 | enum Status { 6 | Inactive, 7 | Active, 8 | Paused 9 | } 10 | 11 | // Struct 12 | struct Struct { 13 | bytes32 A; 14 | bytes32 B; 15 | Status status; 16 | } 17 | 18 | // Custom Type 19 | type CustomUint is uint256; 20 | 21 | // Events 22 | event EventA(uint256 indexed a, string b); 23 | event EventB(uint256 indexed a, string indexed b); 24 | event EventC(uint256 indexed a, string b) anonymous; 25 | 26 | // Error 27 | error ErrorA(uint256 a, uint256 b); 28 | 29 | // Public Variable 30 | Struct public structField; 31 | 32 | // Mapping 33 | mapping(address => Struct) public structsMapping; 34 | 35 | // Array 36 | Struct[] public structsArray; 37 | 38 | // Constructor 39 | constructor(CustomUint a) {} 40 | 41 | // Functions 42 | function Foo(CustomUint a) public returns (CustomUint) { return a; } 43 | 44 | function Bar(Struct[2][2] memory a) public returns (uint8[2][2] memory) { return [[0, 0], [0, 0]]; } 45 | 46 | // Fallback and Receive functions 47 | fallback() external {} 48 | 49 | receive() payable external {} 50 | } 51 | -------------------------------------------------------------------------------- /abi/type_test.go: -------------------------------------------------------------------------------- 1 | package abi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | type nullType struct{} 10 | type nullValue struct{} 11 | type dynamicNullType struct{ nullType } 12 | 13 | func (n nullType) Value() Value { return new(nullValue) } 14 | func (n nullType) String() string { return "null" } 15 | func (n nullType) IsDynamic() bool { return false } 16 | func (n nullType) CanonicalType() string { return "null" } 17 | func (n nullValue) IsDynamic() bool { return false } 18 | func (n nullValue) EncodeABI() (Words, error) { return nil, nil } 19 | func (n nullValue) DecodeABI(_ Words) (int, error) { return 0, nil } 20 | func (n dynamicNullType) IsDynamic() bool { return true } 21 | 22 | func TestAliasType(t *testing.T) { 23 | v := NewAliasType("alias", nullType{}) 24 | assert.Equal(t, &nullValue{}, v.Value()) 25 | assert.Equal(t, "alias", v.String()) 26 | assert.Equal(t, "null", v.CanonicalType()) 27 | } 28 | 29 | func TestTupleType(t *testing.T) { 30 | v := NewTupleType( 31 | TupleTypeElem{Name: "foo", Type: nullType{}}, 32 | TupleTypeElem{Name: "bar", Type: nullType{}}, 33 | TupleTypeElem{Type: nullType{}}, 34 | ) 35 | assert.Equal(t, &TupleValue{ 36 | {Name: "foo", Value: &nullValue{}}, 37 | {Name: "bar", Value: &nullValue{}}, 38 | {Name: "arg2", Value: &nullValue{}}, 39 | }, v.Value()) 40 | assert.Equal(t, "(null foo, null bar, null)", v.String()) 41 | assert.Equal(t, "(null,null,null)", v.CanonicalType()) 42 | } 43 | 44 | func TestEventTupleType(t *testing.T) { 45 | v := NewEventTupleType( 46 | EventTupleElem{Name: "foo", Type: nullType{}}, 47 | EventTupleElem{Name: "bar", Type: nullType{}, Indexed: true}, 48 | EventTupleElem{Name: "qux", Type: dynamicNullType{}, Indexed: true}, 49 | EventTupleElem{Type: nullType{}, Indexed: true}, 50 | EventTupleElem{Type: nullType{}}, 51 | ) 52 | assert.Equal(t, &TupleValue{ 53 | {Name: "bar", Value: &nullValue{}}, 54 | {Name: "qux", Value: &nullValue{}}, 55 | {Name: "topic3", Value: &nullValue{}}, 56 | {Name: "foo", Value: &nullValue{}}, 57 | {Name: "data1", Value: &nullValue{}}, 58 | }, v.Value()) 59 | assert.Equal(t, "(null foo, null indexed bar, null indexed qux, null indexed, null)", v.String()) 60 | assert.Equal(t, "(null,null,null,null,null)", v.CanonicalType()) 61 | assert.Equal(t, "(null bar, bytes32 qux, null topic3)", v.TopicsTuple().String()) 62 | assert.Equal(t, "(null foo, null data1)", v.DataTuple().String()) 63 | } 64 | 65 | func TestArrayType(t *testing.T) { 66 | v := NewArrayType(&nullType{}) 67 | assert.Equal(t, "null[]", v.String()) 68 | assert.Equal(t, "null[]", v.CanonicalType()) 69 | assert.Equal(t, &ArrayValue{ 70 | Type: &nullType{}, 71 | Elems: nil, 72 | }, v.Value()) 73 | } 74 | 75 | func TestFixedArrayType(t *testing.T) { 76 | v := NewFixedArrayType(&nullType{}, 2) 77 | assert.Equal(t, "null[2]", v.String()) 78 | assert.Equal(t, "null[2]", v.CanonicalType()) 79 | assert.Equal(t, &FixedArrayValue{ 80 | &nullValue{}, 81 | &nullValue{}, 82 | }, v.Value()) 83 | } 84 | 85 | func TestBytesType(t *testing.T) { 86 | v := NewBytesType() 87 | assert.Equal(t, "bytes", v.String()) 88 | assert.Equal(t, "bytes", v.CanonicalType()) 89 | assert.Equal(t, &BytesValue{}, v.Value()) 90 | } 91 | 92 | func TestStringType(t *testing.T) { 93 | v := NewStringType() 94 | assert.Equal(t, "string", v.String()) 95 | assert.Equal(t, "string", v.CanonicalType()) 96 | assert.Equal(t, new(StringValue), v.Value()) 97 | } 98 | 99 | func TestFixedBytesType(t *testing.T) { 100 | v := NewFixedBytesType(2) 101 | assert.Equal(t, "bytes2", v.String()) 102 | assert.Equal(t, "bytes2", v.CanonicalType()) 103 | assert.Equal(t, &FixedBytesValue{0, 0}, v.Value()) 104 | } 105 | 106 | func TestUintType(t *testing.T) { 107 | v := NewUintType(256) 108 | assert.Equal(t, "uint256", v.String()) 109 | assert.Equal(t, "uint256", v.CanonicalType()) 110 | assert.Equal(t, &UintValue{Size: 256}, v.Value()) 111 | } 112 | 113 | func TestIntType(t *testing.T) { 114 | v := NewIntType(256) 115 | assert.Equal(t, "int256", v.String()) 116 | assert.Equal(t, "int256", v.CanonicalType()) 117 | assert.Equal(t, &IntValue{Size: 256}, v.Value()) 118 | } 119 | 120 | func TestBoolType(t *testing.T) { 121 | v := NewBoolType() 122 | assert.Equal(t, "bool", v.String()) 123 | assert.Equal(t, "bool", v.CanonicalType()) 124 | assert.Equal(t, new(BoolValue), v.Value()) 125 | } 126 | 127 | func TestAddressType(t *testing.T) { 128 | v := NewAddressType() 129 | assert.Equal(t, "address", v.String()) 130 | assert.Equal(t, "address", v.CanonicalType()) 131 | assert.Equal(t, new(AddressValue), v.Value()) 132 | } 133 | -------------------------------------------------------------------------------- /abi/word.go: -------------------------------------------------------------------------------- 1 | package abi 2 | 3 | import ( 4 | "fmt" 5 | "math/bits" 6 | ) 7 | 8 | // WordLength is the number of bytes in an EVM word. 9 | const WordLength = 32 10 | 11 | // Word represents a 32-bytes EVM word. 12 | type Word [WordLength]byte 13 | 14 | func BytesToWords(b []byte) []Word { 15 | var words Words 16 | words.SetBytes(b) 17 | return words 18 | } 19 | 20 | // SetBytesPadRight sets the word to the given bytes, padded on the right. 21 | func (w *Word) SetBytesPadRight(b []byte) error { 22 | if len(b) > WordLength { 23 | return fmt.Errorf("abi: cannot set %d-byte data to a %d-byte word", len(b), WordLength) 24 | } 25 | for i := len(b); i < WordLength; i++ { 26 | w[i] = 0 27 | } 28 | copy((*w)[:], b) 29 | return nil 30 | } 31 | 32 | // SetBytesPadLeft sets the word to the given bytes, padded on the left. 33 | func (w *Word) SetBytesPadLeft(b []byte) error { 34 | if len(b) > WordLength { 35 | return fmt.Errorf("abi: cannot set %d-byte data to a %d-byte word", len(b), WordLength) 36 | } 37 | for i := 0; i < WordLength-len(b); i++ { 38 | w[i] = 0 39 | } 40 | copy((*w)[WordLength-len(b):], b) 41 | return nil 42 | } 43 | 44 | // Bytes returns the word as a byte slice. 45 | func (w Word) Bytes() []byte { 46 | return w[:] 47 | } 48 | 49 | // IsZero returns true if all bytes in then word are zeros. 50 | func (w Word) IsZero() bool { 51 | for _, b := range w { 52 | if b != 0 { 53 | return false 54 | } 55 | } 56 | return true 57 | } 58 | 59 | // LeadingZeros returns the number of leading zero bits. 60 | func (w Word) LeadingZeros() int { 61 | for i, b := range w { 62 | if b != 0 { 63 | return i*8 + bits.LeadingZeros8(b) 64 | } 65 | } 66 | return WordLength * 8 67 | } 68 | 69 | // TrailingZeros returns the number of trailing zero bits. 70 | func (w Word) TrailingZeros() int { 71 | for i := len(w) - 1; i >= 0; i-- { 72 | if w[i] != 0 { 73 | return (len(w)-i-1)*8 + bits.TrailingZeros8(w[i]) 74 | } 75 | } 76 | return WordLength * 8 77 | } 78 | 79 | // Words is a slice of words. 80 | type Words []Word 81 | 82 | // SetBytes sets the words to the given bytes. 83 | func (w *Words) SetBytes(b []byte) { 84 | *w = make([]Word, requiredWords(len(b))) 85 | for i := 0; i < len(b); i += WordLength { 86 | if len(b)-i < WordLength { 87 | copy((*w)[i/WordLength][i%WordLength:], b[i:]) 88 | } else { 89 | copy((*w)[i/WordLength][:], b[i:i+WordLength]) 90 | } 91 | } 92 | } 93 | 94 | // AppendBytes appends the given bytes to the words. 95 | func (w *Words) AppendBytes(b []byte) { 96 | for len(b) == 0 { 97 | return 98 | } 99 | c := requiredWords(len(b)) 100 | l := len(*w) 101 | w.grow(c) 102 | for i := 0; i < len(b); i += WordLength { 103 | if len(b)-i < WordLength { 104 | copy((*w)[l+i/WordLength][i%WordLength:], b[i:]) 105 | } else { 106 | copy((*w)[l+i/WordLength][:], b[i:i+WordLength]) 107 | } 108 | } 109 | } 110 | 111 | // Bytes returns the words as a byte slice. 112 | func (w Words) Bytes() []byte { 113 | b := make([]byte, len(w)*WordLength) 114 | for i, word := range w { 115 | copy(b[i*WordLength:], word[:]) 116 | } 117 | return b 118 | } 119 | 120 | func (w *Words) grow(n int) { 121 | w.resize(len(*w) + n) 122 | } 123 | 124 | func (w *Words) resize(n int) { 125 | if cap(*w) < n { 126 | cpy := make([]Word, len(*w), n) 127 | copy(cpy, *w) 128 | *w = cpy 129 | } 130 | *w = (*w)[:n] 131 | } 132 | 133 | // requiredWords returns the number of words required to store the given number 134 | // of bytes. 135 | func requiredWords(n int) int { 136 | if n <= 0 { 137 | return 0 138 | } 139 | return 1 + (n-1)/WordLength 140 | } 141 | -------------------------------------------------------------------------------- /abi/word_test.go: -------------------------------------------------------------------------------- 1 | package abi 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/defiweb/go-eth/hexutil" 12 | ) 13 | 14 | func TestWord_SetBytesPadRight(t *testing.T) { 15 | tests := []struct { 16 | args []byte 17 | want Word 18 | wantErr bool 19 | }{ 20 | { 21 | args: []byte{0x01}, 22 | want: hexToWord("0x0100000000000000000000000000000000000000000000000000000000000000"), 23 | }, 24 | { 25 | args: []byte{0x01, 0x02}, 26 | want: hexToWord("0x0102000000000000000000000000000000000000000000000000000000000000"), 27 | }, 28 | { 29 | args: bytes.Repeat([]byte{0x00}, 33), 30 | wantErr: true, 31 | }, 32 | } 33 | for n, tt := range tests { 34 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 35 | var w Word 36 | err := w.SetBytesPadRight(tt.args) 37 | if tt.wantErr { 38 | assert.Error(t, err) 39 | } else { 40 | require.NoError(t, err) 41 | assert.Equal(t, tt.want, w) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestWord_SetBytesPadLeft(t *testing.T) { 48 | tests := []struct { 49 | args []byte 50 | want Word 51 | wantErr bool 52 | }{ 53 | { 54 | args: []byte{0x01}, 55 | want: hexToWord("0x0000000000000000000000000000000000000000000000000000000000000001"), 56 | }, 57 | { 58 | args: []byte{0x01, 0x02}, 59 | want: hexToWord("0x0000000000000000000000000000000000000000000000000000000000000102"), 60 | }, 61 | { 62 | args: bytes.Repeat([]byte{0x00}, 33), 63 | wantErr: true, 64 | }, 65 | } 66 | for n, tt := range tests { 67 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 68 | var w Word 69 | err := w.SetBytesPadLeft(tt.args) 70 | if tt.wantErr { 71 | assert.Error(t, err) 72 | } else { 73 | require.NoError(t, err) 74 | assert.Equal(t, tt.want, w) 75 | } 76 | }) 77 | } 78 | } 79 | 80 | func TestWord_IsZero(t *testing.T) { 81 | assert.True(t, hexToWord("0x0000000000000000000000000000000000000000000000000000000000000000").IsZero()) 82 | assert.False(t, hexToWord("0x0000000000000000000000000000000000000000000000000000000000000001").IsZero()) 83 | assert.False(t, hexToWord("0x1000000000000000000000000000000000000000000000000000000000000000").IsZero()) 84 | } 85 | 86 | func TestWords_SetBytes(t *testing.T) { 87 | tests := []struct { 88 | arg []byte 89 | want Words 90 | }{ 91 | { 92 | arg: []byte{0x01}, 93 | want: Words{hexToWord("0x0100000000000000000000000000000000000000000000000000000000000000")}, 94 | }, 95 | { 96 | arg: hexutil.MustHexToBytes("0x0000000000000000000000000000000000000000000000000000000000000001"), 97 | want: hexToWords("0x0000000000000000000000000000000000000000000000000000000000000001"), 98 | }, 99 | { 100 | arg: hexutil.MustHexToBytes("0x000000000000000000000000000000000000000000000000000000000000000001"), 101 | want: hexToWords("0x00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000"), 102 | }, 103 | } 104 | for n, tt := range tests { 105 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 106 | var got Words 107 | got.SetBytes(tt.arg) 108 | assert.Equal(t, tt.want, got) 109 | }) 110 | } 111 | } 112 | 113 | func TestWord_LeadingZeros(t *testing.T) { 114 | tests := []struct { 115 | arg Word 116 | want int 117 | }{ 118 | { 119 | arg: hexToWord("0x0000000000000000000000000000000000000000000000000000000000000000"), 120 | want: 256, 121 | }, 122 | { 123 | arg: hexToWord("0x0000000000000000000000000000000000000000000000000000000000000001"), 124 | want: 255, 125 | }, 126 | { 127 | arg: hexToWord("0x8000000000000000000000000000000000000000000000000000000000000000"), 128 | want: 0, 129 | }, 130 | } 131 | for n, tt := range tests { 132 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 133 | assert.Equal(t, tt.want, tt.arg.LeadingZeros()) 134 | }) 135 | } 136 | } 137 | 138 | func TestWord_TrailingZeros(t *testing.T) { 139 | tests := []struct { 140 | arg Word 141 | want int 142 | }{ 143 | { 144 | arg: hexToWord("0x0000000000000000000000000000000000000000000000000000000000000000"), 145 | want: 256, 146 | }, 147 | { 148 | arg: hexToWord("0x0000000000000000000000000000000000000000000000000000000000000001"), 149 | want: 0, 150 | }, 151 | { 152 | arg: hexToWord("0x8000000000000000000000000000000000000000000000000000000000000000"), 153 | want: 255, 154 | }, 155 | } 156 | for n, tt := range tests { 157 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 158 | assert.Equal(t, tt.want, tt.arg.TrailingZeros()) 159 | }) 160 | } 161 | } 162 | 163 | func TestWords_AppendBytes(t *testing.T) { 164 | tests := []struct { 165 | words Words 166 | arg []byte 167 | want Words 168 | }{ 169 | { 170 | words: Words{}, 171 | arg: []byte{0x01}, 172 | want: hexToWords("0x0100000000000000000000000000000000000000000000000000000000000000"), 173 | }, 174 | { 175 | words: hexToWords("0x0000000000000000000000000000000000000000000000000000000000000001"), 176 | arg: []byte{0x01}, 177 | want: hexToWords("0x00000000000000000000000000000000000000000000000000000000000000010100000000000000000000000000000000000000000000000000000000000000"), 178 | }, 179 | { 180 | words: hexToWords("0x0000000000000000000000000000000000000000000000000000000000000001"), 181 | arg: hexutil.MustHexToBytes("0x0000000000000000000000000000000000000000000000000000000000000001"), 182 | want: hexToWords("0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001"), 183 | }, 184 | } 185 | for n, tt := range tests { 186 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 187 | tt.words.AppendBytes(tt.arg) 188 | assert.Equal(t, tt.want, tt.words) 189 | }) 190 | } 191 | } 192 | 193 | func hexToWord(h string) Word { 194 | return BytesToWords(hexutil.MustHexToBytes(h))[0] 195 | } 196 | 197 | func hexToWords(h string) Words { 198 | return BytesToWords(hexutil.MustHexToBytes(h)) 199 | } 200 | -------------------------------------------------------------------------------- /crypto/crypto.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "fmt" 6 | 7 | "github.com/defiweb/go-eth/types" 8 | ) 9 | 10 | // Signer is an interface for signing data. 11 | type Signer interface { 12 | // SignHash signs a hash. 13 | SignHash(hash types.Hash) (*types.Signature, error) 14 | 15 | // SignMessage signs a message. 16 | SignMessage(data []byte) (*types.Signature, error) 17 | 18 | // SignTransaction signs a transaction. 19 | SignTransaction(tx *types.Transaction) error 20 | } 21 | 22 | // Recoverer is an interface for recovering addresses from signatures. 23 | type Recoverer interface { 24 | // RecoverHash recovers the address from a hash and signature. 25 | RecoverHash(hash types.Hash, sig types.Signature) (*types.Address, error) 26 | 27 | // RecoverMessage recovers the address from a message and signature. 28 | RecoverMessage(data []byte, sig types.Signature) (*types.Address, error) 29 | 30 | // RecoverTransaction recovers the address from a transaction. 31 | RecoverTransaction(tx *types.Transaction) (*types.Address, error) 32 | } 33 | 34 | // AddMessagePrefix adds the Ethereum message prefix to the given data as 35 | // defined in EIP-191. 36 | func AddMessagePrefix(data []byte) []byte { 37 | return []byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)) 38 | } 39 | 40 | // ECSigner returns a Signer implementation for ECDSA. 41 | func ECSigner(key *ecdsa.PrivateKey) Signer { return &ecSigner{key} } 42 | 43 | // ECRecoverer is a Recoverer implementation for ECDSA. 44 | var ECRecoverer Recoverer = &ecRecoverer{} 45 | 46 | type ( 47 | ecSigner struct{ key *ecdsa.PrivateKey } 48 | ecRecoverer struct{} 49 | ) 50 | 51 | func (s *ecSigner) SignHash(hash types.Hash) (*types.Signature, error) { 52 | return ecSignHash(s.key, hash) 53 | } 54 | 55 | func (s *ecSigner) SignMessage(data []byte) (*types.Signature, error) { 56 | return ecSignMessage(s.key, data) 57 | } 58 | 59 | func (s *ecSigner) SignTransaction(tx *types.Transaction) error { 60 | return ecSignTransaction(s.key, tx) 61 | } 62 | 63 | func (r *ecRecoverer) RecoverHash(hash types.Hash, sig types.Signature) (*types.Address, error) { 64 | return ecRecoverHash(hash, sig) 65 | } 66 | 67 | func (r *ecRecoverer) RecoverMessage(data []byte, sig types.Signature) (*types.Address, error) { 68 | return ecRecoverMessage(data, sig) 69 | } 70 | 71 | func (r *ecRecoverer) RecoverTransaction(tx *types.Transaction) (*types.Address, error) { 72 | return ecRecoverTransaction(tx) 73 | } 74 | -------------------------------------------------------------------------------- /crypto/ecdsa.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "errors" 7 | "fmt" 8 | "math/big" 9 | 10 | "github.com/btcsuite/btcd/btcec/v2" 11 | btcececdsa "github.com/btcsuite/btcd/btcec/v2/ecdsa" 12 | 13 | "github.com/defiweb/go-eth/types" 14 | ) 15 | 16 | var s256 = btcec.S256() 17 | 18 | // ECPublicKeyToAddress returns the Ethereum address for the given ECDSA public key. 19 | func ECPublicKeyToAddress(pub *ecdsa.PublicKey) (addr types.Address) { 20 | b := Keccak256(elliptic.Marshal(s256, pub.X, pub.Y)[1:]) 21 | copy(addr[:], b[12:]) 22 | return 23 | } 24 | 25 | // ecSignHash signs the given hash with the given private key. 26 | func ecSignHash(key *ecdsa.PrivateKey, hash types.Hash) (*types.Signature, error) { 27 | if key == nil { 28 | return nil, fmt.Errorf("missing private key") 29 | } 30 | privKey, _ := btcec.PrivKeyFromBytes(key.D.Bytes()) 31 | sig, err := btcececdsa.SignCompact(privKey, hash.Bytes(), false) 32 | if err != nil { 33 | return nil, err 34 | } 35 | v := sig[0] 36 | switch v { 37 | case 27, 28: 38 | v -= 27 39 | } 40 | copy(sig, sig[1:]) 41 | sig[64] = v 42 | return types.SignatureFromBytesPtr(sig), nil 43 | } 44 | 45 | // ecSignMessage signs the given message with the given private key. 46 | func ecSignMessage(key *ecdsa.PrivateKey, data []byte) (*types.Signature, error) { 47 | if key == nil { 48 | return nil, fmt.Errorf("missing private key") 49 | } 50 | sig, err := ecSignHash(key, Keccak256(AddMessagePrefix(data))) 51 | if err != nil { 52 | return nil, err 53 | } 54 | sig.V = new(big.Int).Add(sig.V, big.NewInt(27)) 55 | return sig, nil 56 | } 57 | 58 | // ecSignTransaction signs the given transaction with the given private key. 59 | func ecSignTransaction(key *ecdsa.PrivateKey, tx *types.Transaction) error { 60 | if key == nil { 61 | return fmt.Errorf("missing private key") 62 | } 63 | from := ECPublicKeyToAddress(&key.PublicKey) 64 | if tx.From != nil && *tx.From != from { 65 | return fmt.Errorf("invalid signer address: %s", tx.From) 66 | } 67 | hash, err := signingHash(tx) 68 | if err != nil { 69 | return err 70 | } 71 | sig, err := ecSignHash(key, hash) 72 | if err != nil { 73 | return err 74 | } 75 | sv, sr, ss := sig.V, sig.R, sig.S 76 | switch tx.Type { 77 | case types.LegacyTxType: 78 | if tx.ChainID != nil { 79 | sv = new(big.Int).Add(sv, new(big.Int).SetUint64(*tx.ChainID*2)) 80 | sv = new(big.Int).Add(sv, big.NewInt(35)) 81 | } else { 82 | sv = new(big.Int).Add(sv, big.NewInt(27)) 83 | } 84 | case types.AccessListTxType: 85 | case types.DynamicFeeTxType: 86 | default: 87 | return fmt.Errorf("unsupported transaction type: %d", tx.Type) 88 | } 89 | tx.From = &from 90 | tx.Signature = types.SignatureFromVRSPtr(sv, sr, ss) 91 | return nil 92 | } 93 | 94 | // ecRecoverHash recovers the Ethereum address from the given hash and signature. 95 | func ecRecoverHash(hash types.Hash, sig types.Signature) (*types.Address, error) { 96 | if sig.V.BitLen() > 8 { 97 | return nil, errors.New("invalid signature: V has more than 8 bits") 98 | } 99 | if sig.R.BitLen() > 256 { 100 | return nil, errors.New("invalid signature: R has more than 256 bits") 101 | } 102 | if sig.S.BitLen() > 256 { 103 | return nil, errors.New("invalid signature: S has more than 256 bits") 104 | } 105 | v := byte(sig.V.Uint64()) 106 | switch v { 107 | case 0, 1: 108 | v += 27 109 | } 110 | rb := sig.R.Bytes() 111 | sb := sig.S.Bytes() 112 | bin := make([]byte, 65) 113 | bin[0] = v 114 | copy(bin[1+(32-len(rb)):], rb) 115 | copy(bin[33+(32-len(sb)):], sb) 116 | pub, _, err := btcececdsa.RecoverCompact(bin, hash.Bytes()) 117 | if err != nil { 118 | return nil, err 119 | } 120 | addr := ECPublicKeyToAddress(pub.ToECDSA()) 121 | return &addr, nil 122 | } 123 | 124 | // ecRecoverMessage recovers the Ethereum address from the given message and signature. 125 | func ecRecoverMessage(data []byte, sig types.Signature) (*types.Address, error) { 126 | sig.V = new(big.Int).Sub(sig.V, big.NewInt(27)) 127 | return ecRecoverHash(Keccak256(AddMessagePrefix(data)), sig) 128 | } 129 | 130 | // ecRecoverTransaction recovers the Ethereum address from the given transaction. 131 | func ecRecoverTransaction(tx *types.Transaction) (*types.Address, error) { 132 | if tx.Signature == nil { 133 | return nil, fmt.Errorf("signature is missing") 134 | } 135 | sig := *tx.Signature 136 | switch tx.Type { 137 | case types.LegacyTxType: 138 | if tx.Signature.V.Cmp(big.NewInt(35)) >= 0 { 139 | x := new(big.Int).Sub(sig.V, big.NewInt(35)) 140 | 141 | // Derive the chain ID from the signature. 142 | chainID := new(big.Int).Div(x, big.NewInt(2)) 143 | if tx.ChainID != nil && *tx.ChainID != chainID.Uint64() { 144 | return nil, fmt.Errorf("invalid chain ID: %d", chainID) 145 | } 146 | 147 | // Derive the recovery byte from the signature. 148 | sig.V = new(big.Int).Add(new(big.Int).Mod(x, big.NewInt(2)), big.NewInt(27)) 149 | } else { 150 | sig.V = new(big.Int).Sub(sig.V, big.NewInt(27)) 151 | } 152 | case types.AccessListTxType: 153 | case types.DynamicFeeTxType: 154 | default: 155 | return nil, fmt.Errorf("unsupported transaction type: %d", tx.Type) 156 | } 157 | hash, err := signingHash(tx) 158 | if err != nil { 159 | return nil, err 160 | } 161 | return ecRecoverHash(hash, sig) 162 | } 163 | -------------------------------------------------------------------------------- /crypto/keccak.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "golang.org/x/crypto/sha3" 5 | 6 | "github.com/defiweb/go-eth/types" 7 | ) 8 | 9 | // Keccak256 calculates the Keccak256 hash of the given data. 10 | func Keccak256(data ...[]byte) types.Hash { 11 | h := sha3.NewLegacyKeccak256() 12 | for _, i := range data { 13 | h.Write(i) 14 | } 15 | return types.MustHashFromBytes(h.Sum(nil), types.PadNone) 16 | } 17 | -------------------------------------------------------------------------------- /crypto/keccak_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestKeccak256(t *testing.T) { 11 | tests := []struct { 12 | data [][]byte 13 | want string 14 | }{ 15 | { 16 | data: [][]byte{[]byte("")}, 17 | want: "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", 18 | }, 19 | { 20 | data: [][]byte{[]byte("ab")}, 21 | want: "0x67fad3bfa1e0321bd021ca805ce14876e50acac8ca8532eda8cbf924da565160", 22 | }, 23 | { 24 | data: [][]byte{[]byte("a"), []byte("b")}, 25 | want: "0x67fad3bfa1e0321bd021ca805ce14876e50acac8ca8532eda8cbf924da565160", 26 | }, 27 | } 28 | for n, tt := range tests { 29 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 30 | assert.Equal(t, tt.want, Keccak256(tt.data...).String()) 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /crypto/transaction.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | 7 | "github.com/defiweb/go-rlp" 8 | 9 | "github.com/defiweb/go-eth/types" 10 | ) 11 | 12 | func signingHash(t *types.Transaction) (types.Hash, error) { 13 | var ( 14 | chainID = uint64(1) 15 | nonce = uint64(0) 16 | gasPrice = big.NewInt(0) 17 | gasLimit = uint64(0) 18 | maxPriorityFeePerGas = big.NewInt(0) 19 | maxFeePerGas = big.NewInt(0) 20 | to = ([]byte)(nil) 21 | value = big.NewInt(0) 22 | accessList = (types.AccessList)(nil) 23 | ) 24 | if t.ChainID != nil { 25 | chainID = *t.ChainID 26 | } 27 | if t.Nonce != nil { 28 | nonce = *t.Nonce 29 | } 30 | if t.GasPrice != nil { 31 | gasPrice = t.GasPrice 32 | } 33 | if t.GasLimit != nil { 34 | gasLimit = *t.GasLimit 35 | } 36 | if t.MaxPriorityFeePerGas != nil { 37 | maxPriorityFeePerGas = t.MaxPriorityFeePerGas 38 | } 39 | if t.MaxFeePerGas != nil { 40 | maxFeePerGas = t.MaxFeePerGas 41 | } 42 | if t.To != nil { 43 | to = t.To[:] 44 | } 45 | if t.Value != nil { 46 | value = t.Value 47 | } 48 | if t.AccessList != nil { 49 | accessList = t.AccessList 50 | } 51 | switch t.Type { 52 | case types.LegacyTxType: 53 | list := rlp.NewList( 54 | rlp.NewUint(nonce), 55 | rlp.NewBigInt(gasPrice), 56 | rlp.NewUint(gasLimit), 57 | rlp.NewBytes(to), 58 | rlp.NewBigInt(value), 59 | rlp.NewBytes(t.Input), 60 | ) 61 | if t.ChainID != nil && *t.ChainID != 0 { 62 | list.Append( 63 | rlp.NewUint(chainID), 64 | rlp.NewUint(0), 65 | rlp.NewUint(0), 66 | ) 67 | } 68 | bin, err := list.EncodeRLP() 69 | if err != nil { 70 | return types.Hash{}, err 71 | } 72 | return Keccak256(bin), nil 73 | case types.AccessListTxType: 74 | bin, err := rlp.NewList( 75 | rlp.NewUint(chainID), 76 | rlp.NewUint(nonce), 77 | rlp.NewBigInt(gasPrice), 78 | rlp.NewUint(gasLimit), 79 | rlp.NewBytes(to), 80 | rlp.NewBigInt(value), 81 | rlp.NewBytes(t.Input), 82 | &t.AccessList, 83 | ).EncodeRLP() 84 | if err != nil { 85 | return types.Hash{}, err 86 | } 87 | bin = append([]byte{byte(t.Type)}, bin...) 88 | return Keccak256(bin), nil 89 | case types.DynamicFeeTxType: 90 | bin, err := rlp.NewList( 91 | rlp.NewUint(chainID), 92 | rlp.NewUint(nonce), 93 | rlp.NewBigInt(maxPriorityFeePerGas), 94 | rlp.NewBigInt(maxFeePerGas), 95 | rlp.NewUint(gasLimit), 96 | rlp.NewBytes(to), 97 | rlp.NewBigInt(value), 98 | rlp.NewBytes(t.Input), 99 | &accessList, 100 | ).EncodeRLP() 101 | if err != nil { 102 | return types.Hash{}, err 103 | } 104 | bin = append([]byte{byte(t.Type)}, bin...) 105 | return Keccak256(bin), nil 106 | default: 107 | return types.Hash{}, fmt.Errorf("invalid transaction type: %d", t.Type) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /crypto/transaction_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/defiweb/go-eth/types" 11 | ) 12 | 13 | func Test_singingHash(t1 *testing.T) { 14 | tests := []struct { 15 | tx *types.Transaction 16 | want types.Hash 17 | }{ 18 | // Empty transaction: 19 | { 20 | tx: &types.Transaction{}, 21 | want: types.MustHashFromHex("5460be86ce1e4ca0564b5761c6e7070d9f054b671f5404268335000806423d75", types.PadNone), 22 | }, 23 | // Legacy transaction: 24 | { 25 | tx: (&types.Transaction{}). 26 | SetType(types.LegacyTxType). 27 | SetFrom(types.MustAddressFromHex("0x1111111111111111111111111111111111111111")). 28 | SetTo(types.MustAddressFromHex("0x2222222222222222222222222222222222222222")). 29 | SetGasLimit(100000). 30 | SetGasPrice(big.NewInt(1000000000)). 31 | SetInput([]byte{1, 2, 3, 4}). 32 | SetNonce(1). 33 | SetValue(big.NewInt(1000000000000000000)). 34 | SetSignature(types.MustSignatureFromHex("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f")). 35 | SetChainID(1), 36 | want: types.MustHashFromHex("1efbe489013ac8c0dad2202f68ac12657471df8d80f70e0683ec07b0564a32ca", types.PadNone), 37 | }, 38 | // Access list transaction: 39 | { 40 | tx: (&types.Transaction{}). 41 | SetType(types.AccessListTxType). 42 | SetFrom(types.MustAddressFromHex("0x1111111111111111111111111111111111111111")). 43 | SetTo(types.MustAddressFromHex("0x2222222222222222222222222222222222222222")). 44 | SetGasLimit(100000). 45 | SetGasPrice(big.NewInt(1000000000)). 46 | SetInput([]byte{1, 2, 3, 4}). 47 | SetNonce(1). 48 | SetValue(big.NewInt(1000000000000000000)). 49 | SetSignature(types.MustSignatureFromHex("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f")). 50 | SetChainID(1). 51 | SetAccessList(types.AccessList{ 52 | types.AccessTuple{ 53 | Address: types.MustAddressFromHex("0x3333333333333333333333333333333333333333"), 54 | StorageKeys: []types.Hash{ 55 | types.MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), 56 | types.MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", types.PadNone), 57 | }, 58 | }, 59 | }), 60 | want: types.MustHashFromHex("71cba0039a020b7a524d7746b79bf6d1f8a521eb1a76715d00116ef1c0f56107", types.PadNone), 61 | }, 62 | // Dynamic fee transaction with access list: 63 | { 64 | tx: (&types.Transaction{}). 65 | SetType(types.DynamicFeeTxType). 66 | SetFrom(types.MustAddressFromHex("0x1111111111111111111111111111111111111111")). 67 | SetTo(types.MustAddressFromHex("0x2222222222222222222222222222222222222222")). 68 | SetGasLimit(100000). 69 | SetInput([]byte{1, 2, 3, 4}). 70 | SetNonce(1). 71 | SetValue(big.NewInt(1000000000000000000)). 72 | SetSignature(types.MustSignatureFromHex("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f")). 73 | SetChainID(1). 74 | SetMaxPriorityFeePerGas(big.NewInt(1000000000)). 75 | SetMaxFeePerGas(big.NewInt(2000000000)). 76 | SetAccessList(types.AccessList{ 77 | types.AccessTuple{ 78 | Address: types.MustAddressFromHex("0x3333333333333333333333333333333333333333"), 79 | StorageKeys: []types.Hash{ 80 | types.MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), 81 | types.MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", types.PadNone), 82 | }, 83 | }, 84 | }), 85 | want: types.MustHashFromHex("a66ab756479bfd56f29658a8a199319094e84711e8a2de073ec136ef5179c4c9", types.PadNone), 86 | }, 87 | // Dynamic fee transaction with no access list: 88 | { 89 | tx: (&types.Transaction{}). 90 | SetType(types.DynamicFeeTxType). 91 | SetFrom(types.MustAddressFromHex("0x1111111111111111111111111111111111111111")). 92 | SetTo(types.MustAddressFromHex("0x2222222222222222222222222222222222222222")). 93 | SetGasLimit(100000). 94 | SetInput([]byte{1, 2, 3, 4}). 95 | SetNonce(1). 96 | SetValue(big.NewInt(1000000000000000000)). 97 | SetSignature(types.MustSignatureFromHex("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f")). 98 | SetChainID(1). 99 | SetMaxPriorityFeePerGas(big.NewInt(1000000000)). 100 | SetMaxFeePerGas(big.NewInt(2000000000)), 101 | want: types.MustHashFromHex("c3266152306909bfe339f90fad4f73f958066860300b5a22b98ee6a1d629706c", types.PadNone), 102 | }, 103 | // Example from EIP-155: 104 | { 105 | tx: (&types.Transaction{}). 106 | SetType(types.LegacyTxType). 107 | SetChainID(1). 108 | SetTo(types.MustAddressFromHex("0x3535353535353535353535353535353535353535")). 109 | SetGasLimit(21000). 110 | SetGasPrice(big.NewInt(20000000000)). 111 | SetNonce(9). 112 | SetValue(big.NewInt(1000000000000000000)). 113 | SetSignature(types.SignatureFromVRS( 114 | func() *big.Int { 115 | v, _ := new(big.Int).SetString("37", 10) 116 | return v 117 | }(), 118 | func() *big.Int { 119 | v, _ := new(big.Int).SetString("18515461264373351373200002665853028612451056578545711640558177340181847433846", 10) 120 | return v 121 | }(), 122 | func() *big.Int { 123 | v, _ := new(big.Int).SetString("46948507304638947509940763649030358759909902576025900602547168820602576006531", 10) 124 | return v 125 | }(), 126 | )), 127 | want: types.MustHashFromHex("daf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53", types.PadNone), 128 | }, 129 | } 130 | for n, tt := range tests { 131 | t1.Run(fmt.Sprintf("case-%d", n+1), func(t1 *testing.T) { 132 | sh, err := signingHash(tt.tx) 133 | require.NoError(t1, err) 134 | require.Equal(t1, tt.want, sh) 135 | }) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /examples/abi-enc-dec-prog/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/defiweb/go-eth/abi" 7 | "github.com/defiweb/go-eth/hexutil" 8 | ) 9 | 10 | func main() { 11 | // Create ABI type: 12 | dataABI := abi.NewTupleType( 13 | abi.TupleTypeElem{ 14 | Name: "intVal", 15 | Type: abi.NewIntType(256), 16 | }, 17 | abi.TupleTypeElem{ 18 | Name: "boolVal", 19 | Type: abi.NewBoolType(), 20 | }, 21 | abi.TupleTypeElem{ 22 | Name: "stringVal", 23 | Type: abi.NewStringType(), 24 | }, 25 | ) 26 | 27 | // Encode data: 28 | encodedData := abi.MustEncodeValues(dataABI, 42, true, "Hello, world!") 29 | 30 | // Print encoded data: 31 | fmt.Printf("Encoded data: %s\n", hexutil.BytesToHex(encodedData)) 32 | 33 | // Decode data: 34 | var ( 35 | intVal int 36 | boolVal bool 37 | stringVal string 38 | ) 39 | abi.MustDecodeValues(dataABI, encodedData, &intVal, &boolVal, &stringVal) 40 | 41 | // Print decoded data: 42 | fmt.Printf("Decoded data: %d, %t, %s\n", intVal, boolVal, stringVal) 43 | } 44 | -------------------------------------------------------------------------------- /examples/abi-enc-dec-struct/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/defiweb/go-eth/abi" 7 | "github.com/defiweb/go-eth/hexutil" 8 | ) 9 | 10 | // Data is a struct that represents the data we want to encode and decode. 11 | type Data struct { 12 | IntValue int `abi:"intVal"` 13 | BoolValue bool `abi:"boolVal"` 14 | StringValue string `abi:"stringVal"` 15 | } 16 | 17 | func main() { 18 | // Parse ABI type: 19 | dataABI := abi.MustParseStruct(`struct Data { int256 intVal; bool boolVal; string stringVal; }`) 20 | 21 | // Encode data: 22 | encodedData := abi.MustEncodeValue(dataABI, Data{ 23 | IntValue: 42, 24 | BoolValue: true, 25 | StringValue: "Hello, world!", 26 | }) 27 | 28 | // Print encoded data: 29 | fmt.Printf("Encoded data: %s\n", hexutil.BytesToHex(encodedData)) 30 | 31 | // Decode data: 32 | var decodedData Data 33 | abi.MustDecodeValue(dataABI, encodedData, &decodedData) 34 | 35 | // Print decoded data: 36 | fmt.Printf("Decoded data: %+v\n", decodedData) 37 | } 38 | -------------------------------------------------------------------------------- /examples/abi-enc-dec-vars/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/defiweb/go-eth/abi" 7 | "github.com/defiweb/go-eth/hexutil" 8 | ) 9 | 10 | func main() { 11 | // Parse ABI type: 12 | dataABI := abi.MustParseStruct(`struct Data { int256 intVal; bool boolVal; string stringVal; }`) 13 | 14 | // Encode data: 15 | encodedData := abi.MustEncodeValues(dataABI, 42, true, "Hello, world!") 16 | 17 | // Print encoded data: 18 | fmt.Printf("Encoded data: %s\n", hexutil.BytesToHex(encodedData)) 19 | 20 | // Decode data: 21 | var ( 22 | intVal int 23 | boolVal bool 24 | stringVal string 25 | ) 26 | abi.MustDecodeValues(dataABI, encodedData, &intVal, &boolVal, &stringVal) 27 | 28 | // Print decoded data: 29 | fmt.Printf("Decoded data: %d, %t, %s\n", intVal, boolVal, stringVal) 30 | } 31 | -------------------------------------------------------------------------------- /examples/call-abi/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/defiweb/go-eth/abi" 8 | "github.com/defiweb/go-eth/rpc" 9 | "github.com/defiweb/go-eth/rpc/transport" 10 | "github.com/defiweb/go-eth/types" 11 | ) 12 | 13 | type Call3 struct { 14 | Target types.Address `abi:"target"` 15 | AllowFailure bool `abi:"allowFailure"` 16 | CallData []byte `abi:"callData"` 17 | } 18 | 19 | type Result struct { 20 | Success bool `abi:"success"` 21 | ReturnData []byte `abi:"returnData"` 22 | } 23 | 24 | func main() { 25 | // Create transport. 26 | t, err := transport.NewHTTP(transport.HTTPOptions{URL: "https://ethereum.publicnode.com"}) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | // Create a JSON-RPC client. 32 | c, err := rpc.NewClient(rpc.WithTransport(t)) 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | // Parse contract ABI. 38 | multicall := abi.MustParseSignatures( 39 | "struct Call { address target; bytes callData; }", 40 | "struct Call3 { address target; bool allowFailure; bytes callData; }", 41 | "struct Call3Value { address target; bool allowFailure; uint256 value; bytes callData; }", 42 | "struct Result { bool success; bytes returnData; }", 43 | "function aggregate(Call[] calldata calls) public payable returns (uint256 blockNumber, bytes[] memory returnData)", 44 | "function aggregate3(Call3[] calldata calls) public payable returns (Result[] memory returnData)", 45 | "function aggregate3Value(Call3Value[] calldata calls) public payable returns (Result[] memory returnData)", 46 | "function blockAndAggregate(Call[] calldata calls) public payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData)", 47 | "function getBasefee() view returns (uint256 basefee)", 48 | "function getBlockHash(uint256 blockNumber) view returns (bytes32 blockHash)", 49 | "function getBlockNumber() view returns (uint256 blockNumber)", 50 | "function getChainId() view returns (uint256 chainid)", 51 | "function getCurrentBlockCoinbase() view returns (address coinbase)", 52 | "function getCurrentBlockDifficulty() view returns (uint256 difficulty)", 53 | "function getCurrentBlockGasLimit() view returns (uint256 gaslimit)", 54 | "function getCurrentBlockTimestamp() view returns (uint256 timestamp)", 55 | "function getEthBalance(address addr) view returns (uint256 balance)", 56 | "function getLastBlockHash() view returns (bytes32 blockHash)", 57 | "function tryAggregate(bool requireSuccess, Call[] calldata calls) public payable returns (Result[] memory returnData)", 58 | "function tryBlockAndAggregate(bool requireSuccess, Call[] calldata calls) public payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData)", 59 | ) 60 | 61 | // Prepare a calldata. 62 | // In this example we will call the `getCurrentBlockGasLimit` and `getCurrentBlockTimestamp` methods 63 | // on the Multicall3 contract. 64 | calldata := multicall.Methods["aggregate3"].MustEncodeArgs([]Call3{ 65 | { 66 | Target: types.MustAddressFromHex("0xcA11bde05977b3631167028862bE2a173976CA11"), 67 | CallData: multicall.Methods["getCurrentBlockGasLimit"].MustEncodeArgs(), 68 | }, 69 | { 70 | Target: types.MustAddressFromHex("0xcA11bde05977b3631167028862bE2a173976CA11"), 71 | CallData: multicall.Methods["getCurrentBlockTimestamp"].MustEncodeArgs(), 72 | }, 73 | }) 74 | 75 | // Prepare a call. 76 | call := types.NewCall(). 77 | SetTo(types.MustAddressFromHex("0xcA11bde05977b3631167028862bE2a173976CA11")). 78 | SetInput(calldata) 79 | 80 | // Call the contract. 81 | b, _, err := c.Call(context.Background(), call, types.LatestBlockNumber) 82 | if err != nil { 83 | panic(err) 84 | } 85 | 86 | // Decode the result. 87 | var ( 88 | results []Result 89 | gasLimit uint64 90 | timestamp uint64 91 | ) 92 | multicall.Methods["aggregate3"].MustDecodeValues(b, &results) 93 | multicall.Methods["getCurrentBlockGasLimit"].MustDecodeValues(results[0].ReturnData, &gasLimit) 94 | multicall.Methods["getCurrentBlockTimestamp"].MustDecodeValues(results[1].ReturnData, ×tamp) 95 | 96 | // Print the result. 97 | fmt.Println("Gas limit:", gasLimit) 98 | fmt.Println("Timestamp:", timestamp) 99 | } 100 | -------------------------------------------------------------------------------- /examples/call/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/big" 7 | 8 | "github.com/defiweb/go-eth/abi" 9 | "github.com/defiweb/go-eth/rpc" 10 | "github.com/defiweb/go-eth/rpc/transport" 11 | "github.com/defiweb/go-eth/types" 12 | ) 13 | 14 | func main() { 15 | // Create transport. 16 | t, err := transport.NewHTTP(transport.HTTPOptions{URL: "https://ethereum.publicnode.com"}) 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | // Create a JSON-RPC client. 22 | c, err := rpc.NewClient(rpc.WithTransport(t)) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | // Parse method signature. 28 | balanceOf := abi.MustParseMethod("balanceOf(address)(uint256)") 29 | 30 | // Prepare a calldata. 31 | calldata := balanceOf.MustEncodeArgs("0xd8da6bf26964af9d7eed9e03e53415d37aa96045") 32 | 33 | // Prepare a call. 34 | call := types.NewCall(). 35 | SetTo(types.MustAddressFromHex("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")). 36 | SetInput(calldata) 37 | 38 | // Call balanceOf. 39 | b, _, err := c.Call(context.Background(), call, types.LatestBlockNumber) 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | // Decode the result. 45 | var balance *big.Int 46 | balanceOf.MustDecodeValues(b, &balance) 47 | 48 | // Print the result. 49 | fmt.Printf("Balance: %s\n", balance.String()) 50 | } 51 | -------------------------------------------------------------------------------- /examples/connect/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/defiweb/go-eth/rpc" 8 | "github.com/defiweb/go-eth/rpc/transport" 9 | ) 10 | 11 | func main() { 12 | // Create transport. 13 | // 14 | // There are several other transports available: 15 | // - HTTP (NewHTTP) 16 | // - WebSocket (NewWebsocket) 17 | // - IPC (NewIPC) 18 | t, err := transport.NewHTTP(transport.HTTPOptions{URL: "https://ethereum.publicnode.com"}) 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | // Create a JSON-RPC client. 24 | c, err := rpc.NewClient(rpc.WithTransport(t)) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | // Get the latest block number. 30 | b, err := c.BlockNumber(context.Background()) 31 | if err != nil { 32 | panic(err) 33 | } 34 | fmt.Println("Latest block number:", b) 35 | } 36 | -------------------------------------------------------------------------------- /examples/contract-hra-abi/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | 7 | "github.com/defiweb/go-eth/abi" 8 | ) 9 | 10 | func main() { 11 | erc20, err := abi.ParseSignatures( 12 | "function name() public view returns (string)", 13 | "function symbol() public view returns (string)", 14 | "function decimals() public view returns (uint8)", 15 | "function totalSupply() public view returns (uint256)", 16 | "function balanceOf(address _owner) public view returns (uint256 balance)", 17 | "function transfer(address _to, uint256 _value) public returns (bool success)", 18 | "function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)", 19 | "function approve(address _spender, uint256 _value) public returns (bool success)", 20 | "function allowance(address _owner, address _spender) public view returns (uint256 remaining)", 21 | "event Transfer(address indexed _from, address indexed _to, uint256 _value)", 22 | "event Approval(address indexed _owner, address indexed _spender, uint256 _value)", 23 | ) 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | transfer := erc20.Methods["transfer"] 29 | calldata, err := transfer.EncodeArgs( 30 | "0x1234567890123456789012345678901234567890", 31 | big.NewInt(1e18), 32 | ) 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | fmt.Printf("Transfer calldata: 0x%x\n", calldata) 38 | } 39 | -------------------------------------------------------------------------------- /examples/contract-json-abi/erc20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": false, 18 | "inputs": [ 19 | { 20 | "name": "_spender", 21 | "type": "address" 22 | }, 23 | { 24 | "name": "_value", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "approve", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "bool" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "totalSupply", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": false, 55 | "inputs": [ 56 | { 57 | "name": "_from", 58 | "type": "address" 59 | }, 60 | { 61 | "name": "_to", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "_value", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "transferFrom", 70 | "outputs": [ 71 | { 72 | "name": "", 73 | "type": "bool" 74 | } 75 | ], 76 | "payable": false, 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "constant": true, 82 | "inputs": [], 83 | "name": "decimals", 84 | "outputs": [ 85 | { 86 | "name": "", 87 | "type": "uint8" 88 | } 89 | ], 90 | "payable": false, 91 | "stateMutability": "view", 92 | "type": "function" 93 | }, 94 | { 95 | "constant": true, 96 | "inputs": [ 97 | { 98 | "name": "_owner", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "balanceOf", 103 | "outputs": [ 104 | { 105 | "name": "balance", 106 | "type": "uint256" 107 | } 108 | ], 109 | "payable": false, 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "constant": true, 115 | "inputs": [], 116 | "name": "symbol", 117 | "outputs": [ 118 | { 119 | "name": "", 120 | "type": "string" 121 | } 122 | ], 123 | "payable": false, 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "constant": false, 129 | "inputs": [ 130 | { 131 | "name": "_to", 132 | "type": "address" 133 | }, 134 | { 135 | "name": "_value", 136 | "type": "uint256" 137 | } 138 | ], 139 | "name": "transfer", 140 | "outputs": [ 141 | { 142 | "name": "", 143 | "type": "bool" 144 | } 145 | ], 146 | "payable": false, 147 | "stateMutability": "nonpayable", 148 | "type": "function" 149 | }, 150 | { 151 | "constant": true, 152 | "inputs": [ 153 | { 154 | "name": "_owner", 155 | "type": "address" 156 | }, 157 | { 158 | "name": "_spender", 159 | "type": "address" 160 | } 161 | ], 162 | "name": "allowance", 163 | "outputs": [ 164 | { 165 | "name": "", 166 | "type": "uint256" 167 | } 168 | ], 169 | "payable": false, 170 | "stateMutability": "view", 171 | "type": "function" 172 | }, 173 | { 174 | "payable": true, 175 | "stateMutability": "payable", 176 | "type": "fallback" 177 | }, 178 | { 179 | "anonymous": false, 180 | "inputs": [ 181 | { 182 | "indexed": true, 183 | "name": "owner", 184 | "type": "address" 185 | }, 186 | { 187 | "indexed": true, 188 | "name": "spender", 189 | "type": "address" 190 | }, 191 | { 192 | "indexed": false, 193 | "name": "value", 194 | "type": "uint256" 195 | } 196 | ], 197 | "name": "Approval", 198 | "type": "event" 199 | }, 200 | { 201 | "anonymous": false, 202 | "inputs": [ 203 | { 204 | "indexed": true, 205 | "name": "from", 206 | "type": "address" 207 | }, 208 | { 209 | "indexed": true, 210 | "name": "to", 211 | "type": "address" 212 | }, 213 | { 214 | "indexed": false, 215 | "name": "value", 216 | "type": "uint256" 217 | } 218 | ], 219 | "name": "Transfer", 220 | "type": "event" 221 | } 222 | ] 223 | -------------------------------------------------------------------------------- /examples/contract-json-abi/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | 7 | "github.com/defiweb/go-eth/abi" 8 | ) 9 | 10 | func main() { 11 | erc20, err := abi.LoadJSON("erc20.json") 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | transfer := erc20.Methods["transfer"] 17 | calldata, err := transfer.EncodeArgs( 18 | "0x1234567890123456789012345678901234567890", 19 | big.NewInt(1e18), 20 | ) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | fmt.Printf("Transfer calldata: 0x%x\n", calldata) 26 | } 27 | -------------------------------------------------------------------------------- /examples/custom-type-advenced/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/defiweb/go-eth/abi" 7 | "github.com/defiweb/go-eth/hexutil" 8 | ) 9 | 10 | // BoolFlagsType is a custom type that represents a 256-bit bitfield. 11 | // 12 | // It must implement the abi.Type interface. 13 | type BoolFlagsType struct{} 14 | 15 | // IsDynamic returns true if the type is dynamic-length, like string or bytes. 16 | func (b BoolFlagsType) IsDynamic() bool { 17 | return false 18 | } 19 | 20 | // CanonicalType is the type as it would appear in the ABI. 21 | // It must only use the types defined in the ABI specification: 22 | // https://docs.soliditylang.org/en/latest/abi-spec.html 23 | func (b BoolFlagsType) CanonicalType() string { 24 | return "bytes32" 25 | } 26 | 27 | // String returns the custom type name. 28 | func (b BoolFlagsType) String() string { 29 | return "BoolFlags" 30 | } 31 | 32 | // Value returns the zero value for this type. 33 | func (b BoolFlagsType) Value() abi.Value { 34 | return &BoolFlagsValue{} 35 | } 36 | 37 | // BoolFlagsValue is the value of the custom type. 38 | // 39 | // It must implement the abi.Value interface. 40 | type BoolFlagsValue [256]bool 41 | 42 | // IsDynamic returns true if the type is dynamic-length, like string or bytes. 43 | func (b BoolFlagsValue) IsDynamic() bool { 44 | return false 45 | } 46 | 47 | // EncodeABI encodes the value to the ABI format. 48 | func (b BoolFlagsValue) EncodeABI() (abi.Words, error) { 49 | var w abi.Word 50 | for i, v := range b { 51 | if v { 52 | w[i/8] |= 1 << uint(i%8) 53 | } 54 | } 55 | return abi.Words{w}, nil 56 | } 57 | 58 | // DecodeABI decodes the value from the ABI format. 59 | func (b *BoolFlagsValue) DecodeABI(words abi.Words) (int, error) { 60 | if len(words) == 0 { 61 | return 0, fmt.Errorf("abi: cannot decode BytesFlags from empty data") 62 | } 63 | for i, v := range words[0] { 64 | for j := 0; j < 8; j++ { 65 | b[i*8+j] = v&(1< 256 { 87 | return fmt.Errorf("abi: cannot map []bool of length %d to BytesFlags", len(src)) 88 | } 89 | for i, v := range src { 90 | b[i] = v 91 | } 92 | } 93 | return nil 94 | } 95 | 96 | // MapTo maps value to a different type. 97 | func (b *BoolFlagsValue) MapTo(_ abi.Mapper, dst any) error { 98 | switch dst := dst.(type) { 99 | case *[256]bool: 100 | *dst = *b 101 | case *[]bool: 102 | *dst = make([]bool, 256) 103 | for i, v := range b { 104 | (*dst)[i] = v 105 | } 106 | } 107 | return nil 108 | } 109 | 110 | func main() { 111 | // Add custom type. 112 | abi.Default.Types["BoolFlags"] = &BoolFlagsType{} 113 | 114 | // Generate calldata. 115 | setFlags := abi.MustParseMethod("setFlags(BoolFlags flags)") 116 | calldata, _ := setFlags.EncodeArgs( 117 | []bool{true, false, true, true, false, true, false, true}, 118 | ) 119 | 120 | // Print the calldata. 121 | fmt.Printf("Calldata: %s\n", hexutil.BytesToHex(calldata)) 122 | } 123 | -------------------------------------------------------------------------------- /examples/custom-type-simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/defiweb/go-eth/abi" 7 | "github.com/defiweb/go-eth/hexutil" 8 | ) 9 | 10 | type Point struct { 11 | X int 12 | Y int 13 | } 14 | 15 | func main() { 16 | // Add custom type. 17 | abi.Default.Types["Point"] = abi.MustParseStruct("struct {int256 x; int256 y;}") 18 | 19 | // Generate calldata. 20 | addTriangle := abi.MustParseMethod("addTriangle(Point a, Point b, Point c)") 21 | calldata := addTriangle.MustEncodeArgs( 22 | Point{X: 1, Y: 2}, 23 | Point{X: 3, Y: 4}, 24 | Point{X: 5, Y: 6}, 25 | ) 26 | 27 | // Print the calldata. 28 | fmt.Printf("Calldata: %s\n", hexutil.BytesToHex(calldata)) 29 | } 30 | -------------------------------------------------------------------------------- /examples/events/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/big" 7 | 8 | "github.com/defiweb/go-eth/abi" 9 | "github.com/defiweb/go-eth/rpc" 10 | "github.com/defiweb/go-eth/rpc/transport" 11 | "github.com/defiweb/go-eth/types" 12 | ) 13 | 14 | func main() { 15 | // Create transport. 16 | t, err := transport.NewHTTP(transport.HTTPOptions{URL: "https://ethereum.publicnode.com"}) 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | // Create a JSON-RPC client. 22 | c, err := rpc.NewClient(rpc.WithTransport(t)) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | transfer := abi.MustParseEvent("Transfer(address indexed src, address indexed dst, uint256 wad)") 28 | 29 | // Create filter query. 30 | query := types.NewFilterLogsQuery(). 31 | SetAddresses(types.MustAddressFromHex("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2")). 32 | SetFromBlock(types.BlockNumberFromUint64Ptr(16492400)). 33 | SetToBlock(types.BlockNumberFromUint64Ptr(16492400)). 34 | SetTopics([]types.Hash{transfer.Topic0()}) 35 | 36 | // Fetch logs for WETH transfer events. 37 | logs, err := c.GetLogs(context.Background(), query) 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | // Decode and print events. 43 | for _, log := range logs { 44 | var src, dst types.Address 45 | var wad *big.Int 46 | transfer.MustDecodeValues(log.Topics, log.Data, &src, &dst, &wad) 47 | fmt.Printf("Transfer: %s -> %s: %s\n", src.String(), dst.String(), wad.String()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/key-mnemonic/key-mnemonic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/defiweb/go-eth/wallet" 7 | ) 8 | 9 | func main() { 10 | // Parse mnemonic. 11 | mnemonic, err := wallet.NewMnemonic("gravity trophy shrimp suspect sheriff avocado label trust dove tragic pitch title network myself spell task protect smooth sword diary brain blossom under bulb", "") 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | // Parse derivation path. 17 | path, err := wallet.ParseDerivationPath("m/44'/60'/0'/10/10") 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | // Derive private key. 23 | key, err := mnemonic.Derive(path) 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | // Print the address of the derived private key. 29 | fmt.Println("Private key:", key.Address().String()) 30 | } 31 | -------------------------------------------------------------------------------- /examples/send-tx/key.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "2d800d93b065ce011af83f316cef9f0d005b0aa4", 3 | "crypto": { 4 | "cipher": "aes-128-ctr", 5 | "ciphertext": "8051dbab2d2415613751ee755d3b9a1f191c2fa15b4de9349848dcf44e656331", 6 | "cipherparams": { 7 | "iv": "4eb5e582782f64d18c58ddc56692fe91" 8 | }, 9 | "kdf": "scrypt", 10 | "kdfparams": { 11 | "dklen": 32, 12 | "n": 262144, 13 | "p": 1, 14 | "r": 8, 15 | "salt": "8b819d893ebda23c4b31e96dfd1a7f4514a5483840f28fb679197f0fa315ade4" 16 | }, 17 | "mac": "5ea8c70945a1c07f1121ab2798392158bf51eb356854040c8a8bfcb2a23ca5c7" 18 | }, 19 | "id": "53697e14-f0e4-4f87-b300-4163a61bc5ef", 20 | "version": 3 21 | } 22 | -------------------------------------------------------------------------------- /examples/send-tx/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/big" 7 | 8 | "github.com/defiweb/go-eth/abi" 9 | "github.com/defiweb/go-eth/rpc" 10 | "github.com/defiweb/go-eth/rpc/transport" 11 | "github.com/defiweb/go-eth/txmodifier" 12 | "github.com/defiweb/go-eth/types" 13 | "github.com/defiweb/go-eth/wallet" 14 | ) 15 | 16 | func main() { 17 | // Load the private key. 18 | key, err := wallet.NewKeyFromJSON("./key.json", "test123") 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | // Create transport. 24 | t, err := transport.NewHTTP(transport.HTTPOptions{URL: "https://ethereum.publicnode.com"}) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | // Create a JSON-RPC client. 30 | c, err := rpc.NewClient( 31 | // Transport is always required. 32 | rpc.WithTransport(t), 33 | 34 | // Specify a key for signing transactions. If provided, the client 35 | // uses it with SignTransaction, SendTransaction, and Sign methods 36 | // instead of relying on the node for signing. 37 | rpc.WithKeys(key), 38 | 39 | // Specify a default address for SendTransaction when the transaction 40 | // does not have a 'From' field set. 41 | rpc.WithDefaultAddress(key.Address()), 42 | 43 | // TX modifiers enable modifications to the transaction before signing 44 | // and sending to the node. While not mandatory, without them, transaction 45 | // parameters like gas limit, gas price, and nonce must be set manually. 46 | rpc.WithTXModifiers( 47 | // GasLimitEstimator automatically estimates the gas limit for the 48 | // transaction. 49 | txmodifier.NewGasLimitEstimator(txmodifier.GasLimitEstimatorOptions{ 50 | Multiplier: 1.25, 51 | }), 52 | 53 | // GasFeeEstimator automatically estimates the gas price for the 54 | // transaction based on the current market conditions. 55 | txmodifier.NewEIP1559GasFeeEstimator(txmodifier.EIP1559GasFeeEstimatorOptions{ 56 | GasPriceMultiplier: 1.25, 57 | PriorityFeePerGasMultiplier: 1.25, 58 | }), 59 | 60 | // NonceProvider automatically sets the nonce for the transaction. 61 | txmodifier.NewNonceProvider(txmodifier.NonceProviderOptions{ 62 | UsePendingBlock: false, 63 | }), 64 | 65 | // ChainIDProvider automatically sets the chain ID for the transaction. 66 | txmodifier.NewChainIDProvider(txmodifier.ChainIDProviderOptions{ 67 | Replace: false, 68 | Cache: true, 69 | }), 70 | ), 71 | ) 72 | if err != nil { 73 | panic(err) 74 | } 75 | 76 | // Parse method signature. 77 | transfer := abi.MustParseMethod("transfer(address, uint256)(bool)") 78 | 79 | // Prepare a calldata for transfer call. 80 | calldata := transfer.MustEncodeArgs("0xd8da6bf26964af9d7eed9e03e53415d37aa96045", new(big.Int).Mul(big.NewInt(100), big.NewInt(1e6))) 81 | 82 | // Prepare a transaction. 83 | tx := types.NewTransaction(). 84 | SetTo(types.MustAddressFromHex("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")). 85 | SetInput(calldata) 86 | 87 | txHash, _, err := c.SendTransaction(context.Background(), tx) 88 | if err != nil { 89 | panic(err) 90 | } 91 | 92 | // Print the transaction hash. 93 | fmt.Printf("Transaction hash: %s\n", txHash.String()) 94 | } 95 | -------------------------------------------------------------------------------- /examples/subscription/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/big" 7 | "os" 8 | "os/signal" 9 | 10 | "github.com/defiweb/go-eth/abi" 11 | "github.com/defiweb/go-eth/rpc" 12 | "github.com/defiweb/go-eth/rpc/transport" 13 | "github.com/defiweb/go-eth/types" 14 | ) 15 | 16 | func main() { 17 | ctx, ctxCancel := signal.NotifyContext(context.Background(), os.Interrupt) 18 | defer ctxCancel() 19 | 20 | // Create transport. 21 | t, err := transport.NewWebsocket(transport.WebsocketOptions{ 22 | Context: ctx, 23 | URL: "wss://ethereum.publicnode.com", 24 | }) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | // Create a JSON-RPC client. 30 | c, err := rpc.NewClient(rpc.WithTransport(t)) 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | // Parse event signature. 36 | transfer := abi.MustParseEvent("event Transfer(address indexed src, address indexed dst, uint256 wad)") 37 | 38 | // Create a filter query. 39 | query := types.NewFilterLogsQuery(). 40 | SetAddresses(types.MustAddressFromHex("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2")). 41 | SetTopics([]types.Hash{transfer.Topic0()}) 42 | 43 | // Fetch logs for WETH transfer events. 44 | logs, err := c.SubscribeLogs(ctx, query) 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | // Decode and print events. 50 | for log := range logs { 51 | var ( 52 | src types.Address 53 | dst types.Address 54 | wad *big.Int 55 | ) 56 | transfer.MustDecodeValues(log.Topics, log.Data, &src, &dst, &wad) 57 | fmt.Printf("Transfer: %s -> %s: %s\n", src.String(), dst.String(), wad.String()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/defiweb/go-eth 2 | 3 | retract v0.4.0 4 | 5 | go 1.18 6 | 7 | require ( 8 | github.com/btcsuite/btcd v0.24.0 9 | github.com/btcsuite/btcd/btcec/v2 v2.3.2 10 | github.com/btcsuite/btcd/btcutil v1.1.5 11 | github.com/defiweb/go-anymapper v0.3.0 12 | github.com/defiweb/go-rlp v0.3.0 13 | github.com/defiweb/go-sigparser v0.6.0 14 | github.com/stretchr/testify v1.8.4 15 | github.com/tyler-smith/go-bip39 v1.1.0 16 | golang.org/x/crypto v0.18.0 17 | nhooyr.io/websocket v1.8.10 18 | ) 19 | 20 | require ( 21 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 24 | github.com/kr/pretty v0.3.1 // indirect 25 | github.com/pmezard/go-difflib v1.0.0 // indirect 26 | github.com/rogpeppe/go-internal v1.12.0 // indirect 27 | github.com/stretchr/objx v0.5.1 // indirect 28 | golang.org/x/sys v0.16.0 // indirect 29 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 30 | gopkg.in/yaml.v3 v3.0.1 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /hexutil/hexutil.go: -------------------------------------------------------------------------------- 1 | package hexutil 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "math/big" 7 | ) 8 | 9 | // BigIntToHex returns the hex representation of the given big integer. 10 | // The hex string is prefixed with "0x". Negative numbers are prefixed with 11 | // "-0x". 12 | func BigIntToHex(x *big.Int) string { 13 | if x == nil { 14 | return "0x0" 15 | } 16 | sign := x.Sign() 17 | switch { 18 | case sign == 0: 19 | return "0x0" 20 | case sign > 0: 21 | return "0x" + x.Text(16) 22 | default: 23 | return "-0x" + x.Text(16)[1:] 24 | } 25 | } 26 | 27 | // HexToBigInt returns the big integer representation of the given hex string. 28 | // The hex string may be prefixed with "0x". 29 | func HexToBigInt(h string) (*big.Int, error) { 30 | isNeg := len(h) > 1 && h[0] == '-' 31 | if isNeg { 32 | h = h[1:] 33 | } 34 | if Has0xPrefix(h) { 35 | h = h[2:] 36 | } 37 | x, ok := new(big.Int).SetString(h, 16) 38 | if !ok { 39 | return nil, fmt.Errorf("invalid hex string") 40 | } 41 | if isNeg { 42 | x.Neg(x) 43 | } 44 | return x, nil 45 | } 46 | 47 | func MustHexToBigInt(h string) *big.Int { 48 | x, err := HexToBigInt(h) 49 | if err != nil { 50 | panic(err) 51 | } 52 | return x 53 | } 54 | 55 | // BytesToHex returns the hex representation of the given bytes. The hex string 56 | // is always even-length and prefixed with "0x". 57 | func BytesToHex(b []byte) string { 58 | r := make([]byte, len(b)*2+2) 59 | copy(r, `0x`) 60 | hex.Encode(r[2:], b) 61 | return string(r) 62 | } 63 | 64 | // HexToBytes returns the bytes representation of the given hex string. 65 | // The number of hex digits must be even. The hex string may be prefixed with 66 | // "0x". 67 | func HexToBytes(h string) ([]byte, error) { 68 | if len(h) == 0 { 69 | return []byte{}, nil 70 | } 71 | if Has0xPrefix(h) { 72 | h = h[2:] 73 | } 74 | if len(h) == 1 && h[0] == '0' { 75 | return []byte{0}, nil 76 | } 77 | if len(h) == 0 { 78 | return []byte{}, nil 79 | } 80 | if len(h)%2 != 0 { 81 | return nil, fmt.Errorf("invalid hex string, length must be even") 82 | } 83 | return hex.DecodeString(h) 84 | } 85 | 86 | func MustHexToBytes(h string) []byte { 87 | b, err := HexToBytes(h) 88 | if err != nil { 89 | panic(err) 90 | } 91 | return b 92 | } 93 | 94 | // Has0xPrefix returns true if the given byte slice starts with "0x". 95 | func Has0xPrefix(h string) bool { 96 | return len(h) >= 2 && h[0] == '0' && (h[1] == 'x' || h[1] == 'X') 97 | } 98 | -------------------------------------------------------------------------------- /hexutil/hexutil_test.go: -------------------------------------------------------------------------------- 1 | package hexutil 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestBigIntToHex(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | input *big.Int 15 | expected string 16 | }{ 17 | {"nil input", nil, "0x0"}, 18 | {"zero value", big.NewInt(0), "0x0"}, 19 | {"positive value", big.NewInt(26), "0x1a"}, 20 | {"negative value", big.NewInt(-26), "-0x1a"}, 21 | } 22 | 23 | for _, tt := range tests { 24 | t.Run(tt.name, func(t *testing.T) { 25 | assert.Equal(t, tt.expected, BigIntToHex(tt.input)) 26 | }) 27 | } 28 | } 29 | 30 | func TestHexToBigInt(t *testing.T) { 31 | tests := []struct { 32 | name string 33 | input string 34 | expected *big.Int 35 | err error 36 | }{ 37 | {"zero", "0x0", big.NewInt(0), nil}, 38 | {"zero without prefix", "0", big.NewInt(0), nil}, 39 | {"valid positive hex", "0x1a", big.NewInt(26), nil}, 40 | {"valid positive hex without prefix", "1a", big.NewInt(26), nil}, 41 | {"valid negative hex", "-0x1a", big.NewInt(-26), nil}, 42 | {"valid negative hex without prefix", "-1a", big.NewInt(-26), nil}, 43 | {"valid positive single char hex", "0xa", big.NewInt(10), nil}, 44 | {"valid negative single char hex", "-0xa", big.NewInt(-10), nil}, 45 | {"empty string", "", nil, fmt.Errorf("invalid hex string")}, 46 | {"invalid hex", "0x1g", nil, fmt.Errorf("invalid hex string")}, 47 | } 48 | 49 | for _, tt := range tests { 50 | t.Run(tt.name, func(t *testing.T) { 51 | result, err := HexToBigInt(tt.input) 52 | assert.Equal(t, tt.err, err) 53 | assert.Equal(t, tt.expected, result) 54 | }) 55 | } 56 | } 57 | 58 | func TestBytesToHex(t *testing.T) { 59 | tests := []struct { 60 | name string 61 | input []byte 62 | expected string 63 | }{ 64 | {"empty bytes", []byte{}, "0x"}, 65 | {"non-empty bytes", []byte("abc"), "0x616263"}, 66 | {"bytes with zeros", []byte{0, 1, 2}, "0x000102"}, 67 | } 68 | 69 | for _, tt := range tests { 70 | t.Run(tt.name, func(t *testing.T) { 71 | assert.Equal(t, tt.expected, BytesToHex(tt.input)) 72 | }) 73 | } 74 | } 75 | 76 | func TestHexToBytes(t *testing.T) { 77 | tests := []struct { 78 | name string 79 | input string 80 | expected []byte 81 | err error 82 | }{ 83 | {"empty string", "", []byte{}, nil}, 84 | {"empty data", "0x", []byte{}, nil}, 85 | {"valid hex", "0x616263", []byte("abc"), nil}, 86 | {"valid hex without prefix", "616263", []byte("abc"), nil}, 87 | {"single zero", "0", []byte{0}, nil}, 88 | {"invalid hex", "0x1", nil, fmt.Errorf("invalid hex string, length must be even")}, 89 | } 90 | 91 | for _, tt := range tests { 92 | t.Run(tt.name, func(t *testing.T) { 93 | result, err := HexToBytes(tt.input) 94 | assert.Equal(t, tt.err, err) 95 | assert.Equal(t, tt.expected, result) 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /rpc/client.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/defiweb/go-eth/rpc/transport" 8 | "github.com/defiweb/go-eth/types" 9 | "github.com/defiweb/go-eth/wallet" 10 | ) 11 | 12 | // Client allows to interact with the Ethereum node. 13 | type Client struct { 14 | baseClient 15 | 16 | keys map[types.Address]wallet.Key 17 | defaultAddr *types.Address 18 | txModifiers []TXModifier 19 | } 20 | 21 | type ClientOptions func(c *Client) error 22 | 23 | // TXModifier allows to modify the transaction before it is signed or sent to 24 | // the node. 25 | type TXModifier interface { 26 | Modify(ctx context.Context, client RPC, tx *types.Transaction) error 27 | } 28 | 29 | type TXModifierFunc func(ctx context.Context, client RPC, tx *types.Transaction) error 30 | 31 | func (f TXModifierFunc) Modify(ctx context.Context, client RPC, tx *types.Transaction) error { 32 | return f(ctx, client, tx) 33 | } 34 | 35 | // WithTransport sets the transport for the client. 36 | func WithTransport(transport transport.Transport) ClientOptions { 37 | return func(c *Client) error { 38 | c.transport = transport 39 | return nil 40 | } 41 | } 42 | 43 | // WithKeys allows to set keys that will be used to sign data. 44 | // It allows to emulate the behavior of the RPC methods that require a key. 45 | // 46 | // The following methods are affected: 47 | // - Accounts - returns the addresses of the provided keys 48 | // - Sign - signs the data with the provided key 49 | // - SignTransaction - signs transaction with the provided key 50 | // - SendTransaction - signs transaction with the provided key and sends it 51 | // using SendRawTransaction 52 | func WithKeys(keys ...wallet.Key) ClientOptions { 53 | return func(c *Client) error { 54 | for _, k := range keys { 55 | c.keys[k.Address()] = k 56 | } 57 | return nil 58 | } 59 | } 60 | 61 | // WithDefaultAddress sets the call "from" address if it is not set in the 62 | // following methods: 63 | // - SignTransaction 64 | // - SendTransaction 65 | // - Call 66 | // - EstimateGas 67 | func WithDefaultAddress(addr types.Address) ClientOptions { 68 | return func(c *Client) error { 69 | c.defaultAddr = &addr 70 | return nil 71 | } 72 | } 73 | 74 | // WithTXModifiers allows to modify the transaction before it is signed and 75 | // sent to the node. 76 | // 77 | // Modifiers will be applied in the order they are provided. 78 | func WithTXModifiers(modifiers ...TXModifier) ClientOptions { 79 | return func(c *Client) error { 80 | c.txModifiers = append(c.txModifiers, modifiers...) 81 | return nil 82 | } 83 | } 84 | 85 | // NewClient creates a new RPC client. 86 | // The WithTransport option is required. 87 | func NewClient(opts ...ClientOptions) (*Client, error) { 88 | c := &Client{keys: make(map[types.Address]wallet.Key)} 89 | for _, opt := range opts { 90 | if err := opt(c); err != nil { 91 | return nil, err 92 | } 93 | } 94 | if c.transport == nil { 95 | return nil, fmt.Errorf("rpc client: transport is required") 96 | } 97 | return c, nil 98 | } 99 | 100 | // Accounts implements the RPC interface. 101 | func (c *Client) Accounts(ctx context.Context) ([]types.Address, error) { 102 | if len(c.keys) > 0 { 103 | var res []types.Address 104 | for _, key := range c.keys { 105 | res = append(res, key.Address()) 106 | } 107 | return res, nil 108 | } 109 | return c.baseClient.Accounts(ctx) 110 | } 111 | 112 | // Sign implements the RPC interface. 113 | func (c *Client) Sign(ctx context.Context, account types.Address, data []byte) (*types.Signature, error) { 114 | if len(c.keys) == 0 { 115 | return c.baseClient.Sign(ctx, account, data) 116 | } 117 | if key := c.findKey(&account); key != nil { 118 | return key.SignMessage(ctx, data) 119 | } 120 | return nil, fmt.Errorf("rpc client: no key found for address %s", account) 121 | } 122 | 123 | // SignTransaction implements the RPC interface. 124 | func (c *Client) SignTransaction(ctx context.Context, tx *types.Transaction) ([]byte, *types.Transaction, error) { 125 | tx, err := c.PrepareTransaction(ctx, tx) 126 | if err != nil { 127 | return nil, nil, err 128 | } 129 | if len(c.keys) == 0 { 130 | return c.baseClient.SignTransaction(ctx, tx) 131 | } 132 | if key := c.findKey(tx.Call.From); key != nil { 133 | if err := key.SignTransaction(ctx, tx); err != nil { 134 | return nil, nil, err 135 | } 136 | raw, err := tx.Raw() 137 | if err != nil { 138 | return nil, nil, err 139 | } 140 | return raw, tx, nil 141 | } 142 | return nil, nil, fmt.Errorf("rpc client: no key found for address %s", tx.Call.From) 143 | } 144 | 145 | // SendTransaction implements the RPC interface. 146 | func (c *Client) SendTransaction(ctx context.Context, tx *types.Transaction) (*types.Hash, *types.Transaction, error) { 147 | tx, err := c.PrepareTransaction(ctx, tx) 148 | if err != nil { 149 | return nil, nil, err 150 | } 151 | if len(c.keys) == 0 { 152 | return c.baseClient.SendTransaction(ctx, tx) 153 | } 154 | if key := c.findKey(tx.Call.From); key != nil { 155 | if err := key.SignTransaction(ctx, tx); err != nil { 156 | return nil, nil, err 157 | } 158 | raw, err := tx.Raw() 159 | if err != nil { 160 | return nil, nil, err 161 | } 162 | txHash, err := c.SendRawTransaction(ctx, raw) 163 | if err != nil { 164 | return nil, nil, err 165 | } 166 | return txHash, tx, nil 167 | } 168 | return nil, nil, fmt.Errorf("rpc client: no key found for address %s", tx.Call.From) 169 | } 170 | 171 | // PrepareTransaction prepares the transaction by applying transaction 172 | // modifiers and setting the default address if it is not set. 173 | // 174 | // A copy of the modified transaction is returned. 175 | func (c *Client) PrepareTransaction(ctx context.Context, tx *types.Transaction) (*types.Transaction, error) { 176 | if tx == nil { 177 | return nil, fmt.Errorf("rpc client: transaction is nil") 178 | } 179 | txCpy := tx.Copy() 180 | if txCpy.Call.From == nil && c.defaultAddr != nil { 181 | defaultAddr := *c.defaultAddr 182 | txCpy.Call.From = &defaultAddr 183 | } 184 | for _, modifier := range c.txModifiers { 185 | if err := modifier.Modify(ctx, c, txCpy); err != nil { 186 | return nil, err 187 | } 188 | } 189 | return txCpy, nil 190 | } 191 | 192 | // Call implements the RPC interface. 193 | func (c *Client) Call(ctx context.Context, call *types.Call, block types.BlockNumber) ([]byte, *types.Call, error) { 194 | if call == nil { 195 | return nil, nil, fmt.Errorf("rpc client: call is nil") 196 | } 197 | callCpy := call.Copy() 198 | if callCpy.From == nil && c.defaultAddr != nil { 199 | defaultAddr := *c.defaultAddr 200 | callCpy.From = &defaultAddr 201 | } 202 | return c.baseClient.Call(ctx, callCpy, block) 203 | } 204 | 205 | // EstimateGas implements the RPC interface. 206 | func (c *Client) EstimateGas(ctx context.Context, call *types.Call, block types.BlockNumber) (uint64, *types.Call, error) { 207 | if call == nil { 208 | return 0, nil, fmt.Errorf("rpc client: call is nil") 209 | } 210 | callCpy := call.Copy() 211 | if callCpy.From == nil && c.defaultAddr != nil { 212 | defaultAddr := *c.defaultAddr 213 | callCpy.From = &defaultAddr 214 | } 215 | return c.baseClient.EstimateGas(ctx, callCpy, block) 216 | } 217 | 218 | // findKey finds a key by address. 219 | func (c *Client) findKey(addr *types.Address) wallet.Key { 220 | if addr == nil { 221 | return nil 222 | } 223 | if key, ok := c.keys[*addr]; ok { 224 | return key 225 | } 226 | return nil 227 | } 228 | -------------------------------------------------------------------------------- /rpc/mocks_test.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/defiweb/go-eth/rpc/transport" 13 | "github.com/defiweb/go-eth/types" 14 | ) 15 | 16 | type roundTripFunc func(req *http.Request) (*http.Response, error) 17 | 18 | func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { 19 | return f(req) 20 | } 21 | 22 | type httpMock struct { 23 | *transport.HTTP 24 | 25 | Request *http.Request 26 | ResponseMock *http.Response 27 | } 28 | 29 | func newHTTPMock() *httpMock { 30 | h := &httpMock{} 31 | h.HTTP, _ = transport.NewHTTP(transport.HTTPOptions{ 32 | URL: "http://localhost", 33 | HTTPClient: &http.Client{ 34 | Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { 35 | h.Request = req 36 | return h.ResponseMock, nil 37 | }), 38 | }, 39 | }) 40 | return h 41 | } 42 | 43 | type streamMock struct { 44 | t *testing.T 45 | 46 | SubscribeMocks []subscribeMock 47 | UnsubscribeMocks []unsubscribeMock 48 | } 49 | 50 | type subscribeMock struct { 51 | ArgMethod string 52 | ArgParams []any 53 | RetCh chan json.RawMessage 54 | RetID string 55 | RetErr error 56 | } 57 | 58 | type unsubscribeMock struct { 59 | ArgID string 60 | ResultErr error 61 | } 62 | 63 | func newStreamMock(t *testing.T) *streamMock { 64 | return &streamMock{t: t} 65 | } 66 | 67 | func (s *streamMock) Call(_ context.Context, _ any, _ string, _ ...any) error { 68 | return errors.New("not implemented") 69 | } 70 | 71 | func (s *streamMock) Subscribe(_ context.Context, method string, args ...any) (ch chan json.RawMessage, id string, err error) { 72 | require.NotEmpty(s.t, s.SubscribeMocks) 73 | m := s.SubscribeMocks[0] 74 | s.SubscribeMocks = s.SubscribeMocks[1:] 75 | require.Equal(s.t, m.ArgMethod, method) 76 | require.Equal(s.t, len(m.ArgParams), len(args)) 77 | for i := range m.ArgParams { 78 | require.Equal(s.t, m.ArgParams[i], args[i]) 79 | } 80 | return m.RetCh, m.RetID, m.RetErr 81 | } 82 | 83 | func (s *streamMock) Unsubscribe(_ context.Context, id string) error { 84 | require.NotEmpty(s.t, s.UnsubscribeMocks) 85 | m := s.UnsubscribeMocks[0] 86 | s.UnsubscribeMocks = s.UnsubscribeMocks[1:] 87 | require.Equal(s.t, m.ArgID, id) 88 | return m.ResultErr 89 | } 90 | 91 | type keyMock struct { 92 | addressCallback func() types.Address 93 | signHashCallback func(hash types.Hash) (*types.Signature, error) 94 | signMessageCallback func(data []byte) (*types.Signature, error) 95 | signTransactionCallback func(tx *types.Transaction) error 96 | } 97 | 98 | func (k *keyMock) Address() types.Address { 99 | return k.addressCallback() 100 | } 101 | 102 | func (k *keyMock) SignHash(ctx context.Context, hash types.Hash) (*types.Signature, error) { 103 | return k.signHashCallback(hash) 104 | } 105 | 106 | func (k *keyMock) SignMessage(ctx context.Context, data []byte) (*types.Signature, error) { 107 | return k.signMessageCallback(data) 108 | } 109 | 110 | func (k *keyMock) SignTransaction(ctx context.Context, tx *types.Transaction) error { 111 | return k.signTransactionCallback(tx) 112 | } 113 | 114 | func (k *keyMock) VerifyHash(ctx context.Context, hash types.Hash, sig types.Signature) bool { 115 | return false 116 | } 117 | 118 | func (k keyMock) VerifyMessage(ctx context.Context, data []byte, sig types.Signature) bool { 119 | return false 120 | } 121 | -------------------------------------------------------------------------------- /rpc/transport/combined.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | ) 7 | 8 | // Combined is transport that uses separate transports for regular calls and 9 | // subscriptions. 10 | // 11 | // It is recommended by some RPC providers to use HTTP for regular calls and 12 | // WebSockets for subscriptions. 13 | type Combined struct { 14 | calls Transport 15 | subs SubscriptionTransport 16 | } 17 | 18 | // NewCombined creates a new Combined transport. 19 | func NewCombined(call Transport, subscriber SubscriptionTransport) *Combined { 20 | return &Combined{ 21 | calls: call, 22 | subs: subscriber, 23 | } 24 | } 25 | 26 | // Call implements the Transport interface. 27 | func (c *Combined) Call(ctx context.Context, result any, method string, args ...any) error { 28 | return c.calls.Call(ctx, result, method, args...) 29 | } 30 | 31 | // Subscribe implements the SubscriptionTransport interface. 32 | func (c *Combined) Subscribe(ctx context.Context, method string, args ...any) (ch chan json.RawMessage, id string, err error) { 33 | return c.subs.Subscribe(ctx, method, args...) 34 | } 35 | 36 | // Unsubscribe implements the SubscriptionTransport interface. 37 | func (c *Combined) Unsubscribe(ctx context.Context, id string) error { 38 | return c.subs.Unsubscribe(ctx, id) 39 | } 40 | -------------------------------------------------------------------------------- /rpc/transport/error.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/defiweb/go-eth/hexutil" 8 | ) 9 | 10 | const ( 11 | // Standard errors: 12 | ErrCodeUnauthorized = 1 13 | ErrCodeActionNotAllowed = 2 14 | ErrCodeExecutionError = 3 15 | ErrCodeParseError = -32700 16 | ErrCodeInvalidRequest = -32600 17 | ErrCodeMethodNotFound = -32601 18 | ErrCodeInvalidParams = -32602 19 | ErrCodeInternalError = -32603 20 | 21 | // Common non-standard errors: 22 | ErrCodeGeneral = -32000 23 | ErrCodeLimitExceeded = -32005 24 | 25 | // Erigon errors: 26 | ErigonErrCodeGeneral = -32000 27 | ErigonErrCodeNotFound = -32601 28 | ErigonErrCodeUnsupportedFork = -38005 29 | 30 | // Nethermind errors: 31 | NethermindErrCodeMethodNotSupported = -32004 32 | NethermindErrCodeLimitExceeded = -32005 33 | NethermindErrCodeTransactionRejected = -32010 34 | NethermindErrCodeExecutionError = -32015 35 | NethermindErrCodeTimeout = -32016 36 | NethermindErrCodeModuleTimeout = -32017 37 | NethermindErrCodeAccountLocked = -32020 38 | NethermindErrCodeUnknownBlockError = -39001 39 | 40 | // Infura errors: 41 | InfuraErrCodeInvalidInput = -32000 42 | InfuraErrCodeResourceNotFound = -32001 43 | InfuraErrCodeResourceUnavailable = -32002 44 | InfuraErrCodeTransactionRejected = -32003 45 | InfuraErrCodeMethodNotSupported = -32004 46 | InfuraErrCodeLimitExceeded = -32005 47 | InfuraErrCodeJSONRPCVersionNotSupported = -32006 48 | 49 | // Alchemy errors: 50 | AlchemyErrCodeLimitExceeded = 429 51 | 52 | // Blast errors: 53 | BlastErrCodeAuthenticationFailed = -32099 54 | BlastErrCodeCapacityExceeded = -32098 55 | BlastErrRateLimitReached = -32097 56 | ) 57 | 58 | type HTTPErrorCode interface { 59 | // HTTPErrorCode returns the HTTP status code. 60 | HTTPErrorCode() int 61 | } 62 | 63 | type RPCErrorCode interface { 64 | // RPCErrorCode returns the JSON-RPC error code. 65 | RPCErrorCode() int 66 | } 67 | 68 | type RPCErrorData interface { 69 | // RPCErrorData returns the JSON-RPC error data. 70 | RPCErrorData() any 71 | } 72 | 73 | // RPCError is an JSON-RPC error. 74 | type RPCError struct { 75 | Code int // Code is the JSON-RPC error code. 76 | Message string // Message is the error message. 77 | Data any // Data associated with the error. 78 | } 79 | 80 | // NewRPCError creates a new RPC error. 81 | // 82 | // If data is a hex-encoded string, it will be decoded. 83 | func NewRPCError(code int, message string, data any) *RPCError { 84 | if bin, ok := decodeHexData(data); ok { 85 | data = bin 86 | } 87 | return &RPCError{ 88 | Code: code, 89 | Message: message, 90 | Data: data, 91 | } 92 | } 93 | 94 | // Error implements the error interface. 95 | func (e *RPCError) Error() string { 96 | return fmt.Sprintf("RPC error: %d %s", e.Code, e.Message) 97 | } 98 | 99 | // RPCErrorCode implements the ErrorCode interface. 100 | func (e *RPCError) RPCErrorCode() int { 101 | return e.Code 102 | } 103 | 104 | // RPCErrorData implements the ErrorData interface. 105 | func (e *RPCError) RPCErrorData() any { 106 | return e.Data 107 | } 108 | 109 | // HTTPError is an HTTP error. 110 | type HTTPError struct { 111 | Code int // Code is the HTTP status code. 112 | Err error // Err is an optional underlying error. 113 | } 114 | 115 | // NewHTTPError creates a new HTTP error. 116 | func NewHTTPError(code int, err error) *HTTPError { 117 | return &HTTPError{ 118 | Code: code, 119 | Err: err, 120 | } 121 | } 122 | 123 | // Error implements the error interface. 124 | func (e *HTTPError) Error() string { 125 | if e.Err == nil { 126 | return fmt.Sprintf("HTTP error: %d %s", e.Code, http.StatusText(e.Code)) 127 | } 128 | return fmt.Sprintf("HTTP error: %d %s: %s", e.Code, http.StatusText(e.Code), e.Err) 129 | } 130 | 131 | // HTTPErrorCode implements the HTTPErrorCode interface. 132 | func (e *HTTPError) HTTPErrorCode() int { 133 | return e.Code 134 | } 135 | 136 | // decodeHexData decodes hex-encoded data if present. 137 | func decodeHexData(data any) (any, bool) { 138 | hex, ok := data.(string) 139 | if !ok { 140 | return nil, false 141 | } 142 | if !hexutil.Has0xPrefix(hex) { 143 | return nil, false 144 | } 145 | bin, err := hexutil.HexToBytes(hex) 146 | if err != nil { 147 | return nil, false 148 | } 149 | return bin, true 150 | } 151 | -------------------------------------------------------------------------------- /rpc/transport/error_test.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/defiweb/go-eth/hexutil" 9 | ) 10 | 11 | func TestNewRPCError(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | code int 15 | message string 16 | data any 17 | expected *RPCError 18 | }{ 19 | { 20 | name: "error with non-hex data", 21 | code: ErrCodeGeneral, 22 | message: "Unauthorized access", 23 | data: "some data", 24 | expected: &RPCError{ 25 | Code: ErrCodeGeneral, 26 | Message: "Unauthorized access", 27 | Data: "some data", 28 | }, 29 | }, 30 | { 31 | name: "error with hex data", 32 | code: ErrCodeGeneral, 33 | message: "Invalid request", 34 | data: "0x68656c6c6f", 35 | expected: &RPCError{ 36 | Code: ErrCodeGeneral, 37 | Message: "Invalid request", 38 | Data: hexutil.MustHexToBytes("0x68656c6c6f"), 39 | }, 40 | }, 41 | // Add more test cases as needed 42 | } 43 | 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | actual := NewRPCError(tt.code, tt.message, tt.data) 47 | assert.Equal(t, tt.expected, actual) 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /rpc/transport/http.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | "sync/atomic" 11 | ) 12 | 13 | // HTTP is a Transport implementation that uses the HTTP protocol. 14 | type HTTP struct { 15 | opts HTTPOptions 16 | id uint64 17 | } 18 | 19 | // HTTPOptions contains options for the HTTP transport. 20 | type HTTPOptions struct { 21 | // URL of the HTTP endpoint. 22 | URL string 23 | 24 | // HTTPClient is the HTTP client to use. If nil, http.DefaultClient is 25 | // used. 26 | HTTPClient *http.Client 27 | 28 | // HTTPHeader specifies the HTTP headers to send with each request. 29 | HTTPHeader http.Header 30 | } 31 | 32 | // NewHTTP creates a new HTTP instance. 33 | func NewHTTP(opts HTTPOptions) (*HTTP, error) { 34 | if opts.URL == "" { 35 | return nil, errors.New("URL cannot be empty") 36 | } 37 | if opts.HTTPClient == nil { 38 | opts.HTTPClient = http.DefaultClient 39 | } 40 | return &HTTP{opts: opts}, nil 41 | } 42 | 43 | // Call implements the Transport interface. 44 | func (h *HTTP) Call(ctx context.Context, result any, method string, args ...any) error { 45 | id := atomic.AddUint64(&h.id, 1) 46 | rpcReq, err := newRPCRequest(&id, method, args) 47 | if err != nil { 48 | return fmt.Errorf("failed to create RPC request: %w", err) 49 | } 50 | httpBody, err := json.Marshal(rpcReq) 51 | if err != nil { 52 | return fmt.Errorf("failed to marshal RPC request: %w", err) 53 | } 54 | httpReq, err := http.NewRequestWithContext(ctx, "POST", h.opts.URL, bytes.NewReader(httpBody)) 55 | if err != nil { 56 | return fmt.Errorf("failed to create HTTP request: %w", err) 57 | } 58 | httpReq.Header.Set("Content-Type", "application/json") 59 | for k, v := range h.opts.HTTPHeader { 60 | httpReq.Header[k] = v 61 | } 62 | httpRes, err := h.opts.HTTPClient.Do(httpReq) 63 | if err != nil { 64 | return fmt.Errorf("failed to send HTTP request: %w", err) 65 | } 66 | defer httpRes.Body.Close() 67 | rpcRes := &rpcResponse{} 68 | if err := json.NewDecoder(httpRes.Body).Decode(rpcRes); err != nil { 69 | // If the response is not a valid JSON-RPC response, return the HTTP 70 | // status code as the error code. 71 | return NewHTTPError(httpRes.StatusCode, nil) 72 | } 73 | if rpcRes.Error != nil { 74 | return NewRPCError( 75 | rpcRes.Error.Code, 76 | rpcRes.Error.Message, 77 | rpcRes.Error.Data, 78 | ) 79 | } 80 | if result == nil { 81 | return nil 82 | } 83 | if err := json.Unmarshal(rpcRes.Result, result); err != nil { 84 | return fmt.Errorf("failed to unmarshal RPC result: %w", err) 85 | } 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /rpc/transport/http_test.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | 14 | "github.com/defiweb/go-eth/types" 15 | ) 16 | 17 | type roundTripFunc func(req *http.Request) (*http.Response, error) 18 | 19 | func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { 20 | return f(req) 21 | } 22 | 23 | type httpMock struct { 24 | *HTTP 25 | Request *http.Request 26 | Response *http.Response 27 | } 28 | 29 | //nolint:funlen 30 | func TestHTTP(t *testing.T) { 31 | tests := []struct { 32 | asserts func(t *testing.T, h *httpMock) 33 | }{ 34 | // Simple request: 35 | { 36 | asserts: func(t *testing.T, h *httpMock) { 37 | h.Response = &http.Response{ 38 | StatusCode: http.StatusOK, 39 | Body: io.NopCloser(bytes.NewReader([]byte(`{"id":1, "jsonrpc":"2.0", "result":"0x1"}`))), 40 | } 41 | result := types.Number{} 42 | require.NoError(t, h.Call(context.Background(), &result, "eth_getBalance", "0x1111111111111111111111111111111111111111", "latest")) 43 | assert.Equal(t, h.Request.URL.String(), "http://localhost") 44 | assert.Equal(t, h.Request.Method, "POST") 45 | assert.Equal(t, h.Request.Header.Get("X-Test"), "test") 46 | assert.Equal(t, h.Request.Header.Get("Content-Type"), "application/json") 47 | requestBody, err := io.ReadAll(h.Request.Body) 48 | assert.NoError(t, err) 49 | assert.JSONEq(t, `{"id":1, "jsonrpc":"2.0", "method":"eth_getBalance", "params":["0x1111111111111111111111111111111111111111", "latest"]}`, string(requestBody)) 50 | assert.Equal(t, result.Big().String(), "1") 51 | }, 52 | }, 53 | // ID must increment: 54 | { 55 | asserts: func(t *testing.T, h *httpMock) { 56 | // First request: 57 | h.Response = &http.Response{ 58 | StatusCode: http.StatusOK, 59 | Body: io.NopCloser(bytes.NewReader([]byte(`{"id":1, "jsonrpc":"2.0", "result":"0x1"}`))), 60 | } 61 | require.NoError(t, h.Call(context.Background(), nil, "eth_a")) 62 | requestBody, err := io.ReadAll(h.Request.Body) 63 | assert.NoError(t, err) 64 | assert.JSONEq(t, `{"id":1, "jsonrpc":"2.0", "method":"eth_a", "params":[]}`, string(requestBody)) 65 | 66 | // Second request: 67 | h.Response = &http.Response{ 68 | StatusCode: http.StatusOK, 69 | Body: io.NopCloser(bytes.NewReader([]byte(`{"id":2, "jsonrpc":"2.0", "result":"0x2"}`))), 70 | } 71 | require.NoError(t, h.Call(context.Background(), nil, "eth_b")) 72 | requestBody, err = io.ReadAll(h.Request.Body) 73 | assert.NoError(t, err) 74 | assert.JSONEq(t, `{"id":2, "jsonrpc":"2.0", "method":"eth_b", "params":[]}`, string(requestBody)) 75 | }, 76 | }, 77 | // Error response: 78 | { 79 | asserts: func(t *testing.T, h *httpMock) { 80 | h.Response = &http.Response{ 81 | StatusCode: http.StatusOK, 82 | Body: io.NopCloser(bytes.NewReader([]byte(`{"id":1, "jsonrpc":"2.0", "error":{"code":-32601, "message":"Method not found"}}`))), 83 | } 84 | result := types.Number{} 85 | err := h.Call(context.Background(), &result, "eth_a") 86 | assert.Error(t, err) 87 | assert.Equal(t, "RPC error: -32601 Method not found", err.Error()) 88 | }, 89 | }, 90 | // Error response with non-200 status code: 91 | { 92 | asserts: func(t *testing.T, h *httpMock) { 93 | h.Response = &http.Response{ 94 | StatusCode: http.StatusTooManyRequests, 95 | Body: io.NopCloser(bytes.NewReader([]byte(`{"id":1, "jsonrpc":"2.0", "error":{"code":-32005, "message":"Limit exceeded"}}`))), 96 | } 97 | result := types.Number{} 98 | err := h.Call(context.Background(), &result, "eth_a") 99 | assert.Error(t, err) 100 | assert.Equal(t, "RPC error: -32005 Limit exceeded", err.Error()) 101 | }, 102 | }, 103 | // Error response with non-200 status code and empty body: 104 | { 105 | asserts: func(t *testing.T, h *httpMock) { 106 | h.Response = &http.Response{ 107 | StatusCode: http.StatusTooManyRequests, 108 | Body: io.NopCloser(bytes.NewReader([]byte(``))), 109 | } 110 | result := types.Number{} 111 | err := h.Call(context.Background(), &result, "eth_a") 112 | assert.Error(t, err) 113 | assert.Equal(t, "HTTP error: 429 Too Many Requests", err.Error()) 114 | }, 115 | }, 116 | // Invalid response: 117 | { 118 | asserts: func(t *testing.T, h *httpMock) { 119 | h.Response = &http.Response{ 120 | StatusCode: http.StatusOK, 121 | Body: io.NopCloser(bytes.NewReader([]byte(`{`))), 122 | } 123 | result := types.Number{} 124 | err := h.Call(context.Background(), &result, "eth_a") 125 | assert.Error(t, err) 126 | }, 127 | }, 128 | } 129 | for n, tt := range tests { 130 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 131 | h := &httpMock{} 132 | h.HTTP, _ = NewHTTP(HTTPOptions{ 133 | URL: "http://localhost", 134 | HTTPHeader: http.Header{ 135 | "X-Test": []string{"test"}, 136 | }, 137 | HTTPClient: &http.Client{ 138 | Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { 139 | h.Request = req 140 | return h.Response, nil 141 | }), 142 | }, 143 | }) 144 | tt.asserts(t, h) 145 | }) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /rpc/transport/ipc.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net" 10 | "time" 11 | ) 12 | 13 | // IPC is a Transport implementation that uses the IPC protocol. 14 | type IPC struct { 15 | *stream 16 | conn net.Conn 17 | } 18 | 19 | // IPCOptions contains options for the IPC transport. 20 | type IPCOptions struct { 21 | // Context used to close the connection. 22 | Context context.Context 23 | 24 | // Path is the path to the IPC socket. 25 | Path string 26 | 27 | // Timeout is the timeout for the IPC requests. Default is 60s. 28 | Timout time.Duration 29 | 30 | // ErrorCh is an optional channel used to report errors. 31 | ErrorCh chan error 32 | } 33 | 34 | // NewIPC creates a new IPC instance. 35 | func NewIPC(opts IPCOptions) (*IPC, error) { 36 | var d net.Dialer 37 | conn, err := d.DialContext(opts.Context, "unix", opts.Path) 38 | if err != nil { 39 | return nil, fmt.Errorf("failed to dial IPC: %w", err) 40 | } 41 | if opts.Context == nil { 42 | return nil, errors.New("context cannot be nil") 43 | } 44 | if opts.Timout == 0 { 45 | opts.Timout = 60 * time.Second 46 | } 47 | i := &IPC{ 48 | stream: &stream{ 49 | ctx: opts.Context, 50 | errCh: opts.ErrorCh, 51 | timeout: opts.Timout, 52 | }, 53 | conn: conn, 54 | } 55 | i.stream.initStream() 56 | go i.readerRoutine() 57 | go i.writerRoutine() 58 | return i, nil 59 | } 60 | 61 | func (i *IPC) readerRoutine() { 62 | dec := json.NewDecoder(i.conn) 63 | for { 64 | var res rpcResponse 65 | if err := dec.Decode(&res); err != nil { 66 | if errors.Is(err, context.Canceled) { 67 | return 68 | } 69 | if errors.Is(err, io.EOF) { 70 | return 71 | } 72 | i.errCh <- err 73 | } 74 | i.readerCh <- res 75 | } 76 | } 77 | 78 | func (i *IPC) writerRoutine() { 79 | enc := json.NewEncoder(i.conn) 80 | for { 81 | select { 82 | case <-i.ctx.Done(): 83 | return 84 | case req := <-i.stream.writerCh: 85 | if err := enc.Encode(req); err != nil { 86 | if errors.Is(err, context.Canceled) { 87 | return 88 | } 89 | if errors.Is(err, io.EOF) { 90 | return 91 | } 92 | i.stream.errCh <- err 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /rpc/transport/retry.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "math" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | var ErrNotSubscriptionTransport = errors.New("transport does not implement SubscriptionTransport") 13 | 14 | var ( 15 | // RetryOnAnyError retries on any error except for the following: 16 | // 3: Execution error. 17 | // -32700: Parse error. 18 | // -32600: Invalid request. 19 | // -32601: Method not found. 20 | // -32602: Invalid params. 21 | // -32000: If error message starts with "execution reverted". 22 | RetryOnAnyError = func(err error) bool { 23 | // List of errors that should not be retried: 24 | switch errorCode(err) { 25 | case ErrCodeExecutionError: 26 | return false 27 | case ErrCodeParseError: 28 | return false 29 | case ErrCodeInvalidRequest: 30 | return false 31 | case ErrCodeMethodNotFound: 32 | return false 33 | case ErrCodeInvalidParams: 34 | return false 35 | case ErrCodeGeneral: 36 | rpcErr := &RPCError{} 37 | if errors.As(err, &rpcErr) { 38 | if strings.HasPrefix(rpcErr.Message, "execution reverted") { 39 | return false 40 | } 41 | } 42 | } 43 | 44 | // Retry on all other errors: 45 | return err != nil 46 | } 47 | 48 | // RetryOnLimitExceeded retries on the following errors: 49 | // -32005: Limit exceeded. 50 | // -32097: Rate limit reached (Blast). 51 | // 429: Too many requests 52 | RetryOnLimitExceeded = func(err error) bool { 53 | switch errorCode(err) { 54 | case ErrCodeLimitExceeded: 55 | return true 56 | case BlastErrRateLimitReached: 57 | return true 58 | case AlchemyErrCodeLimitExceeded: 59 | return true 60 | } 61 | return false 62 | } 63 | ) 64 | 65 | // ExponentialBackoffOptions contains options for the ExponentialBackoff function. 66 | type ExponentialBackoffOptions struct { 67 | // BaseDelay is the base delay before the first retry. 68 | BaseDelay time.Duration 69 | 70 | // MaxDelay is the maximum delay between retries. 71 | MaxDelay time.Duration 72 | 73 | // ExponentialFactor is the exponential factor to use for calculating the delay. 74 | // The delay is calculated as BaseDelay * ExponentialFactor ^ retryCount. 75 | ExponentialFactor float64 76 | } 77 | 78 | var ( 79 | // LinearBackoff returns a BackoffFunc that returns a constant delay. 80 | LinearBackoff = func(delay time.Duration) func(int) time.Duration { 81 | return func(_ int) time.Duration { 82 | return delay 83 | } 84 | } 85 | 86 | // ExponentialBackoff returns a BackoffFunc that returns an exponential delay. 87 | // The delay is calculated as BaseDelay * ExponentialFactor ^ retryCount. 88 | ExponentialBackoff = func(opts ExponentialBackoffOptions) func(int) time.Duration { 89 | return func(retryCount int) time.Duration { 90 | d := time.Duration(float64(opts.BaseDelay) * math.Pow(opts.ExponentialFactor, float64(retryCount))) 91 | if d > opts.MaxDelay { 92 | return opts.MaxDelay 93 | } 94 | return d 95 | } 96 | } 97 | ) 98 | 99 | // Retry is a wrapper around another transport that retries requests. 100 | type Retry struct { 101 | opts RetryOptions 102 | } 103 | 104 | // RetryOptions contains options for the Retry transport. 105 | type RetryOptions struct { 106 | // Transport is the underlying transport to use. 107 | Transport Transport 108 | 109 | // RetryFunc is a function that returns true if the request should be 110 | // retried. The RetryOnAnyError and RetryOnLimitExceeded functions can be 111 | // used or a custom function can be provided. 112 | RetryFunc func(error) bool 113 | 114 | // BackoffFunc is a function that returns the delay before the next retry. 115 | // It takes the current retry count as an argument. 116 | BackoffFunc func(int) time.Duration 117 | 118 | // MaxRetries is the maximum number of retries. If negative, there is no limit. 119 | MaxRetries int 120 | } 121 | 122 | // NewRetry creates a new Retry instance. 123 | func NewRetry(opts RetryOptions) (*Retry, error) { 124 | if opts.Transport == nil { 125 | return nil, errors.New("transport cannot be nil") 126 | } 127 | if opts.RetryFunc == nil { 128 | return nil, errors.New("retry function cannot be nil") 129 | } 130 | if opts.BackoffFunc == nil { 131 | return nil, errors.New("backoff function cannot be nil") 132 | } 133 | if opts.MaxRetries == 0 { 134 | return nil, errors.New("max retries cannot be zero") 135 | } 136 | return &Retry{opts: opts}, nil 137 | } 138 | 139 | // Call implements the Transport interface. 140 | func (c *Retry) Call(ctx context.Context, result any, method string, args ...any) (err error) { 141 | var i int 142 | for { 143 | err = c.opts.Transport.Call(ctx, result, method, args...) 144 | if !c.opts.RetryFunc(err) { 145 | return err 146 | } 147 | if c.opts.MaxRetries >= 0 && i >= c.opts.MaxRetries { 148 | break 149 | } 150 | select { 151 | case <-ctx.Done(): 152 | return ctx.Err() 153 | case <-time.After(c.opts.BackoffFunc(i)): 154 | } 155 | i++ 156 | } 157 | return err 158 | } 159 | 160 | // Subscribe implements the SubscriptionTransport interface. 161 | func (c *Retry) Subscribe(ctx context.Context, method string, args ...any) (ch chan json.RawMessage, id string, err error) { 162 | if s, ok := c.opts.Transport.(SubscriptionTransport); ok { 163 | var i int 164 | for { 165 | ch, id, err = s.Subscribe(ctx, method, args...) 166 | if !c.opts.RetryFunc(err) { 167 | return ch, id, err 168 | } 169 | if c.opts.MaxRetries >= 0 && i >= c.opts.MaxRetries { 170 | break 171 | } 172 | select { 173 | case <-ctx.Done(): 174 | return nil, "", ctx.Err() 175 | case <-time.After(c.opts.BackoffFunc(i)): 176 | } 177 | i++ 178 | } 179 | return nil, "", err 180 | } 181 | return nil, "", ErrNotSubscriptionTransport 182 | } 183 | 184 | // Unsubscribe implements the SubscriptionTransport interface. 185 | func (c *Retry) Unsubscribe(ctx context.Context, id string) (err error) { 186 | if s, ok := c.opts.Transport.(SubscriptionTransport); ok { 187 | var i int 188 | for { 189 | err = s.Unsubscribe(ctx, id) 190 | if !c.opts.RetryFunc(err) { 191 | return err 192 | } 193 | if c.opts.MaxRetries >= 0 && i >= c.opts.MaxRetries { 194 | break 195 | } 196 | select { 197 | case <-ctx.Done(): 198 | return ctx.Err() 199 | case <-time.After(c.opts.BackoffFunc(i)): 200 | } 201 | i++ 202 | } 203 | return err 204 | } 205 | return ErrNotSubscriptionTransport 206 | } 207 | 208 | // errorCode returns either the JSON-RPC error code or HTTP status code. 209 | // If there is no error or error code is not available, it returns 0. 210 | func errorCode(err error) int { 211 | var rpcErr RPCErrorCode 212 | if errors.As(err, &rpcErr) { 213 | return rpcErr.RPCErrorCode() 214 | } 215 | var httpErr HTTPErrorCode 216 | if errors.As(err, &httpErr) { 217 | return httpErr.HTTPErrorCode() 218 | } 219 | return 0 220 | } 221 | -------------------------------------------------------------------------------- /rpc/transport/rpc.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/defiweb/go-eth/types" 7 | ) 8 | 9 | // rpcRequest is the JSON-RPC request object. 10 | type rpcRequest struct { 11 | JSONRPC string `json:"jsonrpc"` 12 | ID *uint64 `json:"id"` 13 | Method string `json:"method"` 14 | Params json.RawMessage `json:"params"` 15 | } 16 | 17 | // rpcResponse is the JSON-RPC response object. 18 | type rpcResponse struct { 19 | // Common fields: 20 | JSONRPC string `json:"jsonrpc"` 21 | ID *uint64 `json:"id"` 22 | 23 | // Call response: 24 | Result json.RawMessage `json:"result,omitempty"` 25 | Error *rpcError `json:"error,omitempty"` 26 | 27 | // Notification response: 28 | Method string `json:"method,omitempty"` 29 | Params json.RawMessage `json:"params,omitempty"` 30 | } 31 | 32 | // rpcSubscription is the JSON-RPC subscription object. 33 | type rpcSubscription struct { 34 | Subscription types.Number `json:"subscription"` 35 | Result json.RawMessage `json:"result"` 36 | } 37 | 38 | // rpcError is the JSON-RPC error object. 39 | type rpcError struct { 40 | Code int `json:"code"` 41 | Message string `json:"message"` 42 | Data any `json:"data,omitempty"` 43 | } 44 | 45 | // newRPCRequest creates a new JSON-RPC request object. 46 | func newRPCRequest(id *uint64, method string, params []any) (rpcRequest, error) { 47 | rpcReq := rpcRequest{ 48 | JSONRPC: "2.0", 49 | ID: id, 50 | Method: method, 51 | Params: json.RawMessage("[]"), 52 | } 53 | if len(params) > 0 { 54 | params, err := json.Marshal(params) 55 | if err != nil { 56 | return rpcRequest{}, err 57 | } 58 | rpcReq.Params = params 59 | } 60 | return rpcReq, nil 61 | } 62 | -------------------------------------------------------------------------------- /rpc/transport/stream.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | 12 | "github.com/defiweb/go-eth/types" 13 | ) 14 | 15 | // stream is a helper for handling JSON-RPC streams. 16 | type stream struct { 17 | mu sync.RWMutex 18 | ctx context.Context 19 | 20 | writerCh chan rpcRequest // Channel for sending requests used by structs that embed stream. 21 | readerCh chan rpcResponse // Channel for receiving responses used by structs that embed stream. 22 | errCh chan error // Channel to which errors are sent. 23 | timeout time.Duration // Timeout for requests. 24 | onClose func() // Callback that is called when the stream is closed. 25 | 26 | // State fields. Should not be accessed by structs that embed stream. 27 | id uint64 // Request ID counter. 28 | calls map[uint64]chan rpcResponse // Map of request IDs to channels. 29 | subs map[string]chan json.RawMessage // Map of subscription IDs to channels. 30 | } 31 | 32 | // initStream initializes the stream struct with default values and starts 33 | // goroutines. 34 | func (s *stream) initStream() *stream { 35 | s.writerCh = make(chan rpcRequest) 36 | s.readerCh = make(chan rpcResponse) 37 | s.calls = make(map[uint64]chan rpcResponse) 38 | s.subs = make(map[string]chan json.RawMessage) 39 | go s.streamRoutine() 40 | go s.contextHandlerRoutine() 41 | return s 42 | } 43 | 44 | // Call implements the Transport interface. 45 | func (s *stream) Call(ctx context.Context, result any, method string, args ...any) error { 46 | ctx, ctxCancel := context.WithTimeout(ctx, s.timeout) 47 | defer ctxCancel() 48 | 49 | // Prepare the RPC request. 50 | id := atomic.AddUint64(&s.id, 1) 51 | req, err := newRPCRequest(&id, method, args) 52 | if err != nil { 53 | return fmt.Errorf("failed to create RPC request: %w", err) 54 | } 55 | 56 | // Prepare the channel for the response. 57 | ch := make(chan rpcResponse) 58 | s.addCallCh(id, ch) 59 | defer s.delCallCh(id) 60 | 61 | // Send the request. 62 | s.writerCh <- req 63 | 64 | // Wait for the response. 65 | // The response is handled by the streamRoutine. It will send the response 66 | // to the ch channel. 67 | select { 68 | case res := <-ch: 69 | if res.Error != nil { 70 | return NewRPCError( 71 | res.Error.Code, 72 | res.Error.Message, 73 | res.Error.Data, 74 | ) 75 | } 76 | if result != nil { 77 | if err := json.Unmarshal(res.Result, result); err != nil { 78 | return fmt.Errorf("failed to unmarshal RPC result: %w", err) 79 | } 80 | } 81 | case <-ctx.Done(): 82 | return ctx.Err() 83 | } 84 | return nil 85 | } 86 | 87 | // Subscribe implements the SubscriptionTransport interface. 88 | func (s *stream) Subscribe(ctx context.Context, method string, args ...any) (chan json.RawMessage, string, error) { 89 | rawID := types.Number{} 90 | params := make([]any, 0, 2) 91 | params = append(params, method) 92 | if len(args) > 0 { 93 | params = append(params, args...) 94 | } 95 | if err := s.Call(ctx, &rawID, "eth_subscribe", params...); err != nil { 96 | return nil, "", err 97 | } 98 | id := rawID.String() 99 | ch := make(chan json.RawMessage) 100 | s.addSubCh(id, ch) 101 | return ch, id, nil 102 | } 103 | 104 | // Unsubscribe implements the SubscriptionTransport interface. 105 | func (s *stream) Unsubscribe(ctx context.Context, id string) error { 106 | if !s.delSubCh(id) { 107 | return errors.New("unknown subscription") 108 | } 109 | num, err := types.NumberFromHex(id) 110 | if err != nil { 111 | return fmt.Errorf("invalid subscription id: %w", err) 112 | } 113 | return s.Call(ctx, nil, "eth_unsubscribe", num) 114 | } 115 | 116 | // readerRoutine reads messages from the stream connection and dispatches 117 | // them to the appropriate channel. 118 | func (s *stream) streamRoutine() { 119 | for { 120 | res, ok := <-s.readerCh 121 | if !ok { 122 | return 123 | } 124 | switch { 125 | case res.ID == nil: 126 | // If the ID is nil, it is a subscription notification. 127 | sub := &rpcSubscription{} 128 | if err := json.Unmarshal(res.Params, sub); err != nil { 129 | if s.errCh != nil { 130 | s.errCh <- fmt.Errorf("failed to unmarshal subscription: %w", err) 131 | } 132 | continue 133 | } 134 | s.subChSend(sub.Subscription.String(), sub.Result) 135 | default: 136 | // If the ID is not nil, it is a response to a request. 137 | s.callChSend(*res.ID, res) 138 | } 139 | } 140 | } 141 | 142 | // contextHandlerRoutine closes the connection when the context is canceled. 143 | func (s *stream) contextHandlerRoutine() { 144 | <-s.ctx.Done() 145 | s.mu.Lock() 146 | defer s.mu.Unlock() 147 | for _, ch := range s.calls { 148 | close(ch) 149 | } 150 | for _, ch := range s.subs { 151 | close(ch) 152 | } 153 | s.calls = nil 154 | s.subs = nil 155 | if s.onClose != nil { 156 | s.onClose() 157 | } 158 | } 159 | 160 | // addCallCh adds a channel to the calls map. Incoming response that match the 161 | // id will be sent to the given channel. Because message ids are unique, the 162 | // channel must be deleted after the response is received using delCallCh. 163 | func (s *stream) addCallCh(id uint64, ch chan rpcResponse) { 164 | s.mu.Lock() 165 | defer s.mu.Unlock() 166 | s.calls[id] = ch 167 | } 168 | 169 | // addSubCh adds a channel to the subs map. Incoming subscription notifications 170 | // that match the id will be sent to the given channel. 171 | func (s *stream) addSubCh(id string, ch chan json.RawMessage) { 172 | s.mu.Lock() 173 | defer s.mu.Unlock() 174 | s.subs[id] = ch 175 | } 176 | 177 | // delCallCh deletes a channel from the calls map. 178 | func (s *stream) delCallCh(id uint64) bool { 179 | s.mu.Lock() 180 | defer s.mu.Unlock() 181 | if ch, ok := s.calls[id]; ok { 182 | close(ch) 183 | delete(s.calls, id) 184 | return true 185 | } 186 | return false 187 | } 188 | 189 | // delSubCh deletes a channel from the subs map. 190 | func (s *stream) delSubCh(id string) bool { 191 | s.mu.Lock() 192 | defer s.mu.Unlock() 193 | if ch, ok := s.subs[id]; ok { 194 | close(ch) 195 | delete(s.subs, id) 196 | return true 197 | } 198 | return false 199 | } 200 | 201 | // callChSend sends a response to the channel that matches the id. 202 | func (s *stream) callChSend(id uint64, res rpcResponse) { 203 | s.mu.RLock() 204 | defer s.mu.RUnlock() 205 | if ch := s.calls[id]; ch != nil { 206 | ch <- res 207 | } 208 | } 209 | 210 | // subChSend sends a subscription notification to the channel that matches the 211 | // id. 212 | func (s *stream) subChSend(id string, res json.RawMessage) { 213 | s.mu.RLock() 214 | defer s.mu.RUnlock() 215 | if ch := s.subs[id]; ch != nil { 216 | ch <- res 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /rpc/transport/transport.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | netURL "net/url" 8 | ) 9 | 10 | // Transport handles the transport layer of the JSON-RPC protocol. 11 | type Transport interface { 12 | // Call performs a JSON-RPC call. 13 | Call(ctx context.Context, result any, method string, args ...any) error 14 | } 15 | 16 | // SubscriptionTransport is transport that supports subscriptions. 17 | type SubscriptionTransport interface { 18 | Transport 19 | 20 | // Subscribe starts a new subscription. It returns a channel that receives 21 | // subscription messages and a subscription ID. 22 | Subscribe(ctx context.Context, method string, args ...any) (ch chan json.RawMessage, id string, err error) 23 | 24 | // Unsubscribe cancels a subscription. The channel returned by Subscribe 25 | // will be closed. 26 | Unsubscribe(ctx context.Context, id string) error 27 | } 28 | 29 | // New returns a new Transport instance based on the URL scheme. 30 | // Supported schemes are: http, https, ws, wss. 31 | // If scheme is empty, it will use IPC. 32 | // 33 | // The context is used to close the underlying connection when the transport 34 | // uses a websocket or IPC. 35 | func New(ctx context.Context, rpcURL string) (Transport, error) { 36 | url, err := netURL.Parse(rpcURL) 37 | if err != nil { 38 | return nil, err 39 | } 40 | switch url.Scheme { 41 | case "http", "https": 42 | return NewHTTP(HTTPOptions{URL: rpcURL}) 43 | case "ws", "wss": 44 | return NewWebsocket(WebsocketOptions{Context: ctx, URL: rpcURL}) 45 | case "": 46 | return NewIPC(IPCOptions{Context: ctx, Path: rpcURL}) 47 | default: 48 | return nil, fmt.Errorf("unsupported scheme: %s", url.Scheme) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /rpc/transport/websocket.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | "nhooyr.io/websocket" 11 | "nhooyr.io/websocket/wsjson" 12 | ) 13 | 14 | // Websocket is a Transport implementation that uses the websocket 15 | // protocol. 16 | type Websocket struct { 17 | *stream 18 | conn *websocket.Conn 19 | } 20 | 21 | // WebsocketOptions contains options for the websocket transport. 22 | type WebsocketOptions struct { 23 | // Context used to close the connection. 24 | Context context.Context 25 | 26 | // URL of the websocket endpoint. 27 | URL string 28 | 29 | // HTTPClient is the HTTP client to use. If nil, http.DefaultClient is 30 | // used. 31 | HTTPClient *http.Client 32 | 33 | // HTTPHeader specifies the HTTP headers to be included in the 34 | // websocket handshake request. 35 | HTTPHeader http.Header 36 | 37 | // Timeout is the timeout for the websocket requests. Default is 60s. 38 | Timout time.Duration 39 | 40 | // ErrorCh is an optional channel used to report errors. 41 | ErrorCh chan error 42 | } 43 | 44 | // NewWebsocket creates a new Websocket instance. 45 | func NewWebsocket(opts WebsocketOptions) (*Websocket, error) { 46 | if opts.URL == "" { 47 | return nil, errors.New("URL cannot be empty") 48 | } 49 | if opts.Context == nil { 50 | return nil, errors.New("context cannot be nil") 51 | } 52 | if opts.Timout == 0 { 53 | opts.Timout = 60 * time.Second 54 | } 55 | conn, _, err := websocket.Dial(opts.Context, opts.URL, &websocket.DialOptions{ //nolint:bodyclose 56 | HTTPClient: opts.HTTPClient, 57 | HTTPHeader: opts.HTTPHeader, 58 | }) 59 | if err != nil { 60 | return nil, fmt.Errorf("failed to dial websocket: %w", err) 61 | } 62 | i := &Websocket{ 63 | stream: &stream{ 64 | ctx: opts.Context, 65 | errCh: opts.ErrorCh, 66 | timeout: opts.Timout, 67 | }, 68 | conn: conn, 69 | } 70 | i.onClose = i.close 71 | i.stream.initStream() 72 | go i.readerRoutine() 73 | go i.writerRoutine() 74 | return i, nil 75 | } 76 | 77 | func (ws *Websocket) readerRoutine() { 78 | // The background context is used here because closing context will 79 | // cause the nhooyr.io/websocket package to close a connection with 80 | // a close code of 1008 (policy violation) which is not what we want. 81 | ctx := context.Background() 82 | for { 83 | res := rpcResponse{} 84 | if err := wsjson.Read(ctx, ws.conn, &res); err != nil { 85 | if ws.ctx.Err() != nil || errors.As(err, &websocket.CloseError{}) { 86 | return 87 | } 88 | if ws.errCh != nil { 89 | ws.errCh <- fmt.Errorf("websocket reading error: %w", err) 90 | } 91 | continue 92 | } 93 | ws.readerCh <- res 94 | } 95 | } 96 | 97 | func (ws *Websocket) writerRoutine() { 98 | for { 99 | select { 100 | case <-ws.ctx.Done(): 101 | return 102 | case req := <-ws.writerCh: 103 | if err := wsjson.Write(ws.ctx, ws.conn, req); err != nil { 104 | if ws.errCh != nil { 105 | ws.errCh <- fmt.Errorf("websocket writing error: %w", err) 106 | } 107 | continue 108 | } 109 | } 110 | } 111 | } 112 | 113 | func (ws *Websocket) close() { 114 | err := ws.conn.Close(websocket.StatusNormalClosure, "") 115 | if err != nil && ws.errCh != nil { 116 | ws.errCh <- fmt.Errorf("websocket closing error: %w", err) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /rpc/transport/websocket_test.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "net/http" 10 | "sync" 11 | "testing" 12 | "time" 13 | 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | "nhooyr.io/websocket" 17 | "nhooyr.io/websocket/wsjson" 18 | 19 | "github.com/defiweb/go-eth/types" 20 | ) 21 | 22 | //nolint:funlen 23 | func TestWebsocket(t *testing.T) { 24 | tests := []struct { 25 | asserts func(t *testing.T, ws *Websocket, reqCh, resCh chan string) 26 | }{ 27 | // Simple case: 28 | { 29 | asserts: func(t *testing.T, ws *Websocket, reqCh, resCh chan string) { 30 | go func() { 31 | assert.JSONEq(t, 32 | `{"id":1, "jsonrpc":"2.0", "method":"eth_getBalance", "params":["0x1111111111111111111111111111111111111111", "latest"]}`, 33 | <-reqCh, 34 | ) 35 | resCh <- `{"id": 1, "result": "0x1"}` 36 | }() 37 | 38 | ctx := context.Background() 39 | res := &types.Number{} 40 | err := ws.Call( 41 | ctx, 42 | res, 43 | "eth_getBalance", 44 | types.MustAddressFromHex("0x1111111111111111111111111111111111111111"), 45 | types.LatestBlockNumber, 46 | ) 47 | 48 | require.NoError(t, err) 49 | assert.Equal(t, uint64(1), res.Big().Uint64()) 50 | }, 51 | }, 52 | // Error response: 53 | { 54 | asserts: func(t *testing.T, ws *Websocket, reqCh, resCh chan string) { 55 | go func() { 56 | <-reqCh 57 | resCh <- `{"id": 1, "error": {"code": 1, "message": "error"}}` 58 | }() 59 | 60 | ctx := context.Background() 61 | res := &types.Number{} 62 | err := ws.Call(ctx, res, "eth_call") 63 | assert.Error(t, err) 64 | }, 65 | }, 66 | // Timeout: 67 | { 68 | asserts: func(t *testing.T, ws *Websocket, reqCh, resCh chan string) { 69 | go func() { 70 | <-reqCh 71 | }() 72 | 73 | ctx := context.Background() 74 | res := &types.Number{} 75 | err := ws.Call( 76 | ctx, 77 | res, 78 | "eth_call", 79 | ) 80 | assert.Error(t, err) 81 | }, 82 | }, 83 | // Subscription: 84 | { 85 | asserts: func(t *testing.T, ws *Websocket, reqCh, resCh chan string) { 86 | go func() { 87 | assert.JSONEq(t, 88 | `{"id":1, "jsonrpc":"2.0", "method":"eth_subscribe", "params":["eth_sub", "foo", "bar"]}`, 89 | <-reqCh, 90 | ) 91 | resCh <- `{"id":1, "result":"0xff"}` 92 | }() 93 | 94 | ctx := context.Background() 95 | ch, id, err := ws.Subscribe(ctx, "eth_sub", "foo", "bar") 96 | require.NoError(t, err) 97 | 98 | go func() { 99 | resCh <- `{"jsonrpc":"2.0", "method":"eth_subscribe", "params": {"subscription":"0xff", "result":"foo"}}` 100 | resCh <- `{"jsonrpc":"2.0", "method":"eth_subscribe", "params": {"subscription":"0xff", "result":"bar"}}` 101 | }() 102 | 103 | assert.Equal(t, "0xff", id) 104 | assert.Equal(t, json.RawMessage(`"foo"`), <-ch) 105 | assert.Equal(t, json.RawMessage(`"bar"`), <-ch) 106 | 107 | go func() { 108 | assert.JSONEq(t, 109 | `{"id":2, "jsonrpc":"2.0", "method":"eth_unsubscribe", "params":["0xff"]}`, 110 | <-reqCh, 111 | ) 112 | resCh <- `{"id":2}` 113 | }() 114 | 115 | err = ws.Unsubscribe(ctx, id) 116 | require.NoError(t, err) 117 | 118 | // Channel must be closed after unsubscribe. 119 | _, ok := <-ch 120 | require.False(t, ok) 121 | }, 122 | }, 123 | } 124 | for n, tt := range tests { 125 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 126 | wg := sync.WaitGroup{} 127 | reqCh := make(chan string) // Received requests. 128 | resCh := make(chan string) // Responses from server. 129 | closeCh := make(chan struct{}) // Stops the server. 130 | 131 | // Websocket server. 132 | server := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 133 | // Handle websocket requests. 134 | ctx := context.Background() 135 | conn, err := websocket.Accept(w, r, nil) 136 | if err != nil { 137 | require.NoError(t, err) 138 | } 139 | 140 | // Request reader. 141 | wg.Add(1) 142 | go func() { 143 | defer wg.Done() 144 | for { 145 | var req json.RawMessage 146 | if err := wsjson.Read(ctx, conn, &req); err != nil { 147 | if errors.As(err, &websocket.CloseError{}) { 148 | return 149 | } 150 | require.NoError(t, err) 151 | } 152 | reqCh <- string(req) 153 | } 154 | }() 155 | 156 | // Response writer. 157 | wg.Add(1) 158 | go func() { 159 | defer wg.Done() 160 | for { 161 | select { 162 | case <-closeCh: 163 | return 164 | case res := <-resCh: 165 | if err := wsjson.Write(ctx, conn, json.RawMessage(res)); err != nil { 166 | if errors.As(err, &websocket.CloseError{}) { 167 | return 168 | } 169 | require.NoError(t, err) 170 | } 171 | } 172 | } 173 | }() 174 | 175 | // Close the connection after the test. 176 | <-closeCh 177 | conn.Close(websocket.StatusNormalClosure, "") 178 | })} 179 | 180 | // Start HTTP server. 181 | ln, err := net.Listen("tcp", "127.0.0.1:0") 182 | if err != nil { 183 | require.NoError(t, err) 184 | } 185 | wg.Add(1) 186 | go func() { 187 | defer wg.Done() 188 | if err := server.Serve(ln); err != nil { 189 | if !errors.Is(err, http.ErrServerClosed) { 190 | require.NoError(t, err) 191 | } 192 | } 193 | }() 194 | 195 | // Create a websocket client. 196 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 197 | defer cancel() 198 | ws, err := NewWebsocket(WebsocketOptions{ 199 | Context: ctx, 200 | URL: "ws://" + ln.Addr().String(), 201 | Timout: time.Second, 202 | }) 203 | require.NoError(t, err) 204 | 205 | // Run the test. 206 | tt.asserts(t, ws, reqCh, resCh) 207 | 208 | // Stop the server. 209 | close(closeCh) 210 | _ = server.Close() 211 | wg.Wait() 212 | }) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /rpc/util.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/defiweb/go-eth/types" 7 | ) 8 | 9 | // signTransactionResult is the result of an eth_signTransaction request. 10 | // Some backends return only RLP encoded data, others return a JSON object, 11 | // this type can handle both. 12 | type signTransactionResult struct { 13 | Raw types.Bytes `json:"raw"` 14 | Tx *types.Transaction `json:"tx"` 15 | } 16 | 17 | func (s *signTransactionResult) UnmarshalJSON(input []byte) error { 18 | if len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' { 19 | return json.Unmarshal(input, &s.Raw) 20 | } 21 | type alias struct { 22 | Raw types.Bytes `json:"raw"` 23 | Tx *types.Transaction `json:"tx"` 24 | } 25 | var dec alias 26 | if err := json.Unmarshal(input, &dec); err != nil { 27 | return err 28 | } 29 | s.Tx = dec.Tx 30 | s.Raw = dec.Raw 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /txmodifier/chainid.go: -------------------------------------------------------------------------------- 1 | package txmodifier 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/defiweb/go-eth/rpc" 9 | "github.com/defiweb/go-eth/types" 10 | ) 11 | 12 | // ChainIDProvider is a transaction modifier that sets the chain ID of the 13 | // transaction. 14 | // 15 | // To use this modifier, add it using the WithTXModifiers option when creating 16 | // a new rpc.Client. 17 | type ChainIDProvider struct { 18 | mu sync.Mutex 19 | chainID uint64 20 | replace bool 21 | cache bool 22 | } 23 | 24 | // ChainIDProviderOptions is the options for NewChainIDProvider. 25 | type ChainIDProviderOptions struct { 26 | // ChainID is the chain ID that will be set for the transaction. 27 | // If 0, the chain ID will be queried from the node. 28 | ChainID uint64 29 | 30 | // Replace is true if the transaction chain ID should be replaced even if 31 | // it is already set. 32 | Replace bool 33 | 34 | // Cache is true if the chain ID will be cached instead of being queried 35 | // for each transaction. Cached chain ID will be used for all RPC clients 36 | // that use the same ChainIDProvider instance. 37 | // 38 | // If ChainID is set, this option is ignored. 39 | Cache bool 40 | } 41 | 42 | // NewChainIDProvider returns a new ChainIDProvider. 43 | func NewChainIDProvider(opts ChainIDProviderOptions) *ChainIDProvider { 44 | if opts.ChainID != 0 { 45 | opts.Cache = true 46 | } 47 | return &ChainIDProvider{ 48 | chainID: opts.ChainID, 49 | replace: opts.Replace, 50 | cache: opts.Cache, 51 | } 52 | } 53 | 54 | // Modify implements the rpc.TXModifier interface. 55 | func (p *ChainIDProvider) Modify(ctx context.Context, client rpc.RPC, tx *types.Transaction) error { 56 | if !p.replace && tx.ChainID != nil { 57 | return nil 58 | } 59 | if !p.cache { 60 | chainID, err := client.ChainID(ctx) 61 | if err != nil { 62 | return fmt.Errorf("chain ID provider: %w", err) 63 | } 64 | tx.ChainID = &chainID 65 | return nil 66 | } 67 | p.mu.Lock() 68 | defer p.mu.Unlock() 69 | var cid uint64 70 | if p.chainID != 0 { 71 | cid = p.chainID 72 | } else { 73 | chainID, err := client.ChainID(ctx) 74 | if err != nil { 75 | return fmt.Errorf("chain ID provider: %w", err) 76 | } 77 | p.chainID = chainID 78 | cid = chainID 79 | } 80 | tx.ChainID = &cid 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /txmodifier/chainid_test.go: -------------------------------------------------------------------------------- 1 | package txmodifier 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/defiweb/go-eth/types" 10 | ) 11 | 12 | func TestChainIDSetter_Modify(t *testing.T) { 13 | ctx := context.Background() 14 | fromAddress := types.MustAddressFromHex("0x1234567890abcdef1234567890abcdef12345678") 15 | 16 | t.Run("cache chain ID", func(t *testing.T) { 17 | tx := &types.Transaction{Call: types.Call{From: &fromAddress}} 18 | rpcMock := new(mockRPC) 19 | 20 | provider := NewChainIDProvider(ChainIDProviderOptions{ 21 | ChainID: 1, 22 | }) 23 | _ = provider.Modify(ctx, rpcMock, tx) 24 | 25 | assert.Equal(t, uint64(1), *tx.ChainID) 26 | }) 27 | 28 | t.Run("query RPC node", func(t *testing.T) { 29 | tx := &types.Transaction{Call: types.Call{From: &fromAddress}} 30 | rpcMock := new(mockRPC) 31 | rpcMock.On("ChainID", ctx).Return(uint64(1), nil) 32 | 33 | provider := NewChainIDProvider(ChainIDProviderOptions{ 34 | Replace: false, 35 | Cache: false, 36 | }) 37 | err := provider.Modify(ctx, rpcMock, tx) 38 | 39 | assert.NoError(t, err) 40 | assert.Equal(t, uint64(1), *tx.ChainID) 41 | }) 42 | 43 | t.Run("replace chain ID", func(t *testing.T) { 44 | tx := &types.Transaction{Call: types.Call{From: &fromAddress}, ChainID: uint64Ptr(2)} 45 | rpcMock := new(mockRPC) 46 | rpcMock.On("ChainID", ctx).Return(uint64(1), nil) 47 | 48 | provider := NewChainIDProvider(ChainIDProviderOptions{ 49 | Replace: true, 50 | Cache: false, 51 | }) 52 | err := provider.Modify(ctx, rpcMock, tx) 53 | 54 | assert.NoError(t, err) 55 | assert.NotEqual(t, uint64(2), *tx.ChainID) 56 | }) 57 | 58 | t.Run("do not replace chain ID", func(t *testing.T) { 59 | tx := &types.Transaction{Call: types.Call{From: &fromAddress}, ChainID: uint64Ptr(2)} 60 | rpcMock := new(mockRPC) 61 | rpcMock.On("ChainID", ctx).Return(uint64(1), nil) 62 | 63 | provider := NewChainIDProvider(ChainIDProviderOptions{ 64 | Replace: false, 65 | Cache: false, 66 | }) 67 | err := provider.Modify(ctx, rpcMock, tx) 68 | 69 | assert.NoError(t, err) 70 | assert.NotEqual(t, uint64(1), *tx.ChainID) 71 | }) 72 | 73 | t.Run("cache chain ID", func(t *testing.T) { 74 | tx := &types.Transaction{Call: types.Call{From: &fromAddress}, ChainID: uint64Ptr(2)} 75 | rpcMock := new(mockRPC) 76 | rpcMock.On("ChainID", ctx).Return(uint64(1), nil).Once() 77 | 78 | provider := NewChainIDProvider(ChainIDProviderOptions{ 79 | Replace: true, 80 | Cache: true, 81 | }) 82 | _ = provider.Modify(ctx, rpcMock, tx) 83 | _ = provider.Modify(ctx, rpcMock, tx) 84 | }) 85 | } 86 | 87 | func uint64Ptr(i uint64) *uint64 { 88 | return &i 89 | } 90 | -------------------------------------------------------------------------------- /txmodifier/gasfee.go: -------------------------------------------------------------------------------- 1 | package txmodifier 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/big" 7 | 8 | "github.com/defiweb/go-eth/rpc" 9 | "github.com/defiweb/go-eth/types" 10 | ) 11 | 12 | // LegacyGasFeeEstimator is a transaction modifier that estimates gas fee 13 | // using the rpc.GasPrice method. 14 | // 15 | // It sets transaction type to types.LegacyTxType or types.AccessListTxType if 16 | // an access list is provided. 17 | // 18 | // To use this modifier, add it using the WithTXModifiers option when creating 19 | // a new rpc.Client. 20 | type LegacyGasFeeEstimator struct { 21 | multiplier float64 22 | minGasPrice *big.Int 23 | maxGasPrice *big.Int 24 | replace bool 25 | } 26 | 27 | // LegacyGasFeeEstimatorOptions is the options for NewLegacyGasFeeEstimator. 28 | type LegacyGasFeeEstimatorOptions struct { 29 | Multiplier float64 // Multiplier is applied to the gas price. 30 | MinGasPrice *big.Int // MinGasPrice is the minimum gas price, or nil if there is no lower bound. 31 | MaxGasPrice *big.Int // MaxGasPrice is the maximum gas price, or nil if there is no upper bound. 32 | Replace bool // Replace is true if the gas price should be replaced even if it is already set. 33 | } 34 | 35 | // NewLegacyGasFeeEstimator returns a new LegacyGasFeeEstimator. 36 | func NewLegacyGasFeeEstimator(opts LegacyGasFeeEstimatorOptions) *LegacyGasFeeEstimator { 37 | return &LegacyGasFeeEstimator{ 38 | multiplier: opts.Multiplier, 39 | minGasPrice: opts.MinGasPrice, 40 | maxGasPrice: opts.MaxGasPrice, 41 | replace: opts.Replace, 42 | } 43 | } 44 | 45 | // Modify implements the rpc.TXModifier interface. 46 | func (e *LegacyGasFeeEstimator) Modify(ctx context.Context, client rpc.RPC, tx *types.Transaction) error { 47 | if !e.replace && tx.GasPrice != nil { 48 | return nil 49 | } 50 | gasPrice, err := client.GasPrice(ctx) 51 | if err != nil { 52 | return fmt.Errorf("legacy gas fee estimator: failed to get gas price: %w", err) 53 | } 54 | gasPrice, _ = new(big.Float).Mul(new(big.Float).SetInt(gasPrice), big.NewFloat(e.multiplier)).Int(nil) 55 | if e.minGasPrice != nil && gasPrice.Cmp(e.minGasPrice) < 0 { 56 | gasPrice = e.minGasPrice 57 | } 58 | if e.maxGasPrice != nil && gasPrice.Cmp(e.maxGasPrice) > 0 { 59 | gasPrice = e.maxGasPrice 60 | } 61 | tx.GasPrice = gasPrice 62 | tx.MaxFeePerGas = nil 63 | tx.MaxPriorityFeePerGas = nil 64 | switch { 65 | case tx.AccessList != nil: 66 | tx.Type = types.AccessListTxType 67 | default: 68 | tx.Type = types.LegacyTxType 69 | } 70 | return nil 71 | } 72 | 73 | // EIP1559GasFeeEstimator is a transaction modifier that estimates gas fee 74 | // using the rpc.GasPrice and rpc.MaxPriorityFeePerGas methods. 75 | // 76 | // It sets transaction type to types.DynamicFeeTxType. 77 | type EIP1559GasFeeEstimator struct { 78 | gasPriceMultiplier float64 79 | priorityFeePerGasMultiplier float64 80 | minGasPrice *big.Int 81 | maxGasPrice *big.Int 82 | minPriorityFeePerGas *big.Int 83 | maxPriorityFeePerGas *big.Int 84 | replace bool 85 | } 86 | 87 | // EIP1559GasFeeEstimatorOptions is the options for NewEIP1559GasFeeEstimator. 88 | type EIP1559GasFeeEstimatorOptions struct { 89 | GasPriceMultiplier float64 // GasPriceMultiplier is applied to the gas price. 90 | PriorityFeePerGasMultiplier float64 // PriorityFeePerGasMultiplier is applied to the priority fee per gas. 91 | MinGasPrice *big.Int // MinGasPrice is the minimum gas price, or nil if there is no lower bound. 92 | MaxGasPrice *big.Int // MaxGasPrice is the maximum gas price, or nil if there is no upper bound. 93 | MinPriorityFeePerGas *big.Int // MinPriorityFeePerGas is the minimum priority fee per gas, or nil if there is no lower bound. 94 | MaxPriorityFeePerGas *big.Int // MaxPriorityFeePerGas is the maximum priority fee per gas, or nil if there is no upper bound. 95 | Replace bool // Replace is true if the gas price should be replaced even if it is already set. 96 | } 97 | 98 | // NewEIP1559GasFeeEstimator returns a new EIP1559GasFeeEstimator. 99 | // 100 | // To use this modifier, add it using the WithTXModifiers option when creating 101 | // a new rpc.Client. 102 | func NewEIP1559GasFeeEstimator(opts EIP1559GasFeeEstimatorOptions) *EIP1559GasFeeEstimator { 103 | return &EIP1559GasFeeEstimator{ 104 | gasPriceMultiplier: opts.GasPriceMultiplier, 105 | priorityFeePerGasMultiplier: opts.PriorityFeePerGasMultiplier, 106 | minGasPrice: opts.MinGasPrice, 107 | maxGasPrice: opts.MaxGasPrice, 108 | minPriorityFeePerGas: opts.MinPriorityFeePerGas, 109 | maxPriorityFeePerGas: opts.MaxPriorityFeePerGas, 110 | replace: opts.Replace, 111 | } 112 | } 113 | 114 | // Modify implements the rpc.TXModifier interface. 115 | func (e *EIP1559GasFeeEstimator) Modify(ctx context.Context, client rpc.RPC, tx *types.Transaction) error { 116 | if !e.replace && tx.MaxFeePerGas != nil && tx.MaxPriorityFeePerGas != nil { 117 | return nil 118 | } 119 | maxFeePerGas, err := client.GasPrice(ctx) 120 | if err != nil { 121 | return fmt.Errorf("EIP-1559 gas fee estimator: failed to get gas price: %w", err) 122 | } 123 | priorityFeePerGas, err := client.MaxPriorityFeePerGas(ctx) 124 | if err != nil { 125 | return fmt.Errorf("EIP-1559 gas fee estimator: failed to get max priority fee per gas: %w", err) 126 | } 127 | maxFeePerGas, _ = new(big.Float).Mul(new(big.Float).SetInt(maxFeePerGas), big.NewFloat(e.gasPriceMultiplier)).Int(nil) 128 | priorityFeePerGas, _ = new(big.Float).Mul(new(big.Float).SetInt(priorityFeePerGas), big.NewFloat(e.priorityFeePerGasMultiplier)).Int(nil) 129 | if e.minGasPrice != nil && maxFeePerGas.Cmp(e.minGasPrice) < 0 { 130 | maxFeePerGas = e.minGasPrice 131 | } 132 | if e.maxGasPrice != nil && maxFeePerGas.Cmp(e.maxGasPrice) > 0 { 133 | maxFeePerGas = e.maxGasPrice 134 | } 135 | if e.minPriorityFeePerGas != nil && priorityFeePerGas.Cmp(e.minPriorityFeePerGas) < 0 { 136 | priorityFeePerGas = e.minPriorityFeePerGas 137 | } 138 | if e.maxPriorityFeePerGas != nil && priorityFeePerGas.Cmp(e.maxPriorityFeePerGas) > 0 { 139 | priorityFeePerGas = e.maxPriorityFeePerGas 140 | } 141 | if maxFeePerGas.Cmp(priorityFeePerGas) < 0 { 142 | priorityFeePerGas = maxFeePerGas 143 | } 144 | tx.GasPrice = nil 145 | tx.MaxFeePerGas = maxFeePerGas 146 | tx.MaxPriorityFeePerGas = priorityFeePerGas 147 | tx.Type = types.DynamicFeeTxType 148 | return nil 149 | } 150 | -------------------------------------------------------------------------------- /txmodifier/gaslimit.go: -------------------------------------------------------------------------------- 1 | package txmodifier 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/big" 7 | 8 | "github.com/defiweb/go-eth/rpc" 9 | "github.com/defiweb/go-eth/types" 10 | ) 11 | 12 | // GasLimitEstimator is a transaction modifier that estimates gas limit 13 | // using the rpc.EstimateGas method. 14 | // 15 | // To use this modifier, add it using the WithTXModifiers option when creating 16 | // a new rpc.Client. 17 | type GasLimitEstimator struct { 18 | multiplier float64 19 | minGas uint64 20 | maxGas uint64 21 | replace bool 22 | } 23 | 24 | // GasLimitEstimatorOptions is the options for NewGasLimitEstimator. 25 | type GasLimitEstimatorOptions struct { 26 | Multiplier float64 // Multiplier is applied to the gas limit. 27 | MinGas uint64 // MinGas is the minimum gas limit, or 0 if there is no lower bound. 28 | MaxGas uint64 // MaxGas is the maximum gas limit, or 0 if there is no upper bound. 29 | Replace bool // Replace is true if the gas limit should be replaced even if it is already set. 30 | } 31 | 32 | // NewGasLimitEstimator returns a new GasLimitEstimator. 33 | func NewGasLimitEstimator(opts GasLimitEstimatorOptions) *GasLimitEstimator { 34 | return &GasLimitEstimator{ 35 | multiplier: opts.Multiplier, 36 | minGas: opts.MinGas, 37 | maxGas: opts.MaxGas, 38 | replace: opts.Replace, 39 | } 40 | } 41 | 42 | // Modify implements the rpc.TXModifier interface. 43 | func (e *GasLimitEstimator) Modify(ctx context.Context, client rpc.RPC, tx *types.Transaction) error { 44 | if !e.replace && tx.GasLimit != nil { 45 | return nil 46 | } 47 | gasLimit, _, err := client.EstimateGas(ctx, &tx.Call, types.LatestBlockNumber) 48 | if err != nil { 49 | return fmt.Errorf("gas limit estimator: failed to estimate gas limit: %w", err) 50 | } 51 | gasLimit, _ = new(big.Float).Mul(new(big.Float).SetUint64(gasLimit), big.NewFloat(e.multiplier)).Uint64() 52 | if gasLimit < e.minGas || (e.maxGas > 0 && gasLimit > e.maxGas) { 53 | return fmt.Errorf("gas limit estimator: estimated gas limit %d is out of range [%d, %d]", gasLimit, e.minGas, e.maxGas) 54 | } 55 | tx.GasLimit = &gasLimit 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /txmodifier/gaslimit_test.go: -------------------------------------------------------------------------------- 1 | package txmodifier 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/defiweb/go-eth/types" 11 | ) 12 | 13 | func TestGasLimitEstimator_Modify(t *testing.T) { 14 | ctx := context.Background() 15 | 16 | t.Run("successful gas estimation", func(t *testing.T) { 17 | tx := &types.Transaction{} 18 | rpcMock := new(mockRPC) 19 | rpcMock.On("EstimateGas", ctx, &tx.Call, types.LatestBlockNumber).Return(uint64(1000), &tx.Call, nil) 20 | 21 | estimator := NewGasLimitEstimator(GasLimitEstimatorOptions{ 22 | Multiplier: 1.5, 23 | MinGas: 500, 24 | MaxGas: 2000, 25 | }) 26 | err := estimator.Modify(ctx, rpcMock, tx) 27 | 28 | assert.NoError(t, err) 29 | assert.Equal(t, uint64(1500), *tx.GasLimit) 30 | }) 31 | 32 | t.Run("gas estimation error", func(t *testing.T) { 33 | tx := &types.Transaction{} 34 | rpcMock := new(mockRPC) 35 | rpcMock.On("EstimateGas", ctx, &tx.Call, types.LatestBlockNumber).Return(uint64(0), &tx.Call, errors.New("rpc error")) 36 | 37 | estimator := NewGasLimitEstimator(GasLimitEstimatorOptions{ 38 | Multiplier: 1.5, 39 | MinGas: 500, 40 | MaxGas: 2000, 41 | }) 42 | err := estimator.Modify(ctx, rpcMock, tx) 43 | 44 | assert.Error(t, err) 45 | assert.Contains(t, err.Error(), "failed to estimate gas") 46 | }) 47 | 48 | t.Run("gas out of range", func(t *testing.T) { 49 | tx := &types.Transaction{} 50 | rpcMock := new(mockRPC) 51 | rpcMock.On("EstimateGas", ctx, &tx.Call, types.LatestBlockNumber).Return(uint64(3000), &tx.Call, nil) 52 | 53 | estimator := NewGasLimitEstimator(GasLimitEstimatorOptions{ 54 | Multiplier: 1.5, 55 | MinGas: 500, 56 | MaxGas: 2000, 57 | }) 58 | err := estimator.Modify(ctx, rpcMock, tx) 59 | 60 | assert.Error(t, err) 61 | assert.Contains(t, err.Error(), "estimated gas") 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /txmodifier/nonce.go: -------------------------------------------------------------------------------- 1 | package txmodifier 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/defiweb/go-eth/rpc" 9 | "github.com/defiweb/go-eth/types" 10 | ) 11 | 12 | // NonceProvider is a transaction modifier that sets the nonce for the 13 | // transaction. 14 | // 15 | // To use this modifier, add it using the WithTXModifiers option when creating 16 | // a new rpc.Client. 17 | type NonceProvider struct { 18 | usePendingBlock bool 19 | replace bool 20 | } 21 | 22 | // NonceProviderOptions is the options for NewNonceProvider. 23 | // 24 | // If UsePendingBlock is true, then the next transaction nonce is fetched from 25 | // the pending block. Otherwise, the next transaction nonce is fetched from the 26 | // latest block. Using the pending block is not recommended as the behavior 27 | // of the GetTransactionCount method on the pending block may be different 28 | // between different Ethereum clients. 29 | type NonceProviderOptions struct { 30 | UsePendingBlock bool // UsePendingBlock indicates whether to use the pending block. 31 | Replace bool // Replace is true if the nonce should be replaced even if it is already set. 32 | } 33 | 34 | // NewNonceProvider returns a new NonceProvider. 35 | func NewNonceProvider(opts NonceProviderOptions) *NonceProvider { 36 | return &NonceProvider{ 37 | usePendingBlock: opts.UsePendingBlock, 38 | replace: opts.Replace, 39 | } 40 | } 41 | 42 | // Modify implements the rpc.TXModifier interface. 43 | func (p *NonceProvider) Modify(ctx context.Context, client rpc.RPC, tx *types.Transaction) error { 44 | if !p.replace && tx.Nonce != nil { 45 | return nil 46 | } 47 | if tx.From == nil { 48 | return errors.New("nonce provider: missing from address") 49 | } 50 | block := types.LatestBlockNumber 51 | if p.usePendingBlock { 52 | block = types.PendingBlockNumber 53 | } 54 | pendingNonce, err := client.GetTransactionCount(ctx, *tx.From, block) 55 | if err != nil { 56 | return fmt.Errorf("nonce provider: %w", err) 57 | } 58 | tx.Nonce = &pendingNonce 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /txmodifier/nonce_test.go: -------------------------------------------------------------------------------- 1 | package txmodifier 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/defiweb/go-eth/types" 11 | ) 12 | 13 | func TestNonceProvider_Modify(t *testing.T) { 14 | ctx := context.Background() 15 | fromAddress := types.MustAddressFromHex("0x1234567890abcdef1234567890abcdef12345678") 16 | 17 | t.Run("nonce fetch from latest block", func(t *testing.T) { 18 | tx := &types.Transaction{Call: types.Call{From: &fromAddress}} 19 | rpcMock := new(mockRPC) 20 | rpcMock.On("GetTransactionCount", ctx, fromAddress, types.LatestBlockNumber).Return(uint64(10), nil) 21 | 22 | provider := NewNonceProvider(NonceProviderOptions{ 23 | UsePendingBlock: false, 24 | }) 25 | err := provider.Modify(ctx, rpcMock, tx) 26 | 27 | assert.NoError(t, err) 28 | assert.Equal(t, uint64(10), *tx.Nonce) 29 | }) 30 | 31 | t.Run("nonce fetch from pending block", func(t *testing.T) { 32 | tx := &types.Transaction{Call: types.Call{From: &fromAddress}} 33 | rpcMock := new(mockRPC) 34 | rpcMock.On("GetTransactionCount", ctx, fromAddress, types.PendingBlockNumber).Return(uint64(11), nil) 35 | 36 | provider := NewNonceProvider(NonceProviderOptions{ 37 | UsePendingBlock: true, 38 | }) 39 | err := provider.Modify(ctx, rpcMock, tx) 40 | 41 | assert.NoError(t, err) 42 | assert.Equal(t, uint64(11), *tx.Nonce) 43 | }) 44 | 45 | t.Run("missing from address", func(t *testing.T) { 46 | txWithoutFrom := &types.Transaction{} 47 | provider := NewNonceProvider(NonceProviderOptions{ 48 | UsePendingBlock: true, 49 | }) 50 | err := provider.Modify(ctx, nil, txWithoutFrom) 51 | 52 | assert.Error(t, err) 53 | assert.Contains(t, err.Error(), "nonce provider: missing from address") 54 | }) 55 | 56 | t.Run("nonce fetch error", func(t *testing.T) { 57 | tx := &types.Transaction{Call: types.Call{From: &fromAddress}} 58 | rpcMock := new(mockRPC) 59 | rpcMock.On("GetTransactionCount", ctx, fromAddress, types.LatestBlockNumber).Return(uint64(0), errors.New("rpc error")) 60 | 61 | provider := NewNonceProvider(NonceProviderOptions{ 62 | UsePendingBlock: false, 63 | }) 64 | err := provider.Modify(ctx, rpcMock, tx) 65 | 66 | assert.Error(t, err) 67 | assert.Contains(t, err.Error(), "nonce provider") 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /txmodifier/txmodifier_test.go: -------------------------------------------------------------------------------- 1 | package txmodifier 2 | 3 | import ( 4 | "context" 5 | "math/big" 6 | 7 | "github.com/stretchr/testify/mock" 8 | 9 | "github.com/defiweb/go-eth/rpc" 10 | "github.com/defiweb/go-eth/types" 11 | ) 12 | 13 | type mockRPC struct { 14 | rpc.Client 15 | mock.Mock 16 | } 17 | 18 | func (m *mockRPC) ChainID(ctx context.Context) (uint64, error) { 19 | args := m.Called(ctx) 20 | return args.Get(0).(uint64), args.Error(1) 21 | } 22 | 23 | func (m *mockRPC) EstimateGas(ctx context.Context, call *types.Call, block types.BlockNumber) (uint64, *types.Call, error) { 24 | args := m.Called(ctx, call, block) 25 | return args.Get(0).(uint64), call, args.Error(2) 26 | } 27 | 28 | func (m *mockRPC) GasPrice(ctx context.Context) (*big.Int, error) { 29 | args := m.Called(ctx) 30 | return args.Get(0).(*big.Int), args.Error(1) 31 | } 32 | 33 | func (m *mockRPC) MaxPriorityFeePerGas(ctx context.Context) (*big.Int, error) { 34 | args := m.Called(ctx) 35 | return args.Get(0).(*big.Int), args.Error(1) 36 | } 37 | 38 | func (m *mockRPC) GetTransactionCount(ctx context.Context, address types.Address, block types.BlockNumber) (uint64, error) { 39 | args := m.Called(ctx, address, block) 40 | return args.Get(0).(uint64), args.Error(1) 41 | } 42 | -------------------------------------------------------------------------------- /types/util.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math/big" 7 | 8 | "github.com/defiweb/go-eth/hexutil" 9 | ) 10 | 11 | // bytesMarshalJSON encodes the given bytes as a JSON string where each byte is 12 | // represented by a two-digit hex number. The hex string is always even-length 13 | // and prefixed with "0x". 14 | func bytesMarshalJSON(input []byte) []byte { 15 | return naiveQuote(bytesMarshalText(input)) 16 | } 17 | 18 | // bytesMarshalText encodes the given bytes as a string where each byte is 19 | // represented by a two-digit hex number. The hex string is always even-length 20 | // and prefixed with "0x". 21 | func bytesMarshalText(input []byte) []byte { 22 | return []byte(hexutil.BytesToHex(input)) 23 | } 24 | 25 | // bytesUnmarshalJSON decodes the given JSON string where each byte is 26 | // represented by a two-digit hex number. The hex string may be prefixed with 27 | // "0x". If the hex string is odd-length, it is padded with a leading zero. 28 | func bytesUnmarshalJSON(input []byte, output *[]byte) error { 29 | if bytes.Equal(input, []byte("null")) { 30 | return nil 31 | } 32 | return bytesUnmarshalText(naiveUnquote(input), output) 33 | } 34 | 35 | // bytesUnmarshalText decodes the given string where each byte is represented by 36 | // a two-digit hex number. The hex string may be prefixed with "0x". If the hex 37 | // string is odd-length, it is padded with a leading zero. 38 | func bytesUnmarshalText(input []byte, output *[]byte) error { 39 | var err error 40 | *output, err = hexutil.HexToBytes(string(input)) 41 | return err 42 | } 43 | 44 | // fixedBytesUnmarshalJSON works like bytesUnmarshalJSON, but it is designed to 45 | // be used with fixed-size byte arrays. The given byte array must be large 46 | // enough to hold the decoded data. 47 | func fixedBytesUnmarshalJSON(input, output []byte) error { 48 | if bytes.Equal(input, []byte("null")) { 49 | return nil 50 | } 51 | return fixedBytesUnmarshalText(naiveUnquote(input), output) 52 | } 53 | 54 | // fixedBytesUnmarshalText works like bytesUnmarshalText, but it is designed to 55 | // be used with fixed-size byte arrays. The given byte array must be large 56 | // enough to hold the decoded data. 57 | func fixedBytesUnmarshalText(input, output []byte) error { 58 | data, err := hexutil.HexToBytes(string(input)) 59 | if err != nil { 60 | return err 61 | } 62 | if len(data) != len(output) { 63 | return fmt.Errorf("invalid length %d, want %d", len(data), len(output)) 64 | } 65 | copy(output, data) 66 | return nil 67 | } 68 | 69 | // numberMarshalJSON encodes the given big integer as JSON string where number 70 | // is resented in hexadecimal format. The hex string is prefixed with "0x". 71 | // Negative numbers are prefixed with "-0x". 72 | func numberMarshalJSON(input *big.Int) []byte { 73 | return naiveQuote(numberMarshalText(input)) 74 | } 75 | 76 | // numberMarshalText encodes the given big integer as string where number is 77 | // resented in hexadecimal format. The hex string is prefixed with "0x". 78 | // Negative numbers are prefixed with "-0x". 79 | func numberMarshalText(input *big.Int) []byte { 80 | return []byte(hexutil.BigIntToHex(input)) 81 | } 82 | 83 | // numberUnmarshalJSON decodes the given JSON string where number is resented in 84 | // hexadecimal format. The hex string may be prefixed with "0x". Negative numbers 85 | // must start with minus sign. 86 | func numberUnmarshalJSON(input []byte, output *big.Int) error { 87 | return numberUnmarshalText(naiveUnquote(input), output) 88 | } 89 | 90 | // numberUnmarshalText decodes the given string where number is resented in 91 | // hexadecimal format. The hex string may be prefixed with "0x". Negative numbers 92 | // must start with minus sign. 93 | func numberUnmarshalText(input []byte, output *big.Int) error { 94 | data, err := hexutil.HexToBigInt(string(input)) 95 | if err != nil { 96 | return err 97 | } 98 | output.Set(data) 99 | return nil 100 | } 101 | 102 | // naiveQuote returns a double-quoted string. It does not perform any escaping. 103 | func naiveQuote(i []byte) []byte { 104 | b := make([]byte, len(i)+2) 105 | b[0] = '"' 106 | b[len(b)-1] = '"' 107 | copy(b[1:], i) 108 | return b 109 | } 110 | 111 | // naiveUnquote returns the string inside the quotes. It does not perform any 112 | // unescaping. 113 | func naiveUnquote(i []byte) []byte { 114 | if len(i) >= 2 && i[0] == '"' && i[len(i)-1] == '"' { 115 | return i[1 : len(i)-1] 116 | } 117 | return i 118 | } 119 | -------------------------------------------------------------------------------- /wallet/key.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/defiweb/go-eth/types" 7 | ) 8 | 9 | // Key is the interface for an Ethereum key. 10 | type Key interface { 11 | // Address returns the address of the key. 12 | Address() types.Address 13 | 14 | // SignMessage signs the given message. 15 | SignMessage(ctx context.Context, data []byte) (*types.Signature, error) 16 | 17 | // SignTransaction signs the given transaction. 18 | SignTransaction(ctx context.Context, tx *types.Transaction) error 19 | 20 | // VerifyMessage verifies whether the given data is signed by the key. 21 | VerifyMessage(ctx context.Context, data []byte, sig types.Signature) bool 22 | } 23 | 24 | // KeyWithHashSigner is the interface for an Ethereum key that can sign data using 25 | // a private key, skipping the EIP-191 message prefix. 26 | type KeyWithHashSigner interface { 27 | Key 28 | 29 | // SignHash signs the given hash without the EIP-191 message prefix. 30 | SignHash(ctx context.Context, hash types.Hash) (*types.Signature, error) 31 | 32 | // VerifyHash whether the given hash is signed by the key without the 33 | // EIP-191 message prefix. 34 | VerifyHash(ctx context.Context, hash types.Hash, sig types.Signature) bool 35 | } 36 | -------------------------------------------------------------------------------- /wallet/key_hd_test.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestWallet_Mnemonic(t *testing.T) { 12 | tests := []struct { 13 | account uint32 14 | index uint32 15 | addr string 16 | }{ 17 | {0, 0, "0x02941ca660485ba7dc196b510d9a6192c2648709"}, 18 | {0, 1, "0xd050d1f66eb5ed560079754f3c1623b369a1a5ee"}, 19 | {1, 0, "0x7931220c3f0ee7efb9e323de4b9053e8aba3ff30"}, 20 | {1, 1, "0x784f2db7796a9198898bc5da9c878cf027c03a33"}, 21 | } 22 | for n, tt := range tests { 23 | t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { 24 | key, err := NewKeyFromMnemonic( 25 | "gravity trophy shrimp suspect sheriff avocado label trust dove tragic pitch title network myself spell task protect smooth sword diary brain blossom under bulb", 26 | "fJF*(SDF*(*@J!)(SU*(D*F&^&TYSDFHL#@HO*&O", 27 | tt.account, 28 | tt.index, 29 | ) 30 | require.NoError(t, err) 31 | assert.Equal(t, tt.addr, key.Address().String()) 32 | }) 33 | } 34 | } 35 | 36 | func TestParseDerivationPath(t *testing.T) { 37 | // Based on test cases from github.com/ethereum/go-ethereum/blob/master/accounts/hd_test.go 38 | tests := []struct { 39 | input string 40 | output DerivationPath 41 | }{ 42 | // Plain absolute derivation paths. 43 | {"m/44'/60'/0'/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, 44 | {"m/44'/60'/0'/128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}}, 45 | {"m/44'/60'/0'/0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, 46 | {"m/44'/60'/0'/128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}}, 47 | {"m/2147483692/2147483708/2147483648/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, 48 | {"m/2147483692/2147483708/2147483648/2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, 49 | 50 | // Plain relative derivation paths. 51 | {"0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0}}, 52 | {"128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 128}}, 53 | {"0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 0}}, 54 | {"128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 128}}, 55 | {"2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 0}}, 56 | 57 | // Hexadecimal absolute derivation paths. 58 | {"m/0x2C'/0x3c'/0x00'/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, 59 | {"m/0x2C'/0x3c'/0x00'/0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}}, 60 | {"m/0x2C'/0x3c'/0x00'/0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, 61 | {"m/0x2C'/0x3c'/0x00'/0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}}, 62 | {"m/0x8000002C/0x8000003c/0x80000000/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, 63 | {"m/0x8000002C/0x8000003c/0x80000000/0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, 64 | 65 | // Hexadecimal relative derivation paths. 66 | {"0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0}}, 67 | {"0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 128}}, 68 | {"0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 0}}, 69 | {"0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 128}}, 70 | {"0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 0}}, 71 | 72 | // Weird inputs just to ensure they work. 73 | {" m / 44 '\n/\n 60 \n\n\t' /\n0 ' /\t\t 0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, 74 | 75 | // Invalid derivation paths 76 | {"", nil}, // Empty relative derivation path. 77 | {"m", nil}, // Empty absolute derivation path. 78 | {"m/", nil}, // Missing last derivation component. 79 | {"/44'/60'/0'/0", nil}, // Absolute path without m prefix, might be user error. 80 | {"m/2147483648'", nil}, // Overflows 32 bit integer. 81 | {"m/-1'", nil}, // Cannot contain negative number. 82 | } 83 | for _, tt := range tests { 84 | t.Run(tt.input, func(t *testing.T) { 85 | got, err := ParseDerivationPath(tt.input) 86 | if tt.output == nil { 87 | assert.Error(t, err) 88 | } else { 89 | assert.NoError(t, err) 90 | assert.Equal(t, tt.output, got) 91 | } 92 | }) 93 | } 94 | } 95 | 96 | func FuzzParseDerivationPath(f *testing.F) { 97 | for _, input := range []string{ 98 | "m", 99 | "/", 100 | "0x", 101 | "44", 102 | "2147483692", 103 | "2147483648", 104 | "'", 105 | " ", 106 | "\t", 107 | "\r", 108 | "\n", 109 | } { 110 | f.Add([]byte(input)) 111 | } 112 | f.Fuzz(func(t *testing.T, input []byte) { 113 | _, _ = ParseDerivationPath(string(input)) 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /wallet/key_json.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "errors" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/defiweb/go-eth/types" 11 | ) 12 | 13 | var ErrKeyNotFound = errors.New("key not found") 14 | 15 | // NewKeyFromJSON loads an Ethereum key from a JSON file. 16 | func NewKeyFromJSON(path string, passphrase string) (*PrivateKey, error) { 17 | content, err := os.ReadFile(path) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return NewKeyFromJSONContent(content, passphrase) 22 | } 23 | 24 | // NewKeyFromJSONContent returns a new key from a JSON. 25 | func NewKeyFromJSONContent(content []byte, passphrase string) (*PrivateKey, error) { 26 | var jKey jsonKey 27 | if err := json.Unmarshal(content, &jKey); err != nil { 28 | return nil, err 29 | } 30 | if jKey.Version != 3 { 31 | return nil, errors.New("only V3 keys are supported") 32 | } 33 | prv, err := decryptV3Key(jKey.Crypto, []byte(passphrase)) 34 | if err != nil { 35 | return nil, err 36 | } 37 | key := NewKeyFromBytes(prv) 38 | if !jKey.Address.IsZero() && jKey.Address != key.Address() { 39 | return nil, errors.New("decrypted key address does not match address in file") 40 | } 41 | return key, nil 42 | } 43 | 44 | // NewKeyFromDirectory returns a new key from a directory containing JSON 45 | // files. 46 | func NewKeyFromDirectory(path string, passphrase string, address types.Address) (*PrivateKey, error) { 47 | items, err := os.ReadDir(path) 48 | if err != nil { 49 | return nil, err 50 | } 51 | for _, item := range items { 52 | if item.IsDir() { 53 | // Skip directories. 54 | continue 55 | } 56 | i, _ := item.Info() 57 | if i.Size() == 0 || i.Size() > 1<<20 { 58 | // Skip empty files and files larger than 1MB. 59 | continue 60 | } 61 | key, err := NewKeyFromJSON(filepath.Join(path, item.Name()), passphrase) 62 | if err != nil { 63 | // Skip files that are not keys or have invalid content. 64 | continue 65 | } 66 | if address == key.Address() { 67 | return key, nil 68 | } 69 | } 70 | return nil, ErrKeyNotFound 71 | } 72 | 73 | type jsonKey struct { 74 | ID string `json:"id"` 75 | Version int64 `json:"version"` 76 | Address types.Address `json:"address"` 77 | Crypto jsonKeyCrypto `json:"crypto"` 78 | } 79 | 80 | type jsonKeyCrypto struct { 81 | Cipher string `json:"cipher"` 82 | CipherText jsonHex `json:"ciphertext"` 83 | CipherParams jsonKeyCipherParams `json:"cipherparams"` 84 | KDF string `json:"kdf"` 85 | KDFParams jsonKeyKDFParams `json:"kdfparams"` 86 | MAC jsonHex `json:"mac"` 87 | } 88 | 89 | type jsonKeyCipherParams struct { 90 | IV jsonHex `json:"iv"` 91 | } 92 | 93 | type jsonKeyKDFParams struct { 94 | DKLen int `json:"dklen"` 95 | Salt jsonHex `json:"salt"` 96 | 97 | // Scrypt params: 98 | N int `json:"n"` 99 | P int `json:"p"` 100 | R int `json:"r"` 101 | 102 | // PBKDF2 params: 103 | C int `json:"c"` 104 | PRF string `json:"prf"` 105 | } 106 | 107 | type jsonHex []byte 108 | 109 | func (h jsonHex) MarshalJSON() ([]byte, error) { 110 | return []byte(`"` + hex.EncodeToString(h) + `"`), nil 111 | } 112 | 113 | func (h *jsonHex) UnmarshalJSON(data []byte) (err error) { 114 | if len(data) < 2 || data[0] != '"' || data[len(data)-1] != '"' { 115 | return errors.New("invalid hex string") 116 | } 117 | *h, err = hex.DecodeString(string(data[1 : len(data)-1])) 118 | return 119 | } 120 | -------------------------------------------------------------------------------- /wallet/key_json_test.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/defiweb/go-eth/types" 10 | ) 11 | 12 | func TestNewKeyFromJSON(t *testing.T) { 13 | t.Run("scrypt", func(t *testing.T) { 14 | key, err := NewKeyFromJSON("./testdata/scrypt.json", "test123") 15 | require.NoError(t, err) 16 | assert.Equal(t, "0x2d800d93b065ce011af83f316cef9f0d005b0aa4", key.Address().String()) 17 | }) 18 | t.Run("pbkdf2", func(t *testing.T) { 19 | key, err := NewKeyFromJSON("./testdata/pbkdf2.json", "testpassword") 20 | require.NoError(t, err) 21 | assert.Equal(t, "0x008aeeda4d805471df9b2a5b0f38a0c3bcba786b", key.Address().String()) 22 | }) 23 | } 24 | 25 | func TestNewKeyFromDirectory(t *testing.T) { 26 | t.Run("key-1", func(t *testing.T) { 27 | key, err := NewKeyFromDirectory("./testdata", "test123", types.MustAddressFromHex("0x2d800d93b065ce011af83f316cef9f0d005b0aa4")) 28 | require.NoError(t, err) 29 | assert.Equal(t, "0x2d800d93b065ce011af83f316cef9f0d005b0aa4", key.Address().String()) 30 | }) 31 | t.Run("key-2", func(t *testing.T) { 32 | key, err := NewKeyFromDirectory("./testdata", "testpassword", types.MustAddressFromHex("0x008aeeda4d805471df9b2a5b0f38a0c3bcba786b")) 33 | require.NoError(t, err) 34 | assert.Equal(t, "0x008aeeda4d805471df9b2a5b0f38a0c3bcba786b", key.Address().String()) 35 | }) 36 | t.Run("invalid-password", func(t *testing.T) { 37 | _, err := NewKeyFromDirectory("./testdata", "", types.MustAddressFromHex("0x2d800d93b065ce011af83f316cef9f0d005b0aa4")) 38 | require.Error(t, err) 39 | }) 40 | t.Run("missing-key", func(t *testing.T) { 41 | _, err := NewKeyFromDirectory("./testdata", "", types.MustAddressFromHex("0x0000000000000000000000000000000000000000")) 42 | require.Error(t, err) 43 | }) 44 | } 45 | 46 | func TestPrivateKey_JSON(t *testing.T) { 47 | t.Run("random", func(t *testing.T) { 48 | key1 := NewRandomKey() 49 | j, err := key1.JSON("test123", LightScryptN, LightScryptP) 50 | require.NoError(t, err) 51 | 52 | key2, err := NewKeyFromJSONContent(j, "test123") 53 | require.NoError(t, err) 54 | 55 | assert.Equal(t, key1.Address(), key2.Address()) 56 | }) 57 | t.Run("existing", func(t *testing.T) { 58 | key1, err := NewKeyFromJSON("./testdata/scrypt.json", "test123") 59 | require.NoError(t, err) 60 | 61 | j, err := key1.JSON("test123", LightScryptN, LightScryptP) 62 | require.NoError(t, err) 63 | 64 | key2, err := NewKeyFromJSONContent(j, "test123") 65 | require.NoError(t, err) 66 | 67 | assert.Equal(t, key1.Address(), key2.Address()) 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /wallet/key_json_v3.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/ecdsa" 8 | "crypto/rand" 9 | "crypto/sha256" 10 | "encoding/hex" 11 | "fmt" 12 | "io" 13 | 14 | "golang.org/x/crypto/pbkdf2" 15 | "golang.org/x/crypto/scrypt" 16 | 17 | "github.com/defiweb/go-eth/crypto" 18 | ) 19 | 20 | // The code below is based on: 21 | // github.com/ethereum/go-ethereum/tree/master/accounts/keystore 22 | 23 | const ( 24 | StandardScryptN = 1 << 18 25 | StandardScryptP = 1 26 | LightScryptN = 1 << 12 27 | LightScryptP = 6 28 | scryptR = 8 29 | scryptDKLen = 32 30 | ) 31 | 32 | func encryptV3Key(key *ecdsa.PrivateKey, passphrase string, scryptN, scryptP int) (*jsonKey, error) { 33 | // Generate a random salt. 34 | salt := make([]byte, 32) 35 | if _, err := rand.Read(salt); err != nil { 36 | return nil, err 37 | } 38 | 39 | // Derive the key from the passphrase. 40 | derivedKey, err := scrypt.Key([]byte(passphrase), salt, scryptN, scryptR, scryptP, scryptDKLen) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | // Generate a random IV. 46 | iv := make([]byte, aes.BlockSize) 47 | if _, err := rand.Read(iv); err != nil { 48 | return nil, err 49 | } 50 | 51 | // Encrypt the key with AES-128-CTR. 52 | d := key.D.Bytes() 53 | data := make([]byte, 32) 54 | copy(data[32-len(d):], d) 55 | cipherText, err := aesCTRXOR(derivedKey[:16], data, iv) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | // Calculate the MAC of the encrypted key. 61 | mac := crypto.Keccak256(derivedKey[16:32], cipherText) 62 | 63 | // Generate a random UUID for the keyfile. 64 | id, err := randUUID() 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | // Assemble and return the key JSON. 70 | return &jsonKey{ 71 | Version: 3, 72 | ID: id, 73 | Address: crypto.ECPublicKeyToAddress(&key.PublicKey), 74 | Crypto: jsonKeyCrypto{ 75 | Cipher: "aes-128-ctr", 76 | CipherParams: jsonKeyCipherParams{ 77 | IV: iv, 78 | }, 79 | CipherText: cipherText, 80 | KDF: "scrypt", 81 | KDFParams: jsonKeyKDFParams{ 82 | DKLen: scryptDKLen, 83 | N: scryptN, 84 | P: scryptP, 85 | R: scryptR, 86 | Salt: salt, 87 | }, 88 | MAC: mac.Bytes(), 89 | }, 90 | }, nil 91 | } 92 | 93 | // decryptKey decrypts the given V3 key with the given passphrase. 94 | func decryptV3Key(cryptoJson jsonKeyCrypto, passphrase []byte) ([]byte, error) { 95 | if cryptoJson.Cipher != "aes-128-ctr" { 96 | return nil, fmt.Errorf("cipher not supported: %v", cryptoJson.Cipher) 97 | } 98 | 99 | // Derive the key from the passphrase. 100 | derivedKey, err := deriveKey(cryptoJson, passphrase) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | // VerifyHash the derived key matches the key in the JSON. If not, the 106 | // passphrase is incorrect. 107 | calculatedMAC := crypto.Keccak256(derivedKey[16:32], cryptoJson.CipherText) 108 | if !bytes.Equal(calculatedMAC.Bytes(), cryptoJson.MAC) { 109 | return nil, fmt.Errorf("invalid passphrase or keyfile") 110 | } 111 | 112 | // Decrypt the key with AES-128-CTR. 113 | plainText, err := aesCTRXOR(derivedKey[:16], cryptoJson.CipherText, cryptoJson.CipherParams.IV) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | return plainText, err 119 | } 120 | 121 | // deriveKey returns the derived key from the JSON keyfile. 122 | func deriveKey(cryptoJSON jsonKeyCrypto, passphrase []byte) ([]byte, error) { 123 | switch cryptoJSON.KDF { 124 | case "scrypt": 125 | return scrypt.Key( 126 | passphrase, 127 | cryptoJSON.KDFParams.Salt, 128 | cryptoJSON.KDFParams.N, 129 | cryptoJSON.KDFParams.R, 130 | cryptoJSON.KDFParams.P, 131 | cryptoJSON.KDFParams.DKLen, 132 | ) 133 | case "pbkdf2": 134 | if cryptoJSON.KDFParams.PRF != "hmac-sha256" { 135 | return nil, fmt.Errorf("unsupported PBKDF2 PRF: %s", cryptoJSON.KDFParams.PRF) 136 | } 137 | key := pbkdf2.Key( 138 | passphrase, 139 | cryptoJSON.KDFParams.Salt, 140 | cryptoJSON.KDFParams.C, 141 | cryptoJSON.KDFParams.DKLen, 142 | sha256.New, 143 | ) 144 | return key, nil 145 | } 146 | return nil, fmt.Errorf("unsupported KDF: %s", cryptoJSON.KDF) 147 | } 148 | 149 | // aesCTRXOR performs AES-128-CTR decryption on the given cipher text with the 150 | // given key and IV. 151 | func aesCTRXOR(key, inText, iv []byte) ([]byte, error) { 152 | aesBlock, err := aes.NewCipher(key) 153 | if err != nil { 154 | return nil, err 155 | } 156 | stream := cipher.NewCTR(aesBlock, iv) 157 | outText := make([]byte, len(inText)) 158 | stream.XORKeyStream(outText, inText) 159 | return outText, err 160 | } 161 | 162 | func randUUID() (string, error) { 163 | var uuid [16]byte 164 | var text [36]byte 165 | if _, err := io.ReadFull(rand.Reader, uuid[:]); err != nil { 166 | return "", err 167 | } 168 | uuid[6] = (uuid[6] & 0x0f) | 0x40 169 | uuid[8] = (uuid[8] & 0x3f) | 0x80 170 | hex.Encode(text[:8], uuid[:4]) 171 | text[8] = '-' 172 | hex.Encode(text[9:13], uuid[4:6]) 173 | text[13] = '-' 174 | hex.Encode(text[14:18], uuid[6:8]) 175 | text[18] = '-' 176 | hex.Encode(text[19:23], uuid[8:10]) 177 | text[23] = '-' 178 | hex.Encode(text[24:], uuid[10:]) 179 | return string(text[:]), nil 180 | } 181 | -------------------------------------------------------------------------------- /wallet/key_priv.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "crypto/rand" 7 | "encoding/json" 8 | 9 | "github.com/btcsuite/btcd/btcec/v2" 10 | 11 | "github.com/defiweb/go-eth/crypto" 12 | "github.com/defiweb/go-eth/types" 13 | ) 14 | 15 | var s256 = btcec.S256() 16 | 17 | type PrivateKey struct { 18 | private *ecdsa.PrivateKey 19 | public *ecdsa.PublicKey 20 | address types.Address 21 | sign crypto.Signer 22 | recover crypto.Recoverer 23 | } 24 | 25 | // NewKeyFromECDSA creates a new private key from an ecdsa.PrivateKey. 26 | func NewKeyFromECDSA(prv *ecdsa.PrivateKey) *PrivateKey { 27 | return &PrivateKey{ 28 | private: prv, 29 | public: &prv.PublicKey, 30 | address: crypto.ECPublicKeyToAddress(&prv.PublicKey), 31 | sign: crypto.ECSigner(prv), 32 | recover: crypto.ECRecoverer, 33 | } 34 | } 35 | 36 | // NewKeyFromBytes creates a new private key from private key bytes. 37 | func NewKeyFromBytes(prv []byte) *PrivateKey { 38 | key, _ := btcec.PrivKeyFromBytes(prv) 39 | return NewKeyFromECDSA(key.ToECDSA()) 40 | } 41 | 42 | // NewRandomKey creates a random private key. 43 | func NewRandomKey() *PrivateKey { 44 | key, err := ecdsa.GenerateKey(s256, rand.Reader) 45 | if err != nil { 46 | panic(err) 47 | } 48 | return NewKeyFromECDSA(key) 49 | } 50 | 51 | // PublicKey returns the ECDSA public key. 52 | func (k *PrivateKey) PublicKey() *ecdsa.PublicKey { 53 | return k.public 54 | } 55 | 56 | // PrivateKey returns the ECDSA private key. 57 | func (k *PrivateKey) PrivateKey() *ecdsa.PrivateKey { 58 | return k.private 59 | } 60 | 61 | // JSON returns the JSON representation of the private key. 62 | func (k *PrivateKey) JSON(passphrase string, scryptN, scryptP int) ([]byte, error) { 63 | key, err := encryptV3Key(k.private, passphrase, scryptN, scryptP) 64 | if err != nil { 65 | return nil, err 66 | } 67 | return json.Marshal(key) 68 | } 69 | 70 | // Address implements the Key interface. 71 | func (k *PrivateKey) Address() types.Address { 72 | return k.address 73 | } 74 | 75 | // SignHash implements the KeyWithHashSigner interface. 76 | func (k *PrivateKey) SignHash(_ context.Context, hash types.Hash) (*types.Signature, error) { 77 | return k.sign.SignHash(hash) 78 | } 79 | 80 | // SignMessage implements the Key interface. 81 | func (k *PrivateKey) SignMessage(_ context.Context, data []byte) (*types.Signature, error) { 82 | return k.sign.SignMessage(data) 83 | } 84 | 85 | // SignTransaction implements the Key interface. 86 | func (k *PrivateKey) SignTransaction(_ context.Context, tx *types.Transaction) error { 87 | return k.sign.SignTransaction(tx) 88 | } 89 | 90 | // VerifyHash implements the KeyWithHashSigner interface. 91 | func (k *PrivateKey) VerifyHash(_ context.Context, hash types.Hash, sig types.Signature) bool { 92 | addr, err := k.recover.RecoverHash(hash, sig) 93 | if err != nil { 94 | return false 95 | } 96 | return *addr == k.address 97 | } 98 | 99 | // VerifyMessage implements the Key interface. 100 | func (k *PrivateKey) VerifyMessage(_ context.Context, data []byte, sig types.Signature) bool { 101 | addr, err := k.recover.RecoverMessage(data, sig) 102 | if err != nil { 103 | return false 104 | } 105 | return *addr == k.address 106 | } 107 | -------------------------------------------------------------------------------- /wallet/key_rpc.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/defiweb/go-eth/crypto" 7 | "github.com/defiweb/go-eth/types" 8 | ) 9 | 10 | // RPCSigningClient is the interface for an Ethereum RPC client that can 11 | // sign messages and transactions. 12 | type RPCSigningClient interface { 13 | Sign(ctx context.Context, account types.Address, data []byte) (*types.Signature, error) 14 | SignTransaction(ctx context.Context, tx *types.Transaction) ([]byte, *types.Transaction, error) 15 | } 16 | 17 | // KeyRPC is an Ethereum key that uses an RPC client to sign messages and transactions. 18 | type KeyRPC struct { 19 | client RPCSigningClient 20 | address types.Address 21 | recover crypto.Recoverer 22 | } 23 | 24 | // NewKeyRPC returns a new KeyRPC. 25 | func NewKeyRPC(client RPCSigningClient, address types.Address) *KeyRPC { 26 | return &KeyRPC{ 27 | client: client, 28 | address: address, 29 | recover: crypto.ECRecoverer, 30 | } 31 | } 32 | 33 | // Address implements the Key interface. 34 | func (k *KeyRPC) Address() types.Address { 35 | return k.address 36 | } 37 | 38 | // SignMessage implements the Key interface. 39 | func (k *KeyRPC) SignMessage(ctx context.Context, data []byte) (*types.Signature, error) { 40 | return k.client.Sign(ctx, k.address, data) 41 | } 42 | 43 | // SignTransaction implements the Key interface. 44 | func (k *KeyRPC) SignTransaction(ctx context.Context, tx *types.Transaction) error { 45 | _, signedTX, err := k.client.SignTransaction(ctx, tx) 46 | if err != nil { 47 | return err 48 | } 49 | *tx = *signedTX 50 | return err 51 | } 52 | 53 | // VerifyMessage implements the Key interface. 54 | func (k *KeyRPC) VerifyMessage(_ context.Context, data []byte, sig types.Signature) bool { 55 | addr, err := k.recover.RecoverMessage(data, sig) 56 | if err != nil { 57 | return false 58 | } 59 | return *addr == k.address 60 | } 61 | -------------------------------------------------------------------------------- /wallet/testdata/pbkdf2.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0x008aeeda4d805471df9b2a5b0f38a0c3bcba786b", 3 | "crypto": { 4 | "cipher": "aes-128-ctr", 5 | "cipherparams": { 6 | "iv": "6087dab2f9fdbbfaddc31a909735c1e6" 7 | }, 8 | "ciphertext": "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", 9 | "kdf": "pbkdf2", 10 | "kdfparams": { 11 | "c": 262144, 12 | "dklen": 32, 13 | "prf": "hmac-sha256", 14 | "salt": "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd" 15 | }, 16 | "mac": "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2" 17 | }, 18 | "id": "3198bc9c-6672-5ab3-d995-4942343ae5b6", 19 | "version": 3 20 | } 21 | -------------------------------------------------------------------------------- /wallet/testdata/scrypt.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "2d800d93b065ce011af83f316cef9f0d005b0aa4", 3 | "crypto": { 4 | "cipher": "aes-128-ctr", 5 | "ciphertext": "8051dbab2d2415613751ee755d3b9a1f191c2fa15b4de9349848dcf44e656331", 6 | "cipherparams": { 7 | "iv": "4eb5e582782f64d18c58ddc56692fe91" 8 | }, 9 | "kdf": "scrypt", 10 | "kdfparams": { 11 | "dklen": 32, 12 | "n": 262144, 13 | "p": 1, 14 | "r": 8, 15 | "salt": "8b819d893ebda23c4b31e96dfd1a7f4514a5483840f28fb679197f0fa315ade4" 16 | }, 17 | "mac": "5ea8c70945a1c07f1121ab2798392158bf51eb356854040c8a8bfcb2a23ca5c7" 18 | }, 19 | "id": "53697e14-f0e4-4f87-b300-4163a61bc5ef", 20 | "version": 3 21 | } 22 | --------------------------------------------------------------------------------