diff --git a/accounts/abi/abi.go b/accounts/abi/abi.go index c7bc2b4541..83b3334e59 100644 --- a/accounts/abi/abi.go +++ b/accounts/abi/abi.go @@ -50,6 +50,7 @@ func JSON(reader io.Reader) (ABI, error) { var abi ABI if err := dec.Decode(&abi); err != nil { + fmt.Println(err) return ABI{}, err } return abi, nil diff --git a/accounts/abi/bind/lib.go b/accounts/abi/bind/lib.go index 7570665f87..6d6dd7b52e 100644 --- a/accounts/abi/bind/lib.go +++ b/accounts/abi/bind/lib.go @@ -28,6 +28,10 @@ type ContractInstance interface { Backend() ContractBackend } +type ContractInstanceV2 interface { + Address() common.Address +} + func CallRaw(instance ContractInstance, opts *CallOpts, input []byte) ([]byte, error) { backend := instance.Backend() c := NewBoundContract(instance.Address(), abi.ABI{}, backend, backend, backend) diff --git a/accounts/abi/bind/template2.go b/accounts/abi/bind/template2.go index f4acbd9b47..85f83605d9 100644 --- a/accounts/abi/bind/template2.go +++ b/accounts/abi/bind/template2.go @@ -55,22 +55,17 @@ var ( // {{.Type}}Instance represents a deployed instance of the {{.Type}} contract. type {{.Type}}Instance struct { {{.Type}} - address common.Address - backend bind.ContractBackend + address common.Address // consider removing this, not clear what it's used for now (and why did we need custom deploy method on previous abi?) } - func New{{.Type}}Instance(c *{{.Type}}, address common.Address, backend bind.ContractBackend) *{{.Type}}Instance { - return &{{.Type}}Instance{Db: *c, address: address, backend: backend} + func New{{.Type}}Instance(c *{{.Type}}, address common.Address) *{{.Type}}Instance { + return &{{.Type}}Instance{ {{$contract.Type}}: *c, address: address} } func (i *{{$contract.Type}}Instance) Address() common.Address { return i.address } - func (i *{{$contract.Type}}Instance) Backend() bind.ContractBackend { - return i.backend - } - // {{.Type}} is an auto generated Go binding around an Ethereum contract. type {{.Type}} struct { abi abi.ABI @@ -136,7 +131,7 @@ var ( Raw *types.Log // Blockchain specific contextual infos } - func (_{{$contract.Type}} *{{$contract.Type}}) {{.Normalized.Name}}EventID() common.Hash { + func {{$contract.Type}}{{.Normalized.Name}}EventID() common.Hash { return common.HexToHash("{{.Original.ID}}") } diff --git a/accounts/abi/bind/v2/backend.go b/accounts/abi/bind/v2/backend.go new file mode 100644 index 0000000000..73b420a27e --- /dev/null +++ b/accounts/abi/bind/v2/backend.go @@ -0,0 +1,20 @@ +package v2 + +import ( + "context" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "math/big" +) + +type V2Backend interface { + SuggestGasPrice(ctx context.Context) (*big.Int, error) + PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) + PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) + SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) + HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) + SendTransaction(ctx context.Context, tx *types.Transaction) error + SuggestGasTipCap(ctx context.Context) (*big.Int, error) + EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) +} diff --git a/accounts/abi/bind/v2/lib.go b/accounts/abi/bind/v2/lib.go index bd66202bef..9b350fb4a3 100644 --- a/accounts/abi/bind/v2/lib.go +++ b/accounts/abi/bind/v2/lib.go @@ -1,12 +1,16 @@ package v2 import ( + "context" + "errors" + "fmt" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/event" + "math/big" ) func FilterLogs[T any](instance bind.ContractInstance, opts *bind.FilterOpts, eventID common.Hash, unpack func(*types.Log) (*T, error), topics ...[]any) (*EventIterator[T], error) { @@ -19,10 +23,44 @@ func FilterLogs[T any](instance bind.ContractInstance, opts *bind.FilterOpts, ev return &EventIterator[T]{unpack: unpack, logs: logs, sub: sub}, nil } -func WatchLogs[T any](instance bind.ContractInstance, opts *bind.WatchOpts, eventID common.Hash, unpack func(*types.Log) (*T, error), sink chan<- *T, topics ...[]any) (event.Subscription, error) { - backend := instance.Backend() - c := bind.NewBoundContract(instance.Address(), abi.ABI{}, backend, backend, backend) - logs, sub, err := c.WatchLogs(opts, eventID.String(), topics...) +// WatchOpts is the collection of options to fine tune subscribing for events +// within a bound contract. +type WatchOpts struct { + Start *uint64 // Start of the queried range (nil = latest) + Context context.Context // Network context to support cancellation and timeouts (nil = no timeout) +} + +func watchLogs(backend V2Backend, address common.Address, opts *WatchOpts, eventID common.Hash, query ...[]interface{}) (chan types.Log, event.Subscription, error) { + // Don't crash on a lazy user + if opts == nil { + opts = new(WatchOpts) + } + // Append the event selector to the query parameters and construct the topic set + query = append([][]interface{}{{eventID}}, query...) + + topics, err := abi.MakeTopics(query...) + if err != nil { + return nil, nil, err + } + // Start the background filtering + logs := make(chan types.Log, 128) + + config := ethereum.FilterQuery{ + Addresses: []common.Address{address}, + Topics: topics, + } + if opts.Start != nil { + config.FromBlock = new(big.Int).SetUint64(*opts.Start) + } + sub, err := backend.SubscribeFilterLogs(ensureContext(opts.Context), config, logs) + if err != nil { + return nil, nil, err + } + return logs, sub, nil +} + +func WatchLogs[T any](address common.Address, backend V2Backend, opts *WatchOpts, eventID common.Hash, unpack func(*types.Log) (*T, error), sink chan<- *T, topics ...[]any) (event.Subscription, error) { + logs, sub, err := watchLogs(backend, address, opts, eventID, topics...) if err != nil { return nil, err } @@ -128,6 +166,165 @@ func Transact(instance bind.ContractInstance, opts *bind.TransactOpts, input []b return c.RawTransact(opts, input) } +// ensureContext is a helper method to ensure a context is not nil, even if the +// user specified it as such. +func ensureContext(ctx context.Context) context.Context { + if ctx == nil { + return context.Background() + } + return ctx +} + +// SignerFn is a signer function callback when a contract requires a method to +// sign the transaction before submission. +type SignerFn func(common.Address, *types.Transaction) (*types.Transaction, error) + +// TransactOpts is the collection of authorization data required to create a +// valid Ethereum transaction. +type TransactOpts struct { + From common.Address // Ethereum account to send the transaction from + Nonce *big.Int // Nonce to use for the transaction execution (nil = use pending state) + Signer SignerFn // Method to use for signing the transaction (mandatory) + + Value *big.Int // Funds to transfer along the transaction (nil = 0 = no funds) + GasPrice *big.Int // Gas price to use for the transaction execution (nil = gas price oracle) + GasFeeCap *big.Int // Gas fee cap to use for the 1559 transaction execution (nil = gas price oracle) + GasTipCap *big.Int // Gas priority fee cap to use for the 1559 transaction execution (nil = gas price oracle) + GasLimit uint64 // Gas limit to set for the transaction execution (0 = estimate) + AccessList types.AccessList // Access list to set for the transaction execution (nil = no access list) + + Context context.Context // Network context to support cancellation and timeouts (nil = no timeout) + + NoSend bool // Do all transact steps but do not send the transaction +} + +func estimateGasLimit(backend V2Backend, address common.Hash, opts *TransactOpts, contract *common.Address, input []byte, gasPrice, gasTipCap, gasFeeCap, value *big.Int) (uint64, error) { + if contract != nil { + // Gas estimation cannot succeed without code for method invocations. + if code, err := backend.PendingCodeAt(ensureContext(opts.Context), address); err != nil { + return 0, err + } else if len(code) == 0 { + return 0, ErrNoCode + } + } + msg := ethereum.CallMsg{ + From: opts.From, + To: contract, + GasPrice: gasPrice, + GasTipCap: gasTipCap, + GasFeeCap: gasFeeCap, + Value: value, + Data: input, + } + return backend.EstimateGas(ensureContext(opts.Context), msg) +} + +func getNonce(backend V2Backend, opts *TransactOpts) (uint64, error) { + if opts.Nonce == nil { + return backend.PendingNonceAt(ensureContext(opts.Context), opts.From) + } else { + return opts.Nonce.Uint64(), nil + } +} + +func createLegacyTx(backend V2Backend, address common.Hash, opts *TransactOpts, contract *common.Address, input []byte) (*types.Transaction, error) { + if opts.GasFeeCap != nil || opts.GasTipCap != nil || opts.AccessList != nil { + return nil, errors.New("maxFeePerGas or maxPriorityFeePerGas or accessList specified but london is not active yet") + } + // Normalize value + value := opts.Value + if value == nil { + value = new(big.Int) + } + // Estimate GasPrice + gasPrice := opts.GasPrice + if gasPrice == nil { + price, err := backend.SuggestGasPrice(ensureContext(opts.Context)) + if err != nil { + return nil, err + } + gasPrice = price + } + // Estimate GasLimit + gasLimit := opts.GasLimit + if opts.GasLimit == 0 { + var err error + gasLimit, err = estimateGasLimit(backend, address, opts, contract, input, gasPrice, nil, nil, value) + if err != nil { + return nil, err + } + } + // create the transaction + nonce, err := getNonce(backend, opts) + if err != nil { + return nil, err + } + baseTx := &types.LegacyTx{ + To: contract, + Nonce: nonce, + GasPrice: gasPrice, + Gas: gasLimit, + Value: value, + Data: input, + } + return types.NewTx(baseTx), nil +} + +const basefeeWiggleMultiplier = 2 + +func createDynamicTx(backend V2Backend, opts *TransactOpts, contract *common.Address, input []byte, head *types.Header) (*types.Transaction, error) { + // Normalize value + value := opts.Value + if value == nil { + value = new(big.Int) + } + // Estimate TipCap + gasTipCap := opts.GasTipCap + if gasTipCap == nil { + tip, err := backend.SuggestGasTipCap(ensureContext(opts.Context)) + if err != nil { + return nil, err + } + gasTipCap = tip + } + // Estimate FeeCap + gasFeeCap := opts.GasFeeCap + if gasFeeCap == nil { + gasFeeCap = new(big.Int).Add( + gasTipCap, + new(big.Int).Mul(head.BaseFee, big.NewInt(basefeeWiggleMultiplier)), + ) + } + if gasFeeCap.Cmp(gasTipCap) < 0 { + return nil, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", gasFeeCap, gasTipCap) + } + // Estimate GasLimit + gasLimit := opts.GasLimit + if opts.GasLimit == 0 { + var err error + gasLimit, err = c.estimateGasLimit(opts, contract, input, nil, gasTipCap, gasFeeCap, value) + if err != nil { + return nil, err + } + } + // create the transaction + nonce, err := c.getNonce(opts) + if err != nil { + return nil, err + } + baseTx := &types.DynamicFeeTx{ + To: contract, + Nonce: nonce, + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, + Gas: gasLimit, + Value: value, + Data: input, + AccessList: opts.AccessList, + } + return types.NewTx(baseTx), nil +} + func Transfer(instance bind.ContractInstance, opts *bind.TransactOpts) (*types.Transaction, error) { backend := instance.Backend() c := bind.NewBoundContract(instance.Address(), abi.ABI{}, backend, backend, backend) diff --git a/accounts/abi/bind/v2/v2_test.go b/accounts/abi/bind/v2/v2_test.go index a6d16806f7..c232bd0cff 100644 --- a/accounts/abi/bind/v2/v2_test.go +++ b/accounts/abi/bind/v2/v2_test.go @@ -4,6 +4,9 @@ import ( "context" "encoding/json" "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" + "github.com/ethereum/go-ethereum/accounts/abi/bind/testdata/v2_generated_testcase" + "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/ethereum/go-ethereum/node" "io" "github.com/ethereum/go-ethereum/accounts/abi" @@ -18,65 +21,6 @@ import ( "testing" ) -const deployer = "6080604052348015600e575f80fd5b506102098061001c5f395ff3fe608060405234801561000f575f80fd5b506004361061003f575f3560e01c80636da1cd55146100435780637b0cb83914610061578063bf54fad41461006b575b5f80fd5b61004b610087565b60405161005891906100e9565b60405180910390f35b61006961008f565b005b61008560048036038101906100809190610130565b6100c8565b005b5f8054905090565b607b7f72c79b1cb25b1b49ae522446226e1591b80634619cef7e71846da52b61b7061d6040516100be906101b5565b60405180910390a2565b805f8190555050565b5f819050919050565b6100e3816100d1565b82525050565b5f6020820190506100fc5f8301846100da565b92915050565b5f80fd5b61010f816100d1565b8114610119575f80fd5b50565b5f8135905061012a81610106565b92915050565b5f6020828403121561014557610144610102565b5b5f6101528482850161011c565b91505092915050565b5f82825260208201905092915050565b7f737472696e6700000000000000000000000000000000000000000000000000005f82015250565b5f61019f60068361015b565b91506101aa8261016b565b602082019050919050565b5f6020820190508181035f8301526101cc81610193565b905091905056fea2646970667358221220212a3a765a98254b596386fdfd10318f9a4bf19e8c9ca9ffa363f990c1798bf664736f6c634300081a0033" - -const contractABIStr = ` -[ - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "firstArg", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "string", - "name": "secondArg", - "type": "string" - } - ], - "name": "ExampleEvent", - "type": "event" - }, - { - "inputs": [], - "name": "emitEvent", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "num", - "type": "uint256" - } - ], - "name": "mutateStorageVal", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "retrieveStorageVal", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - } -] -` - var testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") // JSON returns a parsed ABI interface and error if it failed. @@ -96,10 +40,13 @@ func TestV2(t *testing.T) { types.GenesisAlloc{ testAddr: {Balance: big.NewInt(10000000000000000)}, }, + func(nodeConf *node.Config, ethConf *ethconfig.Config) { + ethConf.Genesis.Difficulty = big.NewInt(0) + }, ) defer backend.Close() - contractABI, err := JSON(strings.NewReader(contractABIStr)) + contractABI, err := JSON(strings.NewReader(v2_generated_testcase.V2GeneratedTestcaseMetaData.ABI)) if err != nil { panic(err) } @@ -120,15 +67,6 @@ func TestV2(t *testing.T) { return signedTx, nil }, Context: context.Background(), - /* - Value: nil, - GasPrice: nil, - GasFeeCap: nil, - GasTipCap: nil, - GasLimit: 0, - AccessList: nil, - NoSend: false, - */ } // we should just be able to use the backend directly, instead of using // this deprecated interface. However, the simulated backend no longer @@ -137,9 +75,28 @@ func TestV2(t *testing.T) { Backend: backend, Client: backend.Client(), } - _, _, _, err = bind.DeployContract(&opts, contractABI, common.Hex2Bytes(deployer), &bindBackend) + address, _, boundContract, err := bind.DeployContract(&opts, contractABI, common.Hex2Bytes(v2_generated_testcase.V2GeneratedTestcaseMetaData.Bin), &bindBackend) if err != nil { t.Fatal(err) } + contract, err := v2_generated_testcase.NewV2GeneratedTestcase() + if err != nil { + t.Fatal(err) // can't happen here with the example used. consider removing this block + } + contractInstance := v2_generated_testcase.NewV2GeneratedTestcaseInstance(contract, address) + sinkCh := make(chan *v2_generated_testcase.V2GeneratedTestcase) + // q: what extra functionality is given by specifying this as a custom method, instead of catching emited methods + // from the sync channel? + unpackStruct := func(log *types.Log) (v2_generated_testcase.V2GeneratedTestcaseStruct, error) { + res, err := contract.UnpackStructEvent(log) + return *res, err + } + // TODO: test using various topics + // q: does nil topics mean to accept any? + sub, err := WatchLogs[v2_generated_testcase.V2GeneratedTestcaseStruct](contractInstance, v2_generated_testcase.V2GeneratedTestcaseStructEventID(), unpackStruct, sinkCh) + if err != nil { + t.Fatal(err) + } + defer sub.Unsubscribe() // send a balance to our contract (contract must accept ether by default) }