diff --git a/accounts/abi/bind/old.go b/accounts/abi/bind/old.go index 5fe7bb13d8..016c1dd5d1 100644 --- a/accounts/abi/bind/old.go +++ b/accounts/abi/bind/old.go @@ -220,10 +220,10 @@ type FilterOpts = bind2.FilterOpts type WatchOpts = bind2.WatchOpts -type BoundContract = bind2.BoundContract +type BoundContract = bind2.BoundContractV1 func NewBoundContract(address common.Address, abi abi.ABI, caller ContractCaller, transactor ContractTransactor, filterer ContractFilterer) *BoundContract { - return bind2.NewBoundContract(address, abi, caller, transactor, filterer) + return bind2.NewBoundContractV1(address, abi, caller, transactor, filterer) } func DeployContract(opts *TransactOpts, abi abi.ABI, bytecode []byte, backend ContractBackend, params ...interface{}) (common.Address, *types.Transaction, *BoundContract, error) { diff --git a/accounts/abi/bind/v2/base.go b/accounts/abi/bind/v2/base.go index 9d9a1e4aea..4c04c9e49e 100644 --- a/accounts/abi/bind/v2/base.go +++ b/accounts/abi/bind/v2/base.go @@ -19,17 +19,13 @@ package bind import ( "context" "errors" - "fmt" "math/big" "strings" "sync" - "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/event" ) const basefeeWiggleMultiplier = 2 @@ -129,467 +125,17 @@ func (m *MetaData) ParseABI() (*abi.ABI, error) { // Ethereum network. It contains a collection of methods that are used by the // higher level contract bindings to operate. type BoundContract struct { - address common.Address // Deployment address of the contract on the Ethereum blockchain - abi abi.ABI // Reflect based ABI to access the correct Ethereum methods - caller ContractCaller // Read interface to interact with the blockchain - transactor ContractTransactor // Write interface to interact with the blockchain - filterer ContractFilterer // Event filtering to interact with the blockchain + address common.Address // Deployment address of the contract on the Ethereum blockchain + abi abi.ABI // Reflect based ABI to access the correct Ethereum methods + backend ContractBackend } // NewBoundContract creates a low level contract interface through which calls // and transactions may be made through. -func NewBoundContract(address common.Address, abi abi.ABI, caller ContractCaller, transactor ContractTransactor, filterer ContractFilterer) *BoundContract { +func NewBoundContract(backend ContractBackend, address common.Address, abi abi.ABI) *BoundContract { return &BoundContract{ - address: address, - abi: abi, - caller: caller, - transactor: transactor, - filterer: filterer, + address, + abi, + backend, } } - -// DeployContract deploys a contract onto the Ethereum blockchain and binds the -// deployment address with a Go wrapper. -func DeployContract(opts *TransactOpts, abi abi.ABI, bytecode []byte, backend ContractBackend, params ...any) (common.Address, *types.Transaction, *BoundContract, error) { - // Otherwise try to deploy the contract - c := NewBoundContract(common.Address{}, abi, backend, backend, backend) - - input, err := c.abi.Pack("", params...) - if err != nil { - return common.Address{}, nil, nil, err - } - tx, err := c.transact(opts, nil, append(bytecode, input...)) - if err != nil { - return common.Address{}, nil, nil, err - } - c.address = crypto.CreateAddress(opts.From, tx.Nonce()) - return c.address, tx, c, nil -} - -// Call invokes the (constant) contract method with params as input values and -// sets the output to result. The result type might be a single field for simple -// returns, a slice of interfaces for anonymous returns and a struct for named -// returns. -func (c *BoundContract) Call(opts *CallOpts, results *[]any, method string, params ...any) error { - if results == nil { - results = new([]any) - } - // Pack the input, call and unpack the results - input, err := c.abi.Pack(method, params...) - if err != nil { - return err - } - - output, err := c.call(opts, input) - if err != nil { - return err - } - - if len(*results) == 0 { - res, err := c.abi.Unpack(method, output) - *results = res - return err - } - res := *results - return c.abi.UnpackIntoInterface(res[0], method, output) -} - -// CallRaw executes an eth_call against the contract with the raw calldata as -// input. It returns the call's return data or an error. -func (c *BoundContract) CallRaw(opts *CallOpts, input []byte) ([]byte, error) { - return c.call(opts, input) -} - -func (c *BoundContract) call(opts *CallOpts, input []byte) ([]byte, error) { - // Don't crash on a lazy user - if opts == nil { - opts = new(CallOpts) - } - var ( - msg = ethereum.CallMsg{From: opts.From, To: &c.address, Data: input} - ctx = ensureContext(opts.Context) - code []byte - output []byte - err error - ) - if opts.Pending { - pb, ok := c.caller.(PendingContractCaller) - if !ok { - return nil, ErrNoPendingState - } - output, err = pb.PendingCallContract(ctx, msg) - if err != nil { - return nil, err - } - if len(output) == 0 { - // Make sure we have a contract to operate on, and bail out otherwise. - if code, err = pb.PendingCodeAt(ctx, c.address); err != nil { - return nil, err - } else if len(code) == 0 { - return nil, ErrNoCode - } - } - } else if opts.BlockHash != (common.Hash{}) { - bh, ok := c.caller.(BlockHashContractCaller) - if !ok { - return nil, ErrNoBlockHashState - } - output, err = bh.CallContractAtHash(ctx, msg, opts.BlockHash) - if err != nil { - return nil, err - } - if len(output) == 0 { - // Make sure we have a contract to operate on, and bail out otherwise. - if code, err = bh.CodeAtHash(ctx, c.address, opts.BlockHash); err != nil { - return nil, err - } else if len(code) == 0 { - return nil, ErrNoCode - } - } - } else { - output, err = c.caller.CallContract(ctx, msg, opts.BlockNumber) - if err != nil { - return nil, err - } - if len(output) == 0 { - // Make sure we have a contract to operate on, and bail out otherwise. - if code, err = c.caller.CodeAt(ctx, c.address, opts.BlockNumber); err != nil { - return nil, err - } else if len(code) == 0 { - return nil, ErrNoCode - } - } - } - return output, nil -} - -// Transact invokes the (paid) contract method with params as input values. -func (c *BoundContract) Transact(opts *TransactOpts, method string, params ...any) (*types.Transaction, error) { - // Otherwise pack up the parameters and invoke the contract - input, err := c.abi.Pack(method, params...) - if err != nil { - return nil, err - } - return c.transact(opts, &c.address, input) -} - -// RawTransact initiates a transaction with the given raw calldata as the input. -// It's usually used to initiate transactions for invoking **Fallback** function. -func (c *BoundContract) RawTransact(opts *TransactOpts, calldata []byte) (*types.Transaction, error) { - return c.transact(opts, &c.address, calldata) -} - -// RawTransact initiates a contract-creation transaction with the given raw calldata as the input. -func (c *BoundContract) RawCreationTransact(opts *TransactOpts, calldata []byte) (*types.Transaction, error) { - return c.transact(opts, nil, calldata) -} - -// Transfer initiates a plain transaction to move funds to the contract, calling -// its default method if one is available. -func (c *BoundContract) Transfer(opts *TransactOpts) (*types.Transaction, error) { - // todo(rjl493456442) check the payable fallback or receive is defined - // or not, reject invalid transaction at the first place - return c.transact(opts, &c.address, nil) -} - -func (c *BoundContract) createDynamicTx(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 := c.transactor.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 (c *BoundContract) createLegacyTx(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 := c.transactor.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 = c.estimateGasLimit(opts, contract, input, gasPrice, nil, nil, value) - if err != nil { - return nil, err - } - } - // create the transaction - nonce, err := c.getNonce(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 -} - -func (c *BoundContract) estimateGasLimit(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 := c.transactor.PendingCodeAt(ensureContext(opts.Context), c.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 c.transactor.EstimateGas(ensureContext(opts.Context), msg) -} - -func (c *BoundContract) getNonce(opts *TransactOpts) (uint64, error) { - if opts.Nonce == nil { - return c.transactor.PendingNonceAt(ensureContext(opts.Context), opts.From) - } else { - return opts.Nonce.Uint64(), nil - } -} - -// transact executes an actual transaction invocation, first deriving any missing -// authorization fields, and then scheduling the transaction for execution. -func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, input []byte) (*types.Transaction, error) { - if opts.GasPrice != nil && (opts.GasFeeCap != nil || opts.GasTipCap != nil) { - return nil, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") - } - // Create the transaction - var ( - rawTx *types.Transaction - err error - ) - if opts.GasPrice != nil { - rawTx, err = c.createLegacyTx(opts, contract, input) - } else if opts.GasFeeCap != nil && opts.GasTipCap != nil { - rawTx, err = c.createDynamicTx(opts, contract, input, nil) - } else { - // Only query for basefee if gasPrice not specified - if head, errHead := c.transactor.HeaderByNumber(ensureContext(opts.Context), nil); errHead != nil { - return nil, errHead - } else if head.BaseFee != nil { - rawTx, err = c.createDynamicTx(opts, contract, input, head) - } else { - // Chain is not London ready -> use legacy transaction - rawTx, err = c.createLegacyTx(opts, contract, input) - } - } - if err != nil { - return nil, err - } - // Sign the transaction and schedule it for execution - if opts.Signer == nil { - return nil, errors.New("no signer to authorize the transaction with") - } - signedTx, err := opts.Signer(opts.From, rawTx) - if err != nil { - return nil, err - } - if opts.NoSend { - return signedTx, nil - } - if err := c.transactor.SendTransaction(ensureContext(opts.Context), signedTx); err != nil { - return nil, err - } - return signedTx, nil -} - -// FilterLogs filters contract logs for past blocks, returning the necessary -// channels to construct a strongly typed bound iterator on top of them. -func (c *BoundContract) FilterLogs(opts *FilterOpts, name string, query ...[]any) (chan types.Log, event.Subscription, error) { - // Don't crash on a lazy user - if opts == nil { - opts = new(FilterOpts) - } - // Append the event selector to the query parameters and construct the topic set - query = append([][]any{{c.abi.Events[name].ID}}, 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{c.address}, - Topics: topics, - FromBlock: new(big.Int).SetUint64(opts.Start), - } - if opts.End != nil { - config.ToBlock = new(big.Int).SetUint64(*opts.End) - } - /* TODO(karalabe): Replace the rest of the method below with this when supported - sub, err := c.filterer.SubscribeFilterLogs(ensureContext(opts.Context), config, logs) - */ - buff, err := c.filterer.FilterLogs(ensureContext(opts.Context), config) - if err != nil { - return nil, nil, err - } - sub := event.NewSubscription(func(quit <-chan struct{}) error { - for _, log := range buff { - select { - case logs <- log: - case <-quit: - return nil - } - } - return nil - }) - - return logs, sub, nil -} - -// WatchLogs filters subscribes to contract logs for future blocks, returning a -// subscription object that can be used to tear down the watcher. -func (c *BoundContract) WatchLogs(opts *WatchOpts, name string, query ...[]any) (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([][]any{{c.abi.Events[name].ID}}, 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{c.address}, - Topics: topics, - } - if opts.Start != nil { - config.FromBlock = new(big.Int).SetUint64(*opts.Start) - } - sub, err := c.filterer.SubscribeFilterLogs(ensureContext(opts.Context), config, logs) - if err != nil { - return nil, nil, err - } - return logs, sub, nil -} - -// UnpackLog unpacks a retrieved log into the provided output structure. -func (c *BoundContract) UnpackLog(out any, event string, log types.Log) error { - // Anonymous events are not supported. - if len(log.Topics) == 0 { - return errNoEventSignature - } - if log.Topics[0] != c.abi.Events[event].ID { - return errEventSignatureMismatch - } - if len(log.Data) > 0 { - if err := c.abi.UnpackIntoInterface(out, event, log.Data); err != nil { - return err - } - } - var indexed abi.Arguments - for _, arg := range c.abi.Events[event].Inputs { - if arg.Indexed { - indexed = append(indexed, arg) - } - } - return abi.ParseTopics(out, indexed, log.Topics[1:]) -} - -// UnpackLogIntoMap unpacks a retrieved log into the provided map. -func (c *BoundContract) UnpackLogIntoMap(out map[string]any, event string, log types.Log) error { - // Anonymous events are not supported. - if len(log.Topics) == 0 { - return errNoEventSignature - } - if log.Topics[0] != c.abi.Events[event].ID { - return errEventSignatureMismatch - } - if len(log.Data) > 0 { - if err := c.abi.UnpackIntoMap(out, event, log.Data); err != nil { - return err - } - } - var indexed abi.Arguments - for _, arg := range c.abi.Events[event].Inputs { - if arg.Indexed { - indexed = append(indexed, arg) - } - } - return abi.ParseTopicsIntoMap(out, indexed, log.Topics[1:]) -} - -// 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 -} diff --git a/accounts/abi/bind/v2/base_test.go b/accounts/abi/bind/v2/base_test.go index 80d0f22f2c..6e12323645 100644 --- a/accounts/abi/bind/v2/base_test.go +++ b/accounts/abi/bind/v2/base_test.go @@ -142,7 +142,7 @@ func TestPassingBlockNumber(t *testing.T) { }, } - bc := bind.NewBoundContract(common.HexToAddress("0x0"), abi.ABI{ + bc := bind.NewBoundContractV1(common.HexToAddress("0x0"), abi.ABI{ Methods: map[string]abi.Method{ "something": { Name: "something", @@ -197,7 +197,7 @@ func TestUnpackIndexedStringTyLogIntoMap(t *testing.T) { abiString := `[{"anonymous":false,"inputs":[{"indexed":true,"name":"name","type":"string"},{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"received","type":"event"}]` parsedAbi, _ := abi.JSON(strings.NewReader(abiString)) - bc := bind.NewBoundContract(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) + bc := bind.NewBoundContractV1(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) expectedReceivedMap := map[string]interface{}{ "name": hash, @@ -214,7 +214,7 @@ func TestUnpackAnonymousLogIntoMap(t *testing.T) { abiString := `[{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"received","type":"event"}]` parsedAbi, _ := abi.JSON(strings.NewReader(abiString)) - bc := bind.NewBoundContract(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) + bc := bind.NewBoundContractV1(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) var received map[string]interface{} err := bc.UnpackLogIntoMap(received, "received", mockLog) @@ -241,7 +241,7 @@ func TestUnpackIndexedSliceTyLogIntoMap(t *testing.T) { abiString := `[{"anonymous":false,"inputs":[{"indexed":true,"name":"names","type":"string[]"},{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"received","type":"event"}]` parsedAbi, _ := abi.JSON(strings.NewReader(abiString)) - bc := bind.NewBoundContract(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) + bc := bind.NewBoundContractV1(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) expectedReceivedMap := map[string]interface{}{ "names": hash, @@ -267,7 +267,7 @@ func TestUnpackIndexedArrayTyLogIntoMap(t *testing.T) { abiString := `[{"anonymous":false,"inputs":[{"indexed":true,"name":"addresses","type":"address[2]"},{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"received","type":"event"}]` parsedAbi, _ := abi.JSON(strings.NewReader(abiString)) - bc := bind.NewBoundContract(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) + bc := bind.NewBoundContractV1(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) expectedReceivedMap := map[string]interface{}{ "addresses": hash, @@ -294,7 +294,7 @@ func TestUnpackIndexedFuncTyLogIntoMap(t *testing.T) { mockLog := newMockLog(topics, common.HexToHash("0x5c698f13940a2153440c6d19660878bc90219d9298fdcf37365aa8d88d40fc42")) abiString := `[{"anonymous":false,"inputs":[{"indexed":true,"name":"function","type":"function"},{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"received","type":"event"}]` parsedAbi, _ := abi.JSON(strings.NewReader(abiString)) - bc := bind.NewBoundContract(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) + bc := bind.NewBoundContractV1(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) expectedReceivedMap := map[string]interface{}{ "function": functionTy, @@ -317,7 +317,7 @@ func TestUnpackIndexedBytesTyLogIntoMap(t *testing.T) { abiString := `[{"anonymous":false,"inputs":[{"indexed":true,"name":"content","type":"bytes"},{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"received","type":"event"}]` parsedAbi, _ := abi.JSON(strings.NewReader(abiString)) - bc := bind.NewBoundContract(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) + bc := bind.NewBoundContractV1(common.HexToAddress("0x0"), parsedAbi, nil, nil, nil) expectedReceivedMap := map[string]interface{}{ "content": hash, @@ -335,7 +335,7 @@ func TestTransactGasFee(t *testing.T) { // GasTipCap and GasFeeCap // When opts.GasTipCap and opts.GasFeeCap are nil mt := &mockTransactor{baseFee: big.NewInt(100), gasTipCap: big.NewInt(5)} - bc := bind.NewBoundContract(common.Address{}, abi.ABI{}, nil, mt, nil) + bc := bind.NewBoundContractV1(common.Address{}, abi.ABI{}, nil, mt, nil) opts := &bind.TransactOpts{Signer: mockSign} tx, err := bc.Transact(opts, "") assert.Nil(err) @@ -357,7 +357,7 @@ func TestTransactGasFee(t *testing.T) { // GasPrice // When opts.GasPrice is nil mt = &mockTransactor{gasPrice: big.NewInt(5)} - bc = bind.NewBoundContract(common.Address{}, abi.ABI{}, nil, mt, nil) + bc = bind.NewBoundContractV1(common.Address{}, abi.ABI{}, nil, mt, nil) opts = &bind.TransactOpts{Signer: mockSign} tx, err = bc.Transact(opts, "") assert.Nil(err) @@ -374,7 +374,7 @@ func TestTransactGasFee(t *testing.T) { assert.True(mt.suggestGasPriceCalled) } -func unpackAndCheck(t *testing.T, bc *bind.BoundContract, expected map[string]interface{}, mockLog types.Log) { +func unpackAndCheck(t *testing.T, bc *bind.BoundContractV1, expected map[string]interface{}, mockLog types.Log) { received := make(map[string]interface{}) if err := bc.UnpackLogIntoMap(received, "received", mockLog); err != nil { t.Error(err) @@ -551,7 +551,7 @@ func TestCall(t *testing.T) { wantErr: true, }} for _, test := range tests { - bc := bind.NewBoundContract(common.HexToAddress("0x0"), abi.ABI{ + bc := bind.NewBoundContractV1(common.HexToAddress("0x0"), abi.ABI{ Methods: map[string]abi.Method{ method: { Name: method, diff --git a/accounts/abi/bind/v2/boundcontractv1.go b/accounts/abi/bind/v2/boundcontractv1.go new file mode 100644 index 0000000000..29ca2d7b7b --- /dev/null +++ b/accounts/abi/bind/v2/boundcontractv1.go @@ -0,0 +1,484 @@ +package bind + +import ( + "context" + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/event" +) + +// BoundContractV1 is the base wrapper object that reflects a contract on the +// Ethereum network. It contains a collection of methods that are used by the +// higher level contract bindings to operate. +type BoundContractV1 struct { + address common.Address // Deployment address of the contract on the Ethereum blockchain + abi abi.ABI // Reflect based ABI to access the correct Ethereum methods + caller ContractCaller // Read interface to interact with the blockchain + transactor ContractTransactor // Write interface to interact with the blockchain + filterer ContractFilterer // Event filtering to interact with the blockchain +} + +// NewBoundContractV1 creates a low level contract interface through which calls +// and transactions may be made through. +func NewBoundContractV1(address common.Address, abi abi.ABI, caller ContractCaller, transactor ContractTransactor, filterer ContractFilterer) *BoundContractV1 { + return &BoundContractV1{ + address: address, + abi: abi, + caller: caller, + transactor: transactor, + filterer: filterer, + } +} + +// DeployContract deploys a contract onto the Ethereum blockchain and binds the +// deployment address with a Go wrapper. +func DeployContract(opts *TransactOpts, abi abi.ABI, bytecode []byte, backend ContractBackend, params ...any) (common.Address, *types.Transaction, *BoundContractV1, error) { + // Otherwise try to deploy the contract + c := NewBoundContractV1(common.Address{}, abi, backend, backend, backend) + + input, err := c.abi.Pack("", params...) + if err != nil { + return common.Address{}, nil, nil, err + } + tx, err := c.transact(opts, nil, append(bytecode, input...)) + if err != nil { + return common.Address{}, nil, nil, err + } + c.address = crypto.CreateAddress(opts.From, tx.Nonce()) + return c.address, tx, c, nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (c *BoundContractV1) Call(opts *CallOpts, results *[]any, method string, params ...any) error { + if results == nil { + results = new([]any) + } + // Pack the input, call and unpack the results + input, err := c.abi.Pack(method, params...) + if err != nil { + return err + } + + output, err := c.call(opts, input) + if err != nil { + return err + } + + if len(*results) == 0 { + res, err := c.abi.Unpack(method, output) + *results = res + return err + } + res := *results + return c.abi.UnpackIntoInterface(res[0], method, output) +} + +// CallRaw executes an eth_call against the contract with the raw calldata as +// input. It returns the call's return data or an error. +func (c *BoundContractV1) CallRaw(opts *CallOpts, input []byte) ([]byte, error) { + return c.call(opts, input) +} + +func (c *BoundContractV1) call(opts *CallOpts, input []byte) ([]byte, error) { + // Don't crash on a lazy user + if opts == nil { + opts = new(CallOpts) + } + var ( + msg = ethereum.CallMsg{From: opts.From, To: &c.address, Data: input} + ctx = ensureContext(opts.Context) + code []byte + output []byte + err error + ) + if opts.Pending { + pb, ok := c.caller.(PendingContractCaller) + if !ok { + return nil, ErrNoPendingState + } + output, err = pb.PendingCallContract(ctx, msg) + if err != nil { + return nil, err + } + if len(output) == 0 { + // Make sure we have a contract to operate on, and bail out otherwise. + if code, err = pb.PendingCodeAt(ctx, c.address); err != nil { + return nil, err + } else if len(code) == 0 { + return nil, ErrNoCode + } + } + } else if opts.BlockHash != (common.Hash{}) { + bh, ok := c.caller.(BlockHashContractCaller) + if !ok { + return nil, ErrNoBlockHashState + } + output, err = bh.CallContractAtHash(ctx, msg, opts.BlockHash) + if err != nil { + return nil, err + } + if len(output) == 0 { + // Make sure we have a contract to operate on, and bail out otherwise. + if code, err = bh.CodeAtHash(ctx, c.address, opts.BlockHash); err != nil { + return nil, err + } else if len(code) == 0 { + return nil, ErrNoCode + } + } + } else { + output, err = c.caller.CallContract(ctx, msg, opts.BlockNumber) + if err != nil { + return nil, err + } + if len(output) == 0 { + // Make sure we have a contract to operate on, and bail out otherwise. + if code, err = c.caller.CodeAt(ctx, c.address, opts.BlockNumber); err != nil { + return nil, err + } else if len(code) == 0 { + return nil, ErrNoCode + } + } + } + return output, nil +} + +// Transact invokes the (paid) contract method with params as input values. +func (c *BoundContractV1) Transact(opts *TransactOpts, method string, params ...any) (*types.Transaction, error) { + // Otherwise pack up the parameters and invoke the contract + input, err := c.abi.Pack(method, params...) + if err != nil { + return nil, err + } + return c.transact(opts, &c.address, input) +} + +// RawTransact initiates a transaction with the given raw calldata as the input. +// It's usually used to initiate transactions for invoking **Fallback** function. +func (c *BoundContractV1) RawTransact(opts *TransactOpts, calldata []byte) (*types.Transaction, error) { + return c.transact(opts, &c.address, calldata) +} + +// RawTransact initiates a contract-creation transaction with the given raw calldata as the input. +func (c *BoundContractV1) RawCreationTransact(opts *TransactOpts, calldata []byte) (*types.Transaction, error) { + return c.transact(opts, nil, calldata) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (c *BoundContractV1) Transfer(opts *TransactOpts) (*types.Transaction, error) { + // todo(rjl493456442) check the payable fallback or receive is defined + // or not, reject invalid transaction at the first place + return c.transact(opts, &c.address, nil) +} + +func (c *BoundContractV1) createDynamicTx(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 := c.transactor.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 (c *BoundContractV1) createLegacyTx(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 := c.transactor.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 = c.estimateGasLimit(opts, contract, input, gasPrice, nil, nil, value) + if err != nil { + return nil, err + } + } + // create the transaction + nonce, err := c.getNonce(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 +} + +func (c *BoundContractV1) estimateGasLimit(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 := c.transactor.PendingCodeAt(ensureContext(opts.Context), c.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 c.transactor.EstimateGas(ensureContext(opts.Context), msg) +} + +func (c *BoundContractV1) getNonce(opts *TransactOpts) (uint64, error) { + if opts.Nonce == nil { + return c.transactor.PendingNonceAt(ensureContext(opts.Context), opts.From) + } else { + return opts.Nonce.Uint64(), nil + } +} + +// transact executes an actual transaction invocation, first deriving any missing +// authorization fields, and then scheduling the transaction for execution. +func (c *BoundContractV1) transact(opts *TransactOpts, contract *common.Address, input []byte) (*types.Transaction, error) { + if opts.GasPrice != nil && (opts.GasFeeCap != nil || opts.GasTipCap != nil) { + return nil, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") + } + // Create the transaction + var ( + rawTx *types.Transaction + err error + ) + if opts.GasPrice != nil { + rawTx, err = c.createLegacyTx(opts, contract, input) + } else if opts.GasFeeCap != nil && opts.GasTipCap != nil { + rawTx, err = c.createDynamicTx(opts, contract, input, nil) + } else { + // Only query for basefee if gasPrice not specified + if head, errHead := c.transactor.HeaderByNumber(ensureContext(opts.Context), nil); errHead != nil { + return nil, errHead + } else if head.BaseFee != nil { + rawTx, err = c.createDynamicTx(opts, contract, input, head) + } else { + // Chain is not London ready -> use legacy transaction + rawTx, err = c.createLegacyTx(opts, contract, input) + } + } + if err != nil { + return nil, err + } + // Sign the transaction and schedule it for execution + if opts.Signer == nil { + return nil, errors.New("no signer to authorize the transaction with") + } + signedTx, err := opts.Signer(opts.From, rawTx) + if err != nil { + return nil, err + } + if opts.NoSend { + return signedTx, nil + } + if err := c.transactor.SendTransaction(ensureContext(opts.Context), signedTx); err != nil { + return nil, err + } + return signedTx, nil +} + +// FilterLogs filters contract logs for past blocks, returning the necessary +// channels to construct a strongly typed bound iterator on top of them. +func (c *BoundContractV1) FilterLogs(opts *FilterOpts, name string, query ...[]any) (chan types.Log, event.Subscription, error) { + // Don't crash on a lazy user + if opts == nil { + opts = new(FilterOpts) + } + // Append the event selector to the query parameters and construct the topic set + query = append([][]any{{c.abi.Events[name].ID}}, 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{c.address}, + Topics: topics, + FromBlock: new(big.Int).SetUint64(opts.Start), + } + if opts.End != nil { + config.ToBlock = new(big.Int).SetUint64(*opts.End) + } + /* TODO(karalabe): Replace the rest of the method below with this when supported + sub, err := c.filterer.SubscribeFilterLogs(ensureContext(opts.Context), config, logs) + */ + buff, err := c.filterer.FilterLogs(ensureContext(opts.Context), config) + if err != nil { + return nil, nil, err + } + sub := event.NewSubscription(func(quit <-chan struct{}) error { + for _, log := range buff { + select { + case logs <- log: + case <-quit: + return nil + } + } + return nil + }) + + return logs, sub, nil +} + +// WatchLogs filters subscribes to contract logs for future blocks, returning a +// subscription object that can be used to tear down the watcher. +func (c *BoundContractV1) WatchLogs(opts *WatchOpts, name string, query ...[]any) (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([][]any{{c.abi.Events[name].ID}}, 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{c.address}, + Topics: topics, + } + if opts.Start != nil { + config.FromBlock = new(big.Int).SetUint64(*opts.Start) + } + sub, err := c.filterer.SubscribeFilterLogs(ensureContext(opts.Context), config, logs) + if err != nil { + return nil, nil, err + } + return logs, sub, nil +} + +// UnpackLog unpacks a retrieved log into the provided output structure. +func (c *BoundContractV1) UnpackLog(out any, event string, log types.Log) error { + // Anonymous events are not supported. + if len(log.Topics) == 0 { + return errNoEventSignature + } + if log.Topics[0] != c.abi.Events[event].ID { + return errEventSignatureMismatch + } + if len(log.Data) > 0 { + if err := c.abi.UnpackIntoInterface(out, event, log.Data); err != nil { + return err + } + } + var indexed abi.Arguments + for _, arg := range c.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + return abi.ParseTopics(out, indexed, log.Topics[1:]) +} + +// UnpackLogIntoMap unpacks a retrieved log into the provided map. +func (c *BoundContractV1) UnpackLogIntoMap(out map[string]any, event string, log types.Log) error { + // Anonymous events are not supported. + if len(log.Topics) == 0 { + return errNoEventSignature + } + if log.Topics[0] != c.abi.Events[event].ID { + return errEventSignatureMismatch + } + if len(log.Data) > 0 { + if err := c.abi.UnpackIntoMap(out, event, log.Data); err != nil { + return err + } + } + var indexed abi.Arguments + for _, arg := range c.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + return abi.ParseTopicsIntoMap(out, indexed, log.Topics[1:]) +} + +// 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 +} diff --git a/accounts/abi/bind/v2/lib.go b/accounts/abi/bind/v2/lib.go index f36a578aa2..b7ad4c0abe 100644 --- a/accounts/abi/bind/v2/lib.go +++ b/accounts/abi/bind/v2/lib.go @@ -27,8 +27,9 @@ import ( "github.com/ethereum/go-ethereum/event" ) +// TODO: remove this and use NewBoundContract directly func NewContractInstance(backend ContractBackend, addr common.Address, abi abi.ABI) *BoundContract { - return NewBoundContract(addr, abi, backend, backend, backend) + return NewBoundContract(backend, addr, abi) } // ContractEvent is a type constraint for ABI event types. @@ -39,7 +40,8 @@ type ContractEvent interface { // FilterEvents returns an EventIterator instance for filtering historical events based on the event id and a block range. func FilterEvents[Ev ContractEvent](c *BoundContract, opts *FilterOpts, unpack func(*types.Log) (*Ev, error), topics ...[]any) (*EventIterator[Ev], error) { var e Ev - logs, sub, err := c.FilterLogs(opts, e.ContractEventName(), topics...) + v1Instance := BoundContractV1{c.address, c.abi, c.backend, c.backend, c.backend} + logs, sub, err := v1Instance.FilterLogs(opts, e.ContractEventName(), topics...) if err != nil { return nil, err } @@ -52,7 +54,8 @@ func FilterEvents[Ev ContractEvent](c *BoundContract, opts *FilterOpts, unpack f // error. func WatchEvents[Ev ContractEvent](c *BoundContract, opts *WatchOpts, unpack func(*types.Log) (*Ev, error), sink chan<- *Ev, topics ...[]any) (event.Subscription, error) { var e Ev - logs, sub, err := c.WatchLogs(opts, e.ContractEventName(), topics...) + v1Instance := BoundContractV1{c.address, c.abi, c.backend, c.backend, c.backend} + logs, sub, err := v1Instance.WatchLogs(opts, e.ContractEventName(), topics...) if err != nil { return nil, err } @@ -161,7 +164,8 @@ func (it *EventIterator[T]) Close() error { // This can be useful if you just want to check that the function doesn't revert. func Call[T any](c *BoundContract, opts *CallOpts, packedInput []byte, unpack func([]byte) (T, error)) (T, error) { var defaultResult T - packedOutput, err := c.CallRaw(opts, packedInput) + v1Instance := BoundContractV1{c.address, c.abi, c.backend, c.backend, c.backend} + packedOutput, err := v1Instance.CallRaw(opts, packedInput) if err != nil { return defaultResult, err } @@ -181,8 +185,10 @@ func Call[T any](c *BoundContract, opts *CallOpts, packedInput []byte, unpack fu // DeployContractRaw deploys a contract onto the Ethereum blockchain and binds the // deployment address with a Go wrapper. It expects its parameters to be abi-encoded // bytes. +// +// TODO: remove address return? we can calculate it from the returned tx func DeployContractRaw(opts *TransactOpts, bytecode []byte, backend ContractBackend, packedParams []byte) (common.Address, *types.Transaction, error) { - c := NewBoundContract(common.Address{}, abi.ABI{}, backend, backend, backend) + c := NewBoundContractV1(common.Address{}, abi.ABI{}, backend, backend, backend) tx, err := c.RawCreationTransact(opts, append(bytecode, packedParams...)) if err != nil { return common.Address{}, nil, err @@ -190,3 +196,10 @@ func DeployContractRaw(opts *TransactOpts, bytecode []byte, backend ContractBack address := crypto.CreateAddress(opts.From, tx.Nonce()) return address, tx, nil } + +// RawTransact initiates a transaction with the given raw calldata as the input. +// It's usually used to initiate transactions for invoking **Fallback** function. +func (c *BoundContract) RawTransact(opts *TransactOpts, calldata []byte) (*types.Transaction, error) { + v1Instance := BoundContractV1{c.address, c.abi, c.backend, c.backend, c.backend} + return v1Instance.RawTransact(opts, calldata) +}