diff --git a/cmd/bee/cmd/cmd.go b/cmd/bee/cmd/cmd.go index e10e015ce81..5b2c156c630 100644 --- a/cmd/bee/cmd/cmd.go +++ b/cmd/bee/cmd/cmd.go @@ -81,6 +81,7 @@ const ( optionMinimumStorageRadius = "minimum-storage-radius" optionReserveCapacityDoubling = "reserve-capacity-doubling" optionSkipPostageSnapshot = "skip-postage-snapshot" + optionNameMinimumGasTipCap = "minimum-gas-tip-cap" ) // nolint:gochecknoinits @@ -290,6 +291,7 @@ func (c *command) setAllFlags(cmd *cobra.Command) { cmd.Flags().Uint(optionMinimumStorageRadius, 0, "minimum radius storage threshold") cmd.Flags().Int(optionReserveCapacityDoubling, 0, "reserve capacity doubling") cmd.Flags().Bool(optionSkipPostageSnapshot, false, "skip postage snapshot") + cmd.Flags().Uint64(optionNameMinimumGasTipCap, 0, "minimum gas tip cap in wei for transactions, 0 means use suggested gas tip cap") } func newLogger(cmd *cobra.Command, verbosity string) (log.Logger, error) { diff --git a/cmd/bee/cmd/deploy.go b/cmd/bee/cmd/deploy.go index 89880273dab..c0e20cb63e7 100644 --- a/cmd/bee/cmd/deploy.go +++ b/cmd/bee/cmd/deploy.go @@ -59,6 +59,7 @@ func (c *command) initDeployCmd() error { signer, blocktime, true, + c.config.GetUint64(optionNameMinimumGasTipCap), ) if err != nil { return err diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index dab68c447fb..c6662bcba57 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -299,6 +299,7 @@ func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger lo EnableWS: c.config.GetBool(optionNameP2PWSEnable), FullNodeMode: fullNode, Logger: logger, + MinimumGasTipCap: c.config.GetUint64(optionNameMinimumGasTipCap), MinimumStorageRadius: c.config.GetUint(optionMinimumStorageRadius), MutexProfile: c.config.GetBool(optionNamePProfMutex), NATAddr: c.config.GetString(optionNameNATAddr), diff --git a/packaging/bee.yaml b/packaging/bee.yaml index 8176dfb47df..42ee156d0de 100644 --- a/packaging/bee.yaml +++ b/packaging/bee.yaml @@ -38,6 +38,8 @@ data-dir: "/var/lib/bee" # help: false ## triggers connect to main net bootnodes. # mainnet: true +## minimum gas tip cap in wei for transactions, 0 means use suggested gas tip cap +# minimum-gas-tip-cap: 0 ## minimum radius storage threshold # minimum-storage-radius: "0" ## NAT exposed address diff --git a/packaging/homebrew-amd64/bee.yaml b/packaging/homebrew-amd64/bee.yaml index c771e5d72c2..14a32f70116 100644 --- a/packaging/homebrew-amd64/bee.yaml +++ b/packaging/homebrew-amd64/bee.yaml @@ -38,6 +38,8 @@ data-dir: "/usr/local/var/lib/swarm-bee" # help: false ## triggers connect to main net bootnodes. # mainnet: true +## minimum gas tip cap in wei for transactions, 0 means use suggested gas tip cap +# minimum-gas-tip-cap: 0 ## minimum radius storage threshold # minimum-storage-radius: "0" ## NAT exposed address diff --git a/packaging/homebrew-arm64/bee.yaml b/packaging/homebrew-arm64/bee.yaml index 057d6c54d32..55d522b601c 100644 --- a/packaging/homebrew-arm64/bee.yaml +++ b/packaging/homebrew-arm64/bee.yaml @@ -38,6 +38,8 @@ data-dir: "/opt/homebrew/var/lib/swarm-bee" # help: false ## triggers connect to main net bootnodes. # mainnet: true +## minimum gas tip cap in wei for transactions, 0 means use suggested gas tip cap +# minimum-gas-tip-cap: 0 ## minimum radius storage threshold # minimum-storage-radius: "0" ## NAT exposed address diff --git a/packaging/scoop/bee.yaml b/packaging/scoop/bee.yaml index d2f97fedd33..70bed38f947 100644 --- a/packaging/scoop/bee.yaml +++ b/packaging/scoop/bee.yaml @@ -38,6 +38,8 @@ data-dir: "./data" # help: false ## triggers connect to main net bootnodes. # mainnet: true +## minimum gas tip cap in wei for transactions, 0 means use suggested gas tip cap +# minimum-gas-tip-cap: 0 ## minimum radius storage threshold # minimum-storage-radius: "0" ## NAT exposed address diff --git a/pkg/api/redistribution_test.go b/pkg/api/redistribution_test.go index 07b4bc28cfe..eef01977866 100644 --- a/pkg/api/redistribution_test.go +++ b/pkg/api/redistribution_test.go @@ -46,8 +46,8 @@ func TestRedistributionStatus(t *testing.T) { backendmock.WithBalanceAt(func(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error) { return big.NewInt(100000000), nil }), - backendmock.WithSuggestGasPriceFunc(func(ctx context.Context) (*big.Int, error) { - return big.NewInt(1), nil + backendmock.WithSuggestedFeeAndTipFunc(func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return big.NewInt(1), big.NewInt(2), nil }), }, }) diff --git a/pkg/node/chain.go b/pkg/node/chain.go index 5931c8dde8e..c1f00dd61bc 100644 --- a/pkg/node/chain.go +++ b/pkg/node/chain.go @@ -32,7 +32,6 @@ import ( "github.com/ethersphere/bee/v2/pkg/storage" "github.com/ethersphere/bee/v2/pkg/transaction" "github.com/ethersphere/bee/v2/pkg/transaction/wrapped" - "github.com/ethersphere/go-sw3-abi/sw3abi" "github.com/prometheus/client_golang/prometheus" ) @@ -53,6 +52,7 @@ func InitChain( signer crypto.Signer, pollingInterval time.Duration, chainEnabled bool, + minimumGasTipCap uint64, ) (transaction.Backend, common.Address, int64, transaction.Monitor, transaction.Service, error) { var backend transaction.Backend = &noOpChainBackend{ chainID: oChainID, @@ -74,7 +74,7 @@ func InitChain( logger.Info("connected to blockchain backend", "version", versionString) - backend = wrapped.NewBackend(ethclient.NewClient(rpcClient)) + backend = wrapped.NewBackend(ethclient.NewClient(rpcClient), minimumGasTipCap) } chainID, err := backend.ChainID(ctx) @@ -346,19 +346,10 @@ type noOpChainBackend struct { chainID int64 } -// BlockByNumber implements transaction.Backend. -func (m *noOpChainBackend) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { - return nil, postagecontract.ErrChainDisabled -} - func (m noOpChainBackend) Metrics() []prometheus.Collector { return nil } -func (m noOpChainBackend) CodeAt(context.Context, common.Address, *big.Int) ([]byte, error) { - return common.FromHex(sw3abi.SimpleSwapFactoryDeployedBinv0_6_9), nil -} - func (m noOpChainBackend) CallContract(context.Context, ethereum.CallMsg, *big.Int) ([]byte, error) { return nil, errors.New("disabled chain backend") } @@ -373,12 +364,12 @@ func (m noOpChainBackend) PendingNonceAt(context.Context, common.Address) (uint6 panic("chain no op: PendingNonceAt") } -func (m noOpChainBackend) SuggestGasPrice(context.Context) (*big.Int, error) { - panic("chain no op: SuggestGasPrice") +func (m noOpChainBackend) SuggestedFeeAndTip(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + panic("chain no op: SuggestedFeeAndTip") } func (m noOpChainBackend) SuggestGasTipCap(context.Context) (*big.Int, error) { - panic("chain no op: SuggestGasPrice") + panic("chain no op: SuggestGasTipCap") } func (m noOpChainBackend) EstimateGas(context.Context, ethereum.CallMsg) (uint64, error) { @@ -419,6 +410,4 @@ func (m noOpChainBackend) ChainID(context.Context) (*big.Int, error) { return big.NewInt(m.chainID), nil } -func (m noOpChainBackend) Close() error { - return nil -} +func (m noOpChainBackend) Close() {} diff --git a/pkg/node/node.go b/pkg/node/node.go index 7d14ceb1b15..a503165b7d8 100644 --- a/pkg/node/node.go +++ b/pkg/node/node.go @@ -106,7 +106,6 @@ type Bee struct { pullSyncCloser io.Closer pssCloser io.Closer gsocCloser io.Closer - ethClientCloser io.Closer transactionMonitorCloser io.Closer transactionCloser io.Closer listenerCloser io.Closer @@ -121,6 +120,7 @@ type Bee struct { shutdownMutex sync.Mutex syncingStopped *syncutil.Signaler accesscontrolCloser io.Closer + ethClientCloser func() } type Options struct { @@ -145,6 +145,7 @@ type Options struct { EnableWS bool FullNodeMode bool Logger log.Logger + MinimumGasTipCap uint64 MinimumStorageRadius uint MutexProfile bool NATAddr string @@ -390,11 +391,13 @@ func NewBee( o.ChainID, signer, o.BlockTime, - chainEnabled) + chainEnabled, + o.MinimumGasTipCap, + ) if err != nil { return nil, fmt.Errorf("init chain: %w", err) } - b.ethClientCloser = chainBackend + b.ethClientCloser = chainBackend.Close logger.Info("using chain with network network", "chain_id", chainID, "network_id", networkID) @@ -1384,7 +1387,10 @@ func (b *Bee) Shutdown() error { wg.Wait() - tryClose(b.ethClientCloser, "eth client") + if b.ethClientCloser != nil { + b.ethClientCloser() + } + tryClose(b.accesscontrolCloser, "accesscontrol") tryClose(b.tracerCloser, "tracer") tryClose(b.topologyCloser, "topology driver") diff --git a/pkg/settlement/swap/chequebook/init.go b/pkg/settlement/swap/chequebook/init.go index a89553e0f1c..8903a17c8bf 100644 --- a/pkg/settlement/swap/chequebook/init.go +++ b/pkg/settlement/swap/chequebook/init.go @@ -60,12 +60,12 @@ func checkBalance( minimumEth := big.NewInt(0) if gasPrice == nil { - gasPrice, err = swapBackend.SuggestGasPrice(timeoutCtx) + gasPrice, _, err = swapBackend.SuggestedFeeAndTip(timeoutCtx, gasPrice, 0) if err != nil { return err } - minimumEth = gasPrice.Mul(gasPrice, big.NewInt(250000)) + minimumEth = new(big.Int).Mul(gasPrice, big.NewInt(250000)) } insufficientERC20 := erc20Balance.Cmp(swapInitialDeposit) < 0 diff --git a/pkg/storageincentives/agent.go b/pkg/storageincentives/agent.go index a87540d390d..04c906c132a 100644 --- a/pkg/storageincentives/agent.go +++ b/pkg/storageincentives/agent.go @@ -46,7 +46,7 @@ type ChainBackend interface { BlockNumber(context.Context) (uint64, error) HeaderByNumber(context.Context, *big.Int) (*types.Header, error) BalanceAt(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error) - SuggestGasPrice(ctx context.Context) (*big.Int, error) + SuggestedFeeAndTip(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) } type Health interface { @@ -604,7 +604,7 @@ func (a *Agent) HasEnoughFundsToPlay(ctx context.Context) (*big.Int, bool, error return nil, false, err } - price, err := a.backend.SuggestGasPrice(ctx) + price, _, err := a.backend.SuggestedFeeAndTip(ctx, nil, redistribution.BoostTipPercent) if err != nil { return nil, false, err } diff --git a/pkg/storageincentives/agent_test.go b/pkg/storageincentives/agent_test.go index 8af6b1d463e..88db0e3b3d4 100644 --- a/pkg/storageincentives/agent_test.go +++ b/pkg/storageincentives/agent_test.go @@ -242,8 +242,8 @@ func (m *mockchainBackend) BalanceAt(ctx context.Context, address common.Address return m.balance, nil } -func (m *mockchainBackend) SuggestGasPrice(ctx context.Context) (*big.Int, error) { - return big.NewInt(4), nil +func (m *mockchainBackend) SuggestedFeeAndTip(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return big.NewInt(4), big.NewInt(5), nil } type contractCall int diff --git a/pkg/storageincentives/redistribution/redistribution.go b/pkg/storageincentives/redistribution/redistribution.go index 4657c81cbff..52b2245db75 100644 --- a/pkg/storageincentives/redistribution/redistribution.go +++ b/pkg/storageincentives/redistribution/redistribution.go @@ -17,7 +17,10 @@ import ( "github.com/ethersphere/bee/v2/pkg/transaction" ) -const loggerName = "redistributionContract" +const ( + loggerName = "redistributionContract" + BoostTipPercent = 50 +) type Contract interface { ReserveSalt(context.Context) ([]byte, error) @@ -117,7 +120,7 @@ func (c *contract) Claim(ctx context.Context, proofs ChunkInclusionProofs) (comm Value: big.NewInt(0), Description: "claim win transaction", } - txHash, err := c.sendAndWait(ctx, request, transaction.RedistributionTipBoostPercent) + txHash, err := c.sendAndWait(ctx, request, BoostTipPercent) if err != nil { return txHash, fmt.Errorf("claim: %w", err) } @@ -140,7 +143,7 @@ func (c *contract) Commit(ctx context.Context, obfusHash []byte, round uint64) ( Value: big.NewInt(0), Description: "commit transaction", } - txHash, err := c.sendAndWait(ctx, request, transaction.RedistributionTipBoostPercent) + txHash, err := c.sendAndWait(ctx, request, BoostTipPercent) if err != nil { return txHash, fmt.Errorf("commit: obfusHash %v: %w", common.BytesToHash(obfusHash), err) } @@ -163,7 +166,7 @@ func (c *contract) Reveal(ctx context.Context, storageDepth uint8, reserveCommit Value: big.NewInt(0), Description: "reveal transaction", } - txHash, err := c.sendAndWait(ctx, request, transaction.RedistributionTipBoostPercent) + txHash, err := c.sendAndWait(ctx, request, BoostTipPercent) if err != nil { return txHash, fmt.Errorf("reveal: storageDepth %d reserveCommitmentHash %v RandomNonce %v: %w", storageDepth, common.BytesToHash(reserveCommitmentHash), common.BytesToHash(RandomNonce), err) } diff --git a/pkg/transaction/backend.go b/pkg/transaction/backend.go index 0bf38c35975..075dbfe19b8 100644 --- a/pkg/transaction/backend.go +++ b/pkg/transaction/backend.go @@ -14,27 +14,13 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethersphere/bee/v2/pkg/log" + "github.com/ethersphere/bee/v2/pkg/transaction/backend" ) // Backend is the minimum of blockchain backend functions we need. type Backend interface { - CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) - CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) - HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) - PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) - SuggestGasPrice(ctx context.Context) (*big.Int, error) - SuggestGasTipCap(ctx context.Context) (*big.Int, error) - EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) - SendTransaction(ctx context.Context, tx *types.Transaction) error - TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) - TransactionByHash(ctx context.Context, hash common.Hash) (tx *types.Transaction, isPending bool, err error) - BlockNumber(ctx context.Context) (uint64, error) - BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) - BalanceAt(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error) - NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) - FilterLogs(ctx context.Context, query ethereum.FilterQuery) ([]types.Log, error) - ChainID(ctx context.Context) (*big.Int, error) - Close() error + backend.Geth + SuggestedFeeAndTip(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) } // IsSynced will check if we are synced with the given blockchain backend. This @@ -42,11 +28,7 @@ type Backend interface { // with the given maxDelay as the maximum duration we can be behind the block // time. func IsSynced(ctx context.Context, backend Backend, maxDelay time.Duration) (bool, time.Time, error) { - number, err := backend.BlockNumber(ctx) - if err != nil { - return false, time.Time{}, err - } - header, err := backend.HeaderByNumber(ctx, big.NewInt(int64(number))) + header, err := backend.HeaderByNumber(ctx, nil) if errors.Is(err, ethereum.NotFound) { return false, time.Time{}, nil } diff --git a/pkg/transaction/backend/backend.go b/pkg/transaction/backend/backend.go new file mode 100644 index 00000000000..67323d14e45 --- /dev/null +++ b/pkg/transaction/backend/backend.go @@ -0,0 +1,32 @@ +// Copyright 2025 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package backend + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// Geth is the interface that an ethclient.Client satisfies. +type Geth interface { + BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) + BlockNumber(ctx context.Context) (uint64, error) + CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) + ChainID(ctx context.Context) (*big.Int, error) + Close() + EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) + FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) + HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) + NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) + PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) + SendTransaction(ctx context.Context, tx *types.Transaction) error + SuggestGasTipCap(ctx context.Context) (*big.Int, error) + TransactionByHash(ctx context.Context, hash common.Hash) (tx *types.Transaction, isPending bool, err error) + TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) +} diff --git a/pkg/transaction/backend_test.go b/pkg/transaction/backend_test.go index d9d09e55b3c..313db5d9b9f 100644 --- a/pkg/transaction/backend_test.go +++ b/pkg/transaction/backend_test.go @@ -22,7 +22,6 @@ func TestIsSynced(t *testing.T) { maxDelay := 10 * time.Second now := time.Now().UTC() ctx := context.Background() - blockNumber := uint64(100) t.Run("synced", func(t *testing.T) { t.Parallel() @@ -30,12 +29,9 @@ func TestIsSynced(t *testing.T) { synced, _, err := transaction.IsSynced( ctx, backendmock.New( - backendmock.WithBlockNumberFunc(func(c context.Context) (uint64, error) { - return blockNumber, nil - }), backendmock.WithHeaderbyNumberFunc(func(ctx context.Context, number *big.Int) (*types.Header, error) { - if number.Uint64() != blockNumber { - return nil, errors.New("called with wrong block number") + if number != nil { + return nil, errors.New("latest block should be called with nil") } return &types.Header{ Time: uint64(now.Unix()), @@ -58,12 +54,9 @@ func TestIsSynced(t *testing.T) { synced, _, err := transaction.IsSynced( ctx, backendmock.New( - backendmock.WithBlockNumberFunc(func(c context.Context) (uint64, error) { - return blockNumber, nil - }), backendmock.WithHeaderbyNumberFunc(func(ctx context.Context, number *big.Int) (*types.Header, error) { - if number.Uint64() != blockNumber { - return nil, errors.New("called with wrong block number") + if number != nil { + return nil, errors.New("latest block should be called with nil") } return &types.Header{ Time: uint64(now.Add(-maxDelay).Unix()), @@ -87,12 +80,9 @@ func TestIsSynced(t *testing.T) { _, _, err := transaction.IsSynced( ctx, backendmock.New( - backendmock.WithBlockNumberFunc(func(c context.Context) (uint64, error) { - return blockNumber, nil - }), backendmock.WithHeaderbyNumberFunc(func(ctx context.Context, number *big.Int) (*types.Header, error) { - if number.Uint64() != blockNumber { - return nil, errors.New("called with wrong block number") + if number != nil { + return nil, errors.New("latest block should be called with nil") } return nil, expectedErr }), diff --git a/pkg/transaction/backendmock/backend.go b/pkg/transaction/backendmock/backend.go index fea46f3936b..25166a898e0 100644 --- a/pkg/transaction/backendmock/backend.go +++ b/pkg/transaction/backendmock/backend.go @@ -16,29 +16,20 @@ import ( ) type backendMock struct { - codeAt func(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) callContract func(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) sendTransaction func(ctx context.Context, tx *types.Transaction) error - suggestGasPrice func(ctx context.Context) (*big.Int, error) + suggestedFeeAndTip func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) suggestGasTipCap func(ctx context.Context) (*big.Int, error) estimateGas func(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) transactionReceipt func(ctx context.Context, txHash common.Hash) (*types.Receipt, error) pendingNonceAt func(ctx context.Context, account common.Address) (uint64, error) transactionByHash func(ctx context.Context, hash common.Hash) (tx *types.Transaction, isPending bool, err error) blockNumber func(ctx context.Context) (uint64, error) - blockByNumber func(ctx context.Context, number *big.Int) (*types.Block, error) headerByNumber func(ctx context.Context, number *big.Int) (*types.Header, error) balanceAt func(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error) nonceAt func(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) } -func (m *backendMock) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { - if m.codeAt != nil { - return m.codeAt(ctx, contract, blockNumber) - } - return nil, errors.New("not implemented") -} - func (m *backendMock) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { if m.callContract != nil { return m.callContract(ctx, call, blockNumber) @@ -46,10 +37,6 @@ func (m *backendMock) CallContract(ctx context.Context, call ethereum.CallMsg, b return nil, errors.New("not implemented") } -func (*backendMock) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { - return nil, errors.New("not implemented") -} - func (m *backendMock) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { if m.pendingNonceAt != nil { return m.pendingNonceAt(ctx, account) @@ -57,11 +44,11 @@ func (m *backendMock) PendingNonceAt(ctx context.Context, account common.Address return 0, errors.New("not implemented") } -func (m *backendMock) SuggestGasPrice(ctx context.Context) (*big.Int, error) { - if m.suggestGasPrice != nil { - return m.suggestGasPrice(ctx) +func (m *backendMock) SuggestedFeeAndTip(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + if m.suggestedFeeAndTip != nil { + return m.suggestedFeeAndTip(ctx, gasPrice, boostPercent) } - return nil, errors.New("not implemented") + return nil, nil, errors.New("not implemented") } func (m *backendMock) EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) { @@ -82,10 +69,6 @@ func (*backendMock) FilterLogs(ctx context.Context, query ethereum.FilterQuery) return nil, errors.New("not implemented") } -func (*backendMock) SubscribeFilterLogs(ctx context.Context, query ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { - return nil, errors.New("not implemented") -} - func (m *backendMock) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { if m.transactionReceipt != nil { return m.transactionReceipt(ctx, txHash) @@ -107,13 +90,6 @@ func (m *backendMock) BlockNumber(ctx context.Context) (uint64, error) { return 0, errors.New("not implemented") } -func (m *backendMock) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { - if m.blockByNumber != nil { - return m.blockByNumber(ctx, number) - } - return nil, errors.New("not implemented") -} - func (m *backendMock) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { if m.headerByNumber != nil { return m.headerByNumber(ctx, number) @@ -146,9 +122,7 @@ func (m *backendMock) ChainID(ctx context.Context) (*big.Int, error) { return nil, errors.New("not implemented") } -func (m *backendMock) Close() error { - return nil -} +func (m *backendMock) Close() {} func New(opts ...Option) transaction.Backend { mock := new(backendMock) @@ -173,12 +147,6 @@ func WithCallContractFunc(f func(ctx context.Context, call ethereum.CallMsg, blo }) } -func WithCodeAtFunc(f func(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error)) Option { - return optionFunc(func(s *backendMock) { - s.codeAt = f - }) -} - func WithBalanceAt(f func(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error)) Option { return optionFunc(func(s *backendMock) { s.balanceAt = f @@ -191,9 +159,9 @@ func WithPendingNonceAtFunc(f func(ctx context.Context, account common.Address) }) } -func WithSuggestGasPriceFunc(f func(ctx context.Context) (*big.Int, error)) Option { +func WithSuggestedFeeAndTipFunc(f func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error)) Option { return optionFunc(func(s *backendMock) { - s.suggestGasPrice = f + s.suggestedFeeAndTip = f }) } @@ -221,12 +189,6 @@ func WithTransactionByHashFunc(f func(ctx context.Context, txHash common.Hash) ( }) } -func WithBlockByNumberFunc(f func(ctx context.Context, number *big.Int) (*types.Block, error)) Option { - return optionFunc(func(s *backendMock) { - s.blockByNumber = f - }) -} - func WithSendTransactionFunc(f func(ctx context.Context, tx *types.Transaction) error) Option { return optionFunc(func(s *backendMock) { s.sendTransaction = f diff --git a/pkg/transaction/backendsimulation/backend.go b/pkg/transaction/backendsimulation/backend.go index 58c183fa0bd..47480fd3994 100644 --- a/pkg/transaction/backendsimulation/backend.go +++ b/pkg/transaction/backendsimulation/backend.go @@ -7,6 +7,7 @@ package backendsimulation import ( "context" "errors" + "maps" "math/big" "github.com/ethereum/go-ethereum" @@ -74,40 +75,24 @@ func (m *simulatedBackend) advanceBlock() { m.blockNumber = block.Number if block.Receipts != nil { - for hash, receipt := range block.Receipts { - m.receipts[hash] = receipt - } + maps.Copy(m.receipts, block.Receipts) } if block.NoncesAt != nil { - for addr, nonce := range block.NoncesAt { - m.noncesAt[addr] = nonce - } + maps.Copy(m.noncesAt, block.NoncesAt) } } -func (m *simulatedBackend) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { - return nil, errors.New("not implemented") -} - -func (m *simulatedBackend) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { - return nil, errors.New("not implemented") -} - func (*simulatedBackend) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { return nil, errors.New("not implemented") } -func (*simulatedBackend) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { - return nil, errors.New("not implemented") -} - func (m *simulatedBackend) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { return 0, errors.New("not implemented") } -func (m *simulatedBackend) SuggestGasPrice(ctx context.Context) (*big.Int, error) { - return nil, errors.New("not implemented") +func (m *simulatedBackend) SuggestedFeeAndTip(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return nil, nil, errors.New("not implemented") } func (m *simulatedBackend) EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) { @@ -122,10 +107,6 @@ func (*simulatedBackend) FilterLogs(ctx context.Context, query ethereum.FilterQu return nil, errors.New("not implemented") } -func (*simulatedBackend) SubscribeFilterLogs(ctx context.Context, query ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { - return nil, errors.New("not implemented") -} - func (m *simulatedBackend) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { receipt, ok := m.receipts[txHash] if ok { @@ -169,6 +150,4 @@ func (m *simulatedBackend) ChainID(ctx context.Context) (*big.Int, error) { return nil, errors.New("not implemented") } -func (m *simulatedBackend) Close() error { - return nil -} +func (m *simulatedBackend) Close() {} diff --git a/pkg/transaction/transaction.go b/pkg/transaction/transaction.go index 2d911e27261..da71a2150e3 100644 --- a/pkg/transaction/transaction.go +++ b/pkg/transaction/transaction.go @@ -41,14 +41,11 @@ var ( ErrTransactionReverted = errors.New("transaction reverted") ErrUnknownTransaction = errors.New("unknown transaction") ErrAlreadyImported = errors.New("already imported") - ErrEIP1559NotSupported = errors.New("network does not appear to support EIP-1559 (no baseFee)") ) const ( - DefaultGasLimit = 1_000_000 - DefaultTipBoostPercent = 25 - MinimumGasTipCap = 1_500_000_000 // 1.5 Gwei - RedistributionTipBoostPercent = 50 + DefaultGasLimit = 1_000_000 + DefaultTipBoostPercent = 25 ) // TxRequest describes a request for a transaction that can be executed. @@ -309,7 +306,7 @@ func (t *transactionService) prepareTransaction(ctx context.Context, request *Tx notice that gas price does not exceed 20 as defined by max fee. */ - gasFeeCap, gasTipCap, err := t.suggestedFeeAndTip(ctx, request.GasPrice, boostPercent) + gasFeeCap, gasTipCap, err := t.backend.SuggestedFeeAndTip(ctx, request.GasPrice, boostPercent) if err != nil { return nil, err } @@ -326,51 +323,6 @@ func (t *transactionService) prepareTransaction(ctx context.Context, request *Tx }), nil } -func (t *transactionService) suggestedFeeAndTip(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { - gasTipCap, err := t.backend.SuggestGasTipCap(ctx) - if err != nil { - return nil, nil, err - } - - multiplier := big.NewInt(int64(boostPercent) + 100) - gasTipCap = new(big.Int).Div(new(big.Int).Mul(gasTipCap, multiplier), big.NewInt(100)) - - minimumTip := big.NewInt(MinimumGasTipCap) - if gasTipCap.Cmp(minimumTip) < 0 { - gasTipCap = new(big.Int).Set(minimumTip) - } - - var gasFeeCap *big.Int - - if gasPrice == nil { - latestBlockHeader, err := t.backend.HeaderByNumber(ctx, nil) - if err != nil { - return nil, nil, fmt.Errorf("failed to get latest block: %w", err) - } - - if latestBlockHeader.BaseFee == nil { - return nil, nil, ErrEIP1559NotSupported - } - - // gasFeeCap = (2 * baseFee) + gasTipCap - gasFeeCap = new(big.Int).Add( - new(big.Int).Mul(latestBlockHeader.BaseFee, big.NewInt(2)), - gasTipCap, - ) - } else { - gasFeeCap = new(big.Int).Set(gasPrice) - } - - if gasTipCap.Cmp(gasFeeCap) > 0 { - t.logger.Warning("gas tip cap is higher than gas fee cap, using gas fee cap as gas tip cap", "gas_tip_cap", gasTipCap, "gas_fee_cap", gasFeeCap) - gasTipCap = new(big.Int).Set(gasFeeCap) - } - - t.logger.Debug("prepare transaction", "gas_max_fee", gasFeeCap, "gas_max_tip", gasTipCap) - - return gasFeeCap, gasTipCap, nil -} - func storedTransactionKey(txHash common.Hash) string { return fmt.Sprintf("%s%x", storedTransactionPrefix, txHash) } @@ -394,7 +346,7 @@ func (t *transactionService) nextNonce(ctx context.Context) (uint64, error) { // PendingNonceAt returns the nonce we should use, but we will // compare this to our pending tx list, therefore the -1. - var maxNonce = onchainNonce - 1 + maxNonce := onchainNonce - 1 for _, txHash := range pendingTxs { trx, _, err := t.backend.TransactionByHash(ctx, txHash) if err != nil { @@ -441,7 +393,7 @@ func (t *transactionService) WatchSentTransaction(txHash common.Hash) (<-chan ty } func (t *transactionService) PendingTransactions() ([]common.Hash, error) { - var txHashes = make([]common.Hash, 0) + txHashes := make([]common.Hash, 0) err := t.store.Iterate(pendingTransactionPrefix, func(key, value []byte) (stop bool, err error) { txHash := common.HexToHash(strings.TrimPrefix(string(key), pendingTransactionPrefix)) txHashes = append(txHashes, txHash) @@ -491,7 +443,7 @@ func (t *transactionService) ResendTransaction(ctx context.Context, txHash commo return err } - gasFeeCap, gasTipCap, err := t.suggestedFeeAndTip(ctx, sctx.GetGasPrice(ctx), storedTransaction.GasTipBoost) + gasFeeCap, gasTipCap, err := t.backend.SuggestedFeeAndTip(ctx, sctx.GetGasPrice(ctx), storedTransaction.GasTipBoost) if err != nil { return err } @@ -531,7 +483,7 @@ func (t *transactionService) CancelTransaction(ctx context.Context, originalTxHa return common.Hash{}, err } - gasFeeCap, gasTipCap, err := t.suggestedFeeAndTip(ctx, sctx.GetGasPrice(ctx), 0) + gasFeeCap, gasTipCap, err := t.backend.SuggestedFeeAndTip(ctx, sctx.GetGasPrice(ctx), 0) if err != nil { return common.Hash{}, err } diff --git a/pkg/transaction/transaction_test.go b/pkg/transaction/transaction_test.go index d8b830e5780..079e341dd27 100644 --- a/pkg/transaction/transaction_test.go +++ b/pkg/transaction/transaction_test.go @@ -31,7 +31,7 @@ import ( ) var ( - minimumTip = big.NewInt(transaction.MinimumGasTipCap) + minimumTip = big.NewInt(1_500_000_000) baseFee = big.NewInt(3_000_000_000) ) @@ -75,6 +75,39 @@ func signerMockForTransaction(t *testing.T, signedTx *types.Transaction, sender ) } +func checkStoredTransaction(t *testing.T, transactionService transaction.Service, txHash common.Hash, request *transaction.TxRequest, recipient common.Address, gasLimit uint64, gasPrice *big.Int, nonce uint64) { + t.Helper() + + storedTransaction, err := transactionService.StoredTransaction(txHash) + if err != nil { + t.Fatal(err) + } + + if storedTransaction.To == nil || *storedTransaction.To != recipient { + t.Fatalf("got wrong recipient in stored transaction. wanted %x, got %x", recipient, storedTransaction.To) + } + + if !bytes.Equal(storedTransaction.Data, request.Data) { + t.Fatalf("got wrong data in stored transaction. wanted %x, got %x", request.Data, storedTransaction.Data) + } + + if storedTransaction.Description != request.Description { + t.Fatalf("got wrong description in stored transaction. wanted %x, got %x", request.Description, storedTransaction.Description) + } + + if storedTransaction.GasLimit != gasLimit { + t.Fatalf("got wrong gas limit in stored transaction. wanted %d, got %d", gasLimit, storedTransaction.GasLimit) + } + + if gasPrice.Cmp(storedTransaction.GasPrice) != 0 { + t.Fatalf("got wrong gas price in stored transaction. wanted %d, got %d", gasPrice, storedTransaction.GasPrice) + } + + if storedTransaction.Nonce != nonce { + t.Fatalf("got wrong nonce in stored transaction. wanted %d, got %d", nonce, storedTransaction.Nonce) + } +} + func TestTransactionSend(t *testing.T) { t.Parallel() @@ -130,11 +163,8 @@ func TestTransactionSend(t *testing.T) { backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) { return nonce - 1, nil }), - backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) { - return suggestedGasTip, nil - }), - backendmock.WithHeaderbyNumberFunc(func(ctx context.Context, number *big.Int) (*types.Header, error) { - return &types.Header{BaseFee: baseFee}, nil + backendmock.WithSuggestedFeeAndTipFunc(func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return gasFeeCap, suggestedGasTip, nil }), ), signerMockForTransaction(t, signedTx, sender, chainID), @@ -160,34 +190,7 @@ func TestTransactionSend(t *testing.T) { t.Fatal("returning wrong transaction hash") } - storedTransaction, err := transactionService.StoredTransaction(txHash) - if err != nil { - t.Fatal(err) - } - - if storedTransaction.To == nil || *storedTransaction.To != recipient { - t.Fatalf("got wrong recipient in stored transaction. wanted %x, got %x", recipient, storedTransaction.To) - } - - if !bytes.Equal(storedTransaction.Data, request.Data) { - t.Fatalf("got wrong data in stored transaction. wanted %x, got %x", request.Data, storedTransaction.Data) - } - - if storedTransaction.Description != request.Description { - t.Fatalf("got wrong description in stored transaction. wanted %x, got %x", request.Description, storedTransaction.Description) - } - - if storedTransaction.GasLimit != gasLimit { - t.Fatalf("got wrong gas limit in stored transaction. wanted %d, got %d", gasLimit, storedTransaction.GasLimit) - } - - if gasFeeCap.Cmp(storedTransaction.GasPrice) != 0 { - t.Fatalf("got wrong gas price in stored transaction. wanted %d, got %d", gasFeeCap, storedTransaction.GasPrice) - } - - if storedTransaction.Nonce != nonce { - t.Fatalf("got wrong nonce in stored transaction. wanted %d, got %d", nonce, storedTransaction.Nonce) - } + checkStoredTransaction(t, transactionService, txHash, request, recipient, gasLimit, gasFeeCap, nonce) pending, err := transactionService.PendingTransactions() if err != nil { @@ -237,11 +240,8 @@ func TestTransactionSend(t *testing.T) { backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) { return nonce - 1, nil }), - backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) { - return suggestedGasTip, nil - }), - backendmock.WithHeaderbyNumberFunc(func(ctx context.Context, number *big.Int) (*types.Header, error) { - return &types.Header{BaseFee: baseFee}, nil + backendmock.WithSuggestedFeeAndTipFunc(func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return gasFeeCap, suggestedGasTip, nil }), ), signerMockForTransaction(t, signedTx, sender, chainID), @@ -267,34 +267,7 @@ func TestTransactionSend(t *testing.T) { t.Fatal("returning wrong transaction hash") } - storedTransaction, err := transactionService.StoredTransaction(txHash) - if err != nil { - t.Fatal(err) - } - - if storedTransaction.To == nil || *storedTransaction.To != recipient { - t.Fatalf("got wrong recipient in stored transaction. wanted %x, got %x", recipient, storedTransaction.To) - } - - if !bytes.Equal(storedTransaction.Data, request.Data) { - t.Fatalf("got wrong data in stored transaction. wanted %x, got %x", request.Data, storedTransaction.Data) - } - - if storedTransaction.Description != request.Description { - t.Fatalf("got wrong description in stored transaction. wanted %x, got %x", request.Description, storedTransaction.Description) - } - - if storedTransaction.GasLimit != gasLimit { - t.Fatalf("got wrong gas limit in stored transaction. wanted %d, got %d", gasLimit, storedTransaction.GasLimit) - } - - if gasFeeCap.Cmp(storedTransaction.GasPrice) != 0 { - t.Fatalf("got wrong gas price in stored transaction. wanted %d, got %d", gasFeeCap, storedTransaction.GasPrice) - } - - if storedTransaction.Nonce != nonce { - t.Fatalf("got wrong nonce in stored transaction. wanted %d, got %d", nonce, storedTransaction.Nonce) - } + checkStoredTransaction(t, transactionService, txHash, request, recipient, gasLimit, gasFeeCap, nonce) pending, err := transactionService.PendingTransactions() if err != nil { @@ -353,11 +326,8 @@ func TestTransactionSend(t *testing.T) { backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) { return nonce - 1, nil }), - backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) { - return suggestedGasTip, nil - }), - backendmock.WithHeaderbyNumberFunc(func(ctx context.Context, number *big.Int) (*types.Header, error) { - return &types.Header{BaseFee: baseFee}, nil + backendmock.WithSuggestedFeeAndTipFunc(func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return gasFeeCapWithBoost, suggestedGasTip, nil }), ), signerMockForTransaction(t, signedTx, sender, chainID), @@ -383,34 +353,7 @@ func TestTransactionSend(t *testing.T) { t.Fatal("returning wrong transaction hash") } - storedTransaction, err := transactionService.StoredTransaction(txHash) - if err != nil { - t.Fatal(err) - } - - if storedTransaction.To == nil || *storedTransaction.To != recipient { - t.Fatalf("got wrong recipient in stored transaction. wanted %x, got %x", recipient, storedTransaction.To) - } - - if !bytes.Equal(storedTransaction.Data, request.Data) { - t.Fatalf("got wrong data in stored transaction. wanted %x, got %x", request.Data, storedTransaction.Data) - } - - if storedTransaction.Description != request.Description { - t.Fatalf("got wrong description in stored transaction. wanted %x, got %x", request.Description, storedTransaction.Description) - } - - if storedTransaction.GasLimit != gasLimit { - t.Fatalf("got wrong gas limit in stored transaction. wanted %d, got %d", gasLimit, storedTransaction.GasLimit) - } - - if gasFeeCapWithBoost.Cmp(storedTransaction.GasPrice) != 0 { - t.Fatalf("got wrong gas price in stored transaction. wanted %d, got %d", gasFeeCapWithBoost, storedTransaction.GasPrice) - } - - if storedTransaction.Nonce != nonce { - t.Fatalf("got wrong nonce in stored transaction. wanted %d, got %d", nonce, storedTransaction.Nonce) - } + checkStoredTransaction(t, transactionService, txHash, request, recipient, gasLimit, gasFeeCapWithBoost, nonce) pending, err := transactionService.PendingTransactions() if err != nil { @@ -465,11 +408,8 @@ func TestTransactionSend(t *testing.T) { backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) { return nonce, nil }), - backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) { - return suggestedGasTip, nil - }), - backendmock.WithHeaderbyNumberFunc(func(ctx context.Context, number *big.Int) (*types.Header, error) { - return &types.Header{BaseFee: baseFee}, nil + backendmock.WithSuggestedFeeAndTipFunc(func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return gasFeeCap, suggestedGasTip, nil }), ), signerMockForTransaction(t, signedTx, sender, chainID), @@ -533,11 +473,8 @@ func TestTransactionSend(t *testing.T) { backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) { return nextNonce, nil }), - backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) { - return suggestedGasTip, nil - }), - backendmock.WithHeaderbyNumberFunc(func(ctx context.Context, number *big.Int) (*types.Header, error) { - return &types.Header{BaseFee: baseFee}, nil + backendmock.WithSuggestedFeeAndTipFunc(func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return gasFeeCap, suggestedGasTip, nil }), ), signerMockForTransaction(t, signedTx, sender, chainID), @@ -602,11 +539,8 @@ func TestTransactionSend(t *testing.T) { backendmock.WithPendingNonceAtFunc(func(ctx context.Context, account common.Address) (uint64, error) { return nextNonce, nil }), - backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) { - return suggestedGasTip, nil - }), - backendmock.WithHeaderbyNumberFunc(func(ctx context.Context, number *big.Int) (*types.Header, error) { - return &types.Header{BaseFee: baseFee}, nil + backendmock.WithSuggestedFeeAndTipFunc(func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return customGasFeeCap, customGasFeeCap, nil }), ), signerMockForTransaction(t, signedTx, sender, chainID), @@ -748,11 +682,8 @@ func TestTransactionResend(t *testing.T) { } return nil }), - backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) { - return gasTip, nil - }), - backendmock.WithHeaderbyNumberFunc(func(ctx context.Context, number *big.Int) (*types.Header, error) { - return &types.Header{BaseFee: baseFee}, nil + backendmock.WithSuggestedFeeAndTipFunc(func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return gasFeeCap, gasTip, nil }), ), signerMockForTransaction(t, signedTx, recipient, chainID), @@ -838,11 +769,8 @@ func TestTransactionCancel(t *testing.T) { } return nil }), - backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) { - return gasTip, nil - }), - backendmock.WithHeaderbyNumberFunc(func(ctx context.Context, number *big.Int) (*types.Header, error) { - return &types.Header{BaseFee: baseFee}, nil + backendmock.WithSuggestedFeeAndTipFunc(func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return fee, minimumTip, nil }), ), signerMockForTransaction(t, cancelTx, recipient, chainID), @@ -879,7 +807,7 @@ func TestTransactionCancel(t *testing.T) { Value: big.NewInt(0), Gas: 21000, GasFeeCap: gasFeeCap, - GasTipCap: gasTip, + GasTipCap: gasTipCap, Data: []byte{}, }) @@ -891,8 +819,8 @@ func TestTransactionCancel(t *testing.T) { } return nil }), - backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) { - return gasTip, nil + backendmock.WithSuggestedFeeAndTipFunc(func(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + return gasFee, gasTip, nil }), ), signerMockForTransaction(t, cancelTx, recipient, chainID), diff --git a/pkg/transaction/wrapped/fee.go b/pkg/transaction/wrapped/fee.go new file mode 100644 index 00000000000..3b02b3423ec --- /dev/null +++ b/pkg/transaction/wrapped/fee.go @@ -0,0 +1,61 @@ +// Copyright 2025 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package wrapped + +import ( + "context" + "errors" + "fmt" + "math/big" +) + +const ( + percentageDivisor = 100 + baseFeeMultiplier = 2 +) + +var ( + ErrEIP1559NotSupported = errors.New("network does not appear to support EIP-1559 (no baseFee)") +) + +// SuggestedFeeAndTip calculates the recommended gasFeeCap and gasTipCap for a transaction. +func (b *wrappedBackend) SuggestedFeeAndTip(ctx context.Context, gasPrice *big.Int, boostPercent int) (*big.Int, *big.Int, error) { + if gasPrice != nil { + // 1. gasFeeCap: The absolute maximum price per gas does not exceed the user's specified price. + // 2. gasTipCap: The entire amount (gasPrice - baseFee) can be used as a priority fee. + return new(big.Int).Set(gasPrice), new(big.Int).Set(gasPrice), nil + } + + gasTipCap, err := b.backend.SuggestGasTipCap(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to suggest gas tip cap: %w", err) + } + + if boostPercent != 0 { + // multiplier: 100 + boostPercent (e.g., 110 for 10% boost) + multiplier := new(big.Int).Add(big.NewInt(int64(percentageDivisor)), big.NewInt(int64(boostPercent))) + // gasTipCap = gasTipCap * (100 + boostPercent) / 100 + gasTipCap.Mul(gasTipCap, multiplier).Div(gasTipCap, big.NewInt(int64(percentageDivisor))) + } + + minimumTip := big.NewInt(b.minimumGasTipCap) + if gasTipCap.Cmp(minimumTip) < 0 { + gasTipCap.Set(minimumTip) + } + + latestBlockHeader, err := b.backend.HeaderByNumber(ctx, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to get latest block header: %w", err) + } + if latestBlockHeader == nil || latestBlockHeader.BaseFee == nil { + return nil, nil, ErrEIP1559NotSupported + } + + // EIP-1559: gasFeeCap = (2 * baseFee) + gasTipCap + gasFeeCap := new(big.Int).Mul(latestBlockHeader.BaseFee, big.NewInt(int64(baseFeeMultiplier))) + gasFeeCap.Add(gasFeeCap, gasTipCap) + + return gasFeeCap, gasTipCap, nil +} diff --git a/pkg/transaction/wrapped/fee_test.go b/pkg/transaction/wrapped/fee_test.go new file mode 100644 index 00000000000..77847ec40d1 --- /dev/null +++ b/pkg/transaction/wrapped/fee_test.go @@ -0,0 +1,128 @@ +// Copyright 2025 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package wrapped_test + +import ( + "context" + "errors" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethersphere/bee/v2/pkg/transaction/backendmock" + "github.com/ethersphere/bee/v2/pkg/transaction/wrapped" + "github.com/google/go-cmp/cmp" +) + +func TestSuggestedFeeAndTip(t *testing.T) { + t.Parallel() + + var ( + ctx = context.Background() + minimumGasTipCap = uint64(10) + baseFee = big.NewInt(100) + ) + + testCases := []struct { + name string + gasPrice *big.Int + boostPercent int + mockSuggestGasTip *big.Int + mockSuggestGasErr error + mockHeader *types.Header + mockHeaderErr error + wantGasFeeCap *big.Int + wantGasTipCap *big.Int + wantErr error + }{ + { + name: "with gas price", + gasPrice: big.NewInt(1000), + wantGasFeeCap: big.NewInt(1000), + wantGasTipCap: big.NewInt(1000), + }, + { + name: "suggest tip error", + mockSuggestGasErr: errors.New("suggest tip error"), + wantErr: errors.New("failed to suggest gas tip cap: suggest tip error"), + }, + { + name: "header error", + mockSuggestGasTip: big.NewInt(20), + mockHeaderErr: errors.New("header error"), + wantErr: errors.New("failed to get latest block header: header error"), + }, + { + name: "no base fee", + mockSuggestGasTip: big.NewInt(20), + mockHeader: &types.Header{}, + wantErr: wrapped.ErrEIP1559NotSupported, + }, + { + name: "suggested tip > minimum", + mockSuggestGasTip: big.NewInt(20), + mockHeader: &types.Header{BaseFee: baseFee}, + wantGasFeeCap: big.NewInt(220), // 2*100 + 20 + wantGasTipCap: big.NewInt(20), + }, + { + name: "suggested tip < minimum", + mockSuggestGasTip: big.NewInt(5), + mockHeader: &types.Header{BaseFee: baseFee}, + wantGasFeeCap: big.NewInt(210), // 2*100 + 10 + wantGasTipCap: big.NewInt(10), + }, + { + name: "with boost", + boostPercent: 10, + mockSuggestGasTip: big.NewInt(20), + mockHeader: &types.Header{BaseFee: baseFee}, + wantGasFeeCap: big.NewInt(222), // 2*100 + 22 + wantGasTipCap: big.NewInt(22), // 20 * 1.1 + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + backend := wrapped.NewBackend( + backendmock.New( + backendmock.WithSuggestGasTipCapFunc(func(ctx context.Context) (*big.Int, error) { + return tc.mockSuggestGasTip, tc.mockSuggestGasErr + }), + backendmock.WithHeaderbyNumberFunc(func(ctx context.Context, number *big.Int) (*types.Header, error) { + return tc.mockHeader, tc.mockHeaderErr + }), + ), + minimumGasTipCap, + ) + + gasFeeCap, gasTipCap, err := backend.SuggestedFeeAndTip(ctx, tc.gasPrice, tc.boostPercent) + + if tc.wantErr != nil { + if err == nil { + t.Fatal("expected error but got none") + } + if err.Error() != tc.wantErr.Error() { + t.Fatalf("unexpected error. want %v, got %v", tc.wantErr, err) + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if diff := cmp.Diff(tc.wantGasFeeCap.String(), gasFeeCap.String()); diff != "" { + t.Errorf("gasFeeCap mismatch (-want +got):\n%s", diff) + } + + if diff := cmp.Diff(tc.wantGasTipCap.String(), gasTipCap.String()); diff != "" { + t.Errorf("gasTipCap mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/transaction/wrapped/metrics.go b/pkg/transaction/wrapped/metrics.go index cb7ce3967c3..a5cfaa554a0 100644 --- a/pkg/transaction/wrapped/metrics.go +++ b/pkg/transaction/wrapped/metrics.go @@ -18,16 +18,14 @@ type metrics struct { BlockNumberCalls prometheus.Counter BlockHeaderCalls prometheus.Counter BalanceCalls prometheus.Counter - CodeAtCalls prometheus.Counter NonceAtCalls prometheus.Counter PendingNonceCalls prometheus.Counter CallContractCalls prometheus.Counter - SuggestGasPriceCalls prometheus.Counter + SuggestGasTipCapCalls prometheus.Counter EstimateGasCalls prometheus.Counter SendTransactionCalls prometheus.Counter FilterLogsCalls prometheus.Counter ChainIDCalls prometheus.Counter - BlockByNumberCalls prometheus.Counter } func newMetrics() metrics { @@ -76,12 +74,6 @@ func newMetrics() metrics { Name: "calls_balance", Help: "Count of eth_getBalance rpc calls", }), - CodeAtCalls: prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: m.Namespace, - Subsystem: subsystem, - Name: "calls_code_at", - Help: "Count of eth_getCode rpc calls", - }), NonceAtCalls: prometheus.NewCounter(prometheus.CounterOpts{ Namespace: m.Namespace, Subsystem: subsystem, @@ -100,11 +92,11 @@ func newMetrics() metrics { Name: "calls_eth_call", Help: "Count of eth_call rpc calls", }), - SuggestGasPriceCalls: prometheus.NewCounter(prometheus.CounterOpts{ + SuggestGasTipCapCalls: prometheus.NewCounter(prometheus.CounterOpts{ Namespace: m.Namespace, Subsystem: subsystem, - Name: "calls_suggest_gasprice", - Help: "Count of eth_suggestGasPrice rpc calls", + Name: "calls_suggest_gas_tip_cap", + Help: "Count of eth_maxPriorityFeePerGas rpc calls", }), EstimateGasCalls: prometheus.NewCounter(prometheus.CounterOpts{ Namespace: m.Namespace, @@ -130,12 +122,6 @@ func newMetrics() metrics { Name: "calls_chain_id", Help: "Count of eth_chainId rpc calls", }), - BlockByNumberCalls: prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: m.Namespace, - Subsystem: subsystem, - Name: "calls_block_by_number", - Help: "Count of eth_getBlockByNumber rpc calls", - }), } } diff --git a/pkg/transaction/wrapped/wrapped.go b/pkg/transaction/wrapped/wrapped.go index 87ec74da7e9..1a51a618a88 100644 --- a/pkg/transaction/wrapped/wrapped.go +++ b/pkg/transaction/wrapped/wrapped.go @@ -12,21 +12,25 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethclient" "github.com/ethersphere/bee/v2/pkg/transaction" + "github.com/ethersphere/bee/v2/pkg/transaction/backend" ) -var _ transaction.Backend = (*wrappedBackend)(nil) +var ( + _ transaction.Backend = (*wrappedBackend)(nil) +) type wrappedBackend struct { - backend *ethclient.Client - metrics metrics + backend backend.Geth + metrics metrics + minimumGasTipCap int64 } -func NewBackend(backend *ethclient.Client) transaction.Backend { +func NewBackend(backend backend.Geth, minimumGasTipCap uint64) transaction.Backend { return &wrappedBackend{ - backend: backend, - metrics: newMetrics(), + backend: backend, + minimumGasTipCap: int64(minimumGasTipCap), + metrics: newMetrics(), } } @@ -102,17 +106,6 @@ func (b *wrappedBackend) NonceAt(ctx context.Context, account common.Address, bl return nonce, nil } -func (b *wrappedBackend) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { - b.metrics.TotalRPCCalls.Inc() - b.metrics.CodeAtCalls.Inc() - code, err := b.backend.CodeAt(ctx, contract, blockNumber) - if err != nil { - b.metrics.TotalRPCErrors.Inc() - return nil, err - } - return code, nil -} - func (b *wrappedBackend) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { b.metrics.TotalRPCCalls.Inc() b.metrics.CallContractCalls.Inc() @@ -135,20 +128,9 @@ func (b *wrappedBackend) PendingNonceAt(ctx context.Context, account common.Addr return nonce, nil } -func (b *wrappedBackend) SuggestGasPrice(ctx context.Context) (*big.Int, error) { - b.metrics.TotalRPCCalls.Inc() - b.metrics.SuggestGasPriceCalls.Inc() - gasPrice, err := b.backend.SuggestGasPrice(ctx) - if err != nil { - b.metrics.TotalRPCErrors.Inc() - return nil, err - } - return gasPrice, nil -} - func (b *wrappedBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { b.metrics.TotalRPCCalls.Inc() - b.metrics.SuggestGasPriceCalls.Inc() + b.metrics.SuggestGasTipCapCalls.Inc() gasTipCap, err := b.backend.SuggestGasTipCap(ctx) if err != nil { b.metrics.TotalRPCErrors.Inc() @@ -201,21 +183,6 @@ func (b *wrappedBackend) ChainID(ctx context.Context) (*big.Int, error) { return chainID, nil } -// BlockByNumber implements transaction.Backend. -func (b *wrappedBackend) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { - b.metrics.TotalRPCCalls.Inc() - b.metrics.BlockByNumberCalls.Inc() - block, err := b.backend.BlockByNumber(ctx, number) - if err != nil { - if !errors.Is(err, ethereum.NotFound) { - b.metrics.TotalRPCErrors.Inc() - } - return nil, err - } - return block, nil -} - -func (b *wrappedBackend) Close() error { +func (b *wrappedBackend) Close() { b.backend.Close() - return nil }