From b2c727fff80ac8aa5d1ac34d169d8cdec7139599 Mon Sep 17 00:00:00 2001 From: Daniel Liu Date: Thu, 30 May 2024 18:14:57 +0800 Subject: [PATCH] accounts/abi/bind: refactor transact method (#23719) --- accounts/abi/bind/base.go | 212 ++++++++++++++++++++------------- accounts/abi/bind/base_test.go | 177 +++++++++++++++++++++++++++ 2 files changed, 308 insertions(+), 81 deletions(-) create mode 100644 accounts/abi/bind/base_test.go diff --git a/accounts/abi/bind/base.go b/accounts/abi/bind/base.go index 480aa596a4d7..869141e7792e 100644 --- a/accounts/abi/bind/base.go +++ b/accounts/abi/bind/base.go @@ -188,108 +188,158 @@ func (c *BoundContract) Transfer(opts *TransactOpts) (*types.Transaction, error) return c.transact(opts, &c.address, 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) { - var err error - - // Ensure a valid value field and resolve the account nonce +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) } - var nonce uint64 - if opts.Nonce == nil { - nonce, err = c.transactor.PendingNonceAt(ensureContext(opts.Context), opts.From) + // Estimate TipCap + gasTipCap := opts.GasTipCap + if gasTipCap == nil { + tip, err := c.transactor.SuggestGasTipCap(ensureContext(opts.Context)) if err != nil { - return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + return nil, err } - } else { - nonce = opts.Nonce.Uint64() + gasTipCap = tip } - // Figure out reasonable gas price values - if opts.GasPrice != nil && (opts.GasFeeCap != nil || opts.GasTipCap != nil) { - return nil, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") + // Estimate FeeCap + gasFeeCap := opts.GasFeeCap + if gasFeeCap == nil { + gasFeeCap = new(big.Int).Add( + gasTipCap, + new(big.Int).Mul(head.BaseFee, big.NewInt(2)), + ) } - head, err := c.transactor.HeaderByNumber(opts.Context, nil) + 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 } - if head.BaseFee != nil && opts.GasPrice == nil { - if opts.GasTipCap == nil { - tip, err := c.transactor.SuggestGasTipCap(opts.Context) - if err != nil { - return nil, err - } - opts.GasTipCap = tip - } - if opts.GasFeeCap == nil { - gasFeeCap := new(big.Int).Add( - opts.GasTipCap, - new(big.Int).Mul(head.BaseFee, big.NewInt(2)), - ) - opts.GasFeeCap = gasFeeCap - } - if opts.GasFeeCap.Cmp(opts.GasTipCap) < 0 { - return nil, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", opts.GasFeeCap, opts.GasTipCap) - } - } else { - if opts.GasFeeCap != nil || opts.GasTipCap != nil { - return nil, errors.New("maxFeePerGas or maxPriorityFeePerGas specified but london is not active yet") - } - if opts.GasPrice == nil { - price, err := c.transactor.SuggestGasPrice(ensureContext(opts.Context)) - if err != nil { - return nil, err - } - opts.GasPrice = price + baseTx := &types.DynamicFeeTx{ + To: contract, + Nonce: nonce, + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, + Gas: gasLimit, + Value: value, + Data: input, + } + 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 { + return nil, errors.New("maxFeePerGas or maxPriorityFeePerGas specified but EIP-1559 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 gasLimit == 0 { - // Gas estimation cannot succeed without code for method invocations - if contract != nil { - if code, err := c.transactor.PendingCodeAt(ensureContext(opts.Context), c.address); err != nil { - return nil, err - } else if len(code) == 0 { - return nil, ErrNoCode - } - } - // If the contract surely has code (or code is not needed), estimate the transaction - msg := XDPoSChain.CallMsg{From: opts.From, To: contract, GasPrice: opts.GasPrice, GasTipCap: opts.GasTipCap, GasFeeCap: opts.GasFeeCap, Value: value, Data: input} - gasLimit, err = c.transactor.EstimateGas(ensureContext(opts.Context), msg) + if opts.GasLimit == 0 { + var err error + gasLimit, err = c.estimateGasLimit(opts, contract, input, gasPrice, nil, nil, value) if err != nil { - return nil, fmt.Errorf("failed to estimate gas needed: %v", err) + return nil, err } } - // Create the transaction, sign it and schedule it for execution - var rawTx *types.Transaction - if opts.GasFeeCap == nil { - baseTx := &types.LegacyTx{ - Nonce: nonce, - GasPrice: opts.GasPrice, - Gas: gasLimit, - Value: value, - Data: input, - } - if contract != nil { - baseTx.To = &c.address + // 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 } - rawTx = types.NewTx(baseTx) + } + msg := XDPoSChain.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 { - baseTx := &types.DynamicFeeTx{ - Nonce: nonce, - GasFeeCap: opts.GasFeeCap, - GasTipCap: opts.GasTipCap, - Gas: gasLimit, - Value: value, - Data: input, - } - if contract != nil { - baseTx.To = &c.address + 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 { + // Only query for basefee if gasPrice not specified + if head, errHead := c.transactor.HeaderByNumber(ensureContext(opts.Context), nil); err != 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) } - rawTx = types.NewTx(baseTx) } + 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") } diff --git a/accounts/abi/bind/base_test.go b/accounts/abi/bind/base_test.go new file mode 100644 index 000000000000..037f1d24e8db --- /dev/null +++ b/accounts/abi/bind/base_test.go @@ -0,0 +1,177 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package bind_test + +import ( + "context" + "math/big" + "testing" + + "github.com/XinFinOrg/XDPoSChain" + "github.com/XinFinOrg/XDPoSChain/accounts/abi" + "github.com/XinFinOrg/XDPoSChain/accounts/abi/bind" + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/core/types" + "github.com/stretchr/testify/assert" +) + +func mockSign(addr common.Address, tx *types.Transaction) (*types.Transaction, error) { return tx, nil } + +type mockTransactor struct { + baseFee *big.Int + gasTipCap *big.Int + gasPrice *big.Int + suggestGasTipCapCalled bool + suggestGasPriceCalled bool +} + +func (mt *mockTransactor) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { + return &types.Header{BaseFee: mt.baseFee}, nil +} + +func (mt *mockTransactor) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { + return []byte{1}, nil +} + +func (mt *mockTransactor) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { + return 0, nil +} + +func (mt *mockTransactor) SuggestGasPrice(ctx context.Context) (*big.Int, error) { + mt.suggestGasPriceCalled = true + return mt.gasPrice, nil +} + +func (mt *mockTransactor) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { + mt.suggestGasTipCapCalled = true + return mt.gasTipCap, nil +} + +func (mt *mockTransactor) EstimateGas(ctx context.Context, call XDPoSChain.CallMsg) (gas uint64, err error) { + return 0, nil +} + +func (mt *mockTransactor) SendTransaction(ctx context.Context, tx *types.Transaction) error { + return nil +} + +type mockCaller struct { + codeAtBlockNumber *big.Int + callContractBlockNumber *big.Int + pendingCodeAtCalled bool + pendingCallContractCalled bool +} + +func (mc *mockCaller) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { + mc.codeAtBlockNumber = blockNumber + return []byte{1, 2, 3}, nil +} + +func (mc *mockCaller) CallContract(ctx context.Context, call XDPoSChain.CallMsg, blockNumber *big.Int) ([]byte, error) { + mc.callContractBlockNumber = blockNumber + return nil, nil +} + +func (mc *mockCaller) PendingCodeAt(ctx context.Context, contract common.Address) ([]byte, error) { + mc.pendingCodeAtCalled = true + return nil, nil +} + +func (mc *mockCaller) PendingCallContract(ctx context.Context, call XDPoSChain.CallMsg) ([]byte, error) { + mc.pendingCallContractCalled = true + return nil, nil +} +func TestPassingBlockNumber(t *testing.T) { + + mc := &mockCaller{} + + bc := bind.NewBoundContract(common.HexToAddress("0x0"), abi.ABI{ + Methods: map[string]abi.Method{ + "something": { + Name: "something", + Outputs: abi.Arguments{}, + }, + }, + }, mc, nil, nil) + + bc.Call(&bind.CallOpts{}, nil, "something") + + bc.Call(&bind.CallOpts{}, nil, "something") + + if mc.callContractBlockNumber != nil { + t.Fatalf("CallContract() was passed a block number when it should not have been") + } + + if mc.codeAtBlockNumber != nil { + t.Fatalf("CodeAt() was passed a block number when it should not have been") + } + + bc.Call(&bind.CallOpts{Pending: true}, nil, "something") + + if !mc.pendingCallContractCalled { + t.Fatalf("CallContract() was not passed the block number") + } + + if !mc.pendingCodeAtCalled { + t.Fatalf("CodeAt() was not passed the block number") + } +} + +func TestTransactGasFee(t *testing.T) { + assert := assert.New(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) + opts := &bind.TransactOpts{Signer: mockSign} + tx, err := bc.Transact(opts, "") + assert.Nil(err) + assert.Equal(big.NewInt(5), tx.GasTipCap()) + assert.Equal(big.NewInt(205), tx.GasFeeCap()) + assert.Nil(opts.GasTipCap) + assert.Nil(opts.GasFeeCap) + assert.True(mt.suggestGasTipCapCalled) + + // Second call to Transact should use latest suggested GasTipCap + mt.gasTipCap = big.NewInt(6) + mt.suggestGasTipCapCalled = false + tx, err = bc.Transact(opts, "") + assert.Nil(err) + assert.Equal(big.NewInt(6), tx.GasTipCap()) + assert.Equal(big.NewInt(206), tx.GasFeeCap()) + assert.True(mt.suggestGasTipCapCalled) + + // GasPrice + // When opts.GasPrice is nil + mt = &mockTransactor{gasPrice: big.NewInt(5)} + bc = bind.NewBoundContract(common.Address{}, abi.ABI{}, nil, mt, nil) + opts = &bind.TransactOpts{Signer: mockSign} + tx, err = bc.Transact(opts, "") + assert.Nil(err) + assert.Equal(big.NewInt(5), tx.GasPrice()) + assert.Nil(opts.GasPrice) + assert.True(mt.suggestGasPriceCalled) + + // Second call to Transact should use latest suggested GasPrice + mt.gasPrice = big.NewInt(6) + mt.suggestGasPriceCalled = false + tx, err = bc.Transact(opts, "") + assert.Nil(err) + assert.Equal(big.NewInt(6), tx.GasPrice()) + assert.True(mt.suggestGasPriceCalled) +}