diff --git a/pkg/.mockery.yaml b/pkg/.mockery.yaml index 955dd987ef..0e916995a9 100644 --- a/pkg/.mockery.yaml +++ b/pkg/.mockery.yaml @@ -53,7 +53,7 @@ packages: config: dir: txm outpkg: txm - mockname: "mock{{ .InterfaceName }}" + mockname: "Mock{{ .InterfaceName }}" filename: "mock_{{ .InterfaceName | snakecase }}_test.go" interfaces: Client: diff --git a/pkg/chains/legacyevm/evm_txm.go b/pkg/chains/legacyevm/evm_txm.go index 1c5e2acc6c..27cd2c73be 100644 --- a/pkg/chains/legacyevm/evm_txm.go +++ b/pkg/chains/legacyevm/evm_txm.go @@ -60,6 +60,7 @@ func newEvmTxm( logPoller, opts.KeyStore, estimator, + cfg.GasEstimator(), ) if cfg.Transactions().TransactionManagerV2().DualBroadcast() == nil || !*cfg.Transactions().TransactionManagerV2().DualBroadcast() { return txmv2, err diff --git a/pkg/txm/attempt_builder.go b/pkg/txm/attempt_builder.go index e2f84a9c3f..2e3fd53d82 100644 --- a/pkg/txm/attempt_builder.go +++ b/pkg/txm/attempt_builder.go @@ -2,6 +2,7 @@ package txm import ( "context" + "errors" "fmt" "math/big" @@ -13,24 +14,34 @@ import ( "github.com/smartcontractkit/chainlink-evm/pkg/gas" "github.com/smartcontractkit/chainlink-evm/pkg/keys" "github.com/smartcontractkit/chainlink-evm/pkg/txm/types" + "github.com/smartcontractkit/chainlink-framework/chains/fees" ) +// maxBumpThreshold controls the maximum number of bumps for an attempt. +const maxBumpThreshold = 5 + type attemptBuilder struct { gas.EvmFeeEstimator - priceMaxKey func(common.Address) *assets.Wei - keystore keys.TxSigner + priceMaxKey func(common.Address) *assets.Wei + keystore keys.TxSigner + emptyTxLimitDefault uint64 } -func NewAttemptBuilder(priceMaxKey func(common.Address) *assets.Wei, estimator gas.EvmFeeEstimator, keystore keys.TxSigner) *attemptBuilder { +func NewAttemptBuilder(priceMaxKey func(common.Address) *assets.Wei, estimator gas.EvmFeeEstimator, keystore keys.TxSigner, emptyTxLimitDefault uint64) *attemptBuilder { return &attemptBuilder{ - priceMaxKey: priceMaxKey, - EvmFeeEstimator: estimator, - keystore: keystore, + priceMaxKey: priceMaxKey, + EvmFeeEstimator: estimator, + keystore: keystore, + emptyTxLimitDefault: emptyTxLimitDefault, } } func (a *attemptBuilder) NewAttempt(ctx context.Context, lggr logger.Logger, tx *types.Transaction, dynamic bool) (*types.Attempt, error) { - fee, estimatedGasLimit, err := a.EvmFeeEstimator.GetFee(ctx, tx.Data, tx.SpecifiedGasLimit, a.priceMaxKey(tx.FromAddress), &tx.FromAddress, &tx.ToAddress) + gasLimit := tx.SpecifiedGasLimit + if tx.IsPurgeable { + gasLimit = a.emptyTxLimitDefault + } + fee, estimatedGasLimit, err := a.EvmFeeEstimator.GetFee(ctx, tx.Data, gasLimit, a.priceMaxKey(tx.FromAddress), &tx.FromAddress, &tx.ToAddress) if err != nil { return nil, err } @@ -42,13 +53,52 @@ func (a *attemptBuilder) NewAttempt(ctx context.Context, lggr logger.Logger, tx } func (a *attemptBuilder) NewBumpAttempt(ctx context.Context, lggr logger.Logger, tx *types.Transaction, previousAttempt types.Attempt) (*types.Attempt, error) { - bumpedFee, bumpedFeeLimit, err := a.EvmFeeEstimator.BumpFee(ctx, previousAttempt.Fee, tx.SpecifiedGasLimit, a.priceMaxKey(tx.FromAddress), nil) + gasLimit := tx.SpecifiedGasLimit + if tx.IsPurgeable { + gasLimit = a.emptyTxLimitDefault + } + bumpedFee, bumpedFeeLimit, err := a.EvmFeeEstimator.BumpFee(ctx, previousAttempt.Fee, gasLimit, a.priceMaxKey(tx.FromAddress), nil) if err != nil { return nil, err } return a.newCustomAttempt(ctx, tx, bumpedFee, bumpedFeeLimit, previousAttempt.Type, lggr) } +func (a *attemptBuilder) NewAgnosticBumpAttempt(ctx context.Context, lggr logger.Logger, tx *types.Transaction, dynamic bool) (*types.Attempt, error) { + attempt, err := a.NewAttempt(ctx, lggr, tx, dynamic) + if err != nil { + return nil, err + } + + // bump purge attempts + if tx.IsPurgeable { + // TODO: add better handling + for { + bumpedAttempt, err := a.NewBumpAttempt(ctx, lggr, tx, *attempt) + if err != nil { + if errors.Is(err, fees.ErrConnectivity) { + return attempt, nil + } + return nil, fmt.Errorf("error bumping attempt for txID: %v, err: %w", tx.ID, err) + } + attempt = bumpedAttempt + } + } else { + // bump regular attempts + bumps := min(maxBumpThreshold, tx.AttemptCount) + for range bumps { + bumpedAttempt, err := a.NewBumpAttempt(ctx, lggr, tx, *attempt) + if err != nil { + lggr.Errorf("error bumping attempt: %v for txID: %v", err, tx.ID) + return attempt, nil + } + attempt = bumpedAttempt + } + } + + return attempt, nil +} + func (a *attemptBuilder) newCustomAttempt( ctx context.Context, tx *types.Transaction, diff --git a/pkg/txm/attempt_builder_test.go b/pkg/txm/attempt_builder_test.go index b75c78f491..4df500de01 100644 --- a/pkg/txm/attempt_builder_test.go +++ b/pkg/txm/attempt_builder_test.go @@ -1,22 +1,28 @@ package txm import ( + "errors" "testing" + "github.com/ethereum/go-ethereum/common" evmtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-evm/pkg/assets" "github.com/smartcontractkit/chainlink-evm/pkg/gas" + "github.com/smartcontractkit/chainlink-evm/pkg/gas/mocks" "github.com/smartcontractkit/chainlink-evm/pkg/keys/keystest" "github.com/smartcontractkit/chainlink-evm/pkg/testutils" "github.com/smartcontractkit/chainlink-evm/pkg/txm/types" + "github.com/smartcontractkit/chainlink-framework/chains/fees" ) func TestAttemptBuilder_newLegacyAttempt(t *testing.T) { - ab := NewAttemptBuilder(nil, nil, keystest.TxSigner(nil)) + ab := NewAttemptBuilder(nil, nil, keystest.TxSigner(nil), 100) address := testutils.NewAddress() lggr := logger.Test(t) var gasLimit uint64 = 100 @@ -51,7 +57,7 @@ func TestAttemptBuilder_newLegacyAttempt(t *testing.T) { } func TestAttemptBuilder_newDynamicFeeAttempt(t *testing.T) { - ab := NewAttemptBuilder(nil, nil, keystest.TxSigner(nil)) + ab := NewAttemptBuilder(nil, nil, keystest.TxSigner(nil), 100) address := testutils.NewAddress() lggr := logger.Test(t) @@ -85,3 +91,214 @@ func TestAttemptBuilder_newDynamicFeeAttempt(t *testing.T) { assert.Equal(t, gasLimit, a.GasLimit) }) } + +func TestAttemptBuilder_NewAttempt(t *testing.T) { + mockEstimator := mocks.NewEvmFeeEstimator(t) + priceMaxKey := func(addr common.Address) *assets.Wei { + return assets.NewWeiI(1000) + } + var nonce uint64 = 1 + var gasLimit uint64 = 100 + ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), gasLimit) + address := testutils.NewAddress() + lggr := logger.Test(t) + + t.Run("creates legacy attempt with fields", func(t *testing.T) { + tx := &types.Transaction{ID: 10, FromAddress: address, Nonce: &nonce} + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{GasPrice: assets.NewWeiI(100)}, gasLimit, nil).Once() + a, err := ab.NewAttempt(t.Context(), lggr, tx, false) + require.NoError(t, err) + assert.Equal(t, tx.ID, a.TxID) + assert.Equal(t, evmtypes.LegacyTxType, int(a.Type)) + assert.NotNil(t, a.Fee.GasPrice) + assert.Equal(t, "100 wei", a.Fee.GasPrice.String()) + assert.Nil(t, a.Fee.GasTipCap) + assert.Nil(t, a.Fee.GasFeeCap) + assert.Equal(t, gasLimit, a.GasLimit) + }) + + t.Run("creates dynamic fee attempt with fields", func(t *testing.T) { + tx := &types.Transaction{ID: 10, FromAddress: address, Nonce: &nonce} + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{DynamicFee: gas.DynamicFee{GasTipCap: assets.NewWeiI(1), GasFeeCap: assets.NewWeiI(2)}}, gasLimit, nil).Once() + a, err := ab.NewAttempt(t.Context(), lggr, tx, true) + require.NoError(t, err) + assert.Equal(t, tx.ID, a.TxID) + assert.Equal(t, evmtypes.DynamicFeeTxType, int(a.Type)) + }) + + t.Run("creates purgeable attempt with fields", func(t *testing.T) { + tx := &types.Transaction{ID: 10, FromAddress: address, IsPurgeable: true, Nonce: &nonce} + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{GasPrice: assets.NewWeiI(100)}, gasLimit, nil).Once() + a, err := ab.NewAttempt(t.Context(), lggr, tx, false) + require.NoError(t, err) + assert.Equal(t, tx.ID, a.TxID) + assert.Equal(t, evmtypes.LegacyTxType, int(a.Type)) + }) + + t.Run("creates dynamic fee purgeable attempt with fields", func(t *testing.T) { + tx := &types.Transaction{ID: 10, FromAddress: address, IsPurgeable: true, Nonce: &nonce} + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{DynamicFee: gas.DynamicFee{GasTipCap: assets.NewWeiI(1), GasFeeCap: assets.NewWeiI(2)}}, gasLimit, nil).Once() + a, err := ab.NewAttempt(t.Context(), lggr, tx, true) + require.NoError(t, err) + assert.Equal(t, tx.ID, a.TxID) + assert.Equal(t, evmtypes.DynamicFeeTxType, int(a.Type)) + }) + + t.Run("fails if estimator returns error", func(t *testing.T) { + tx := &types.Transaction{ID: 10, FromAddress: address} + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{}, uint64(0), errors.New("estimator error")).Once() + _, err := ab.NewAttempt(t.Context(), lggr, tx, false) + require.Error(t, err) + assert.Contains(t, err.Error(), "estimator error") + mockEstimator.AssertExpectations(t) + }) +} + +func TestAttemptBuilder_NewAgnosticBumpAttempt(t *testing.T) { + address := testutils.NewAddress() + lggr := logger.Test(t) + var nonce uint64 = 77 + priceMaxKey := func(addr common.Address) *assets.Wei { + return assets.NewWeiI(1000) + } + + t.Run("returns original attempt when AttemptCount is 0", func(t *testing.T) { + mockEstimator := mocks.NewEvmFeeEstimator(t) + ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100) + + tx := &types.Transaction{ + ID: 10, + FromAddress: address, + Nonce: &nonce, + AttemptCount: 0, + } + + gasPrice := assets.NewWeiI(100) + initialFee := gas.EvmFee{GasPrice: gasPrice} + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(initialFee, uint64(21000), nil).Once() + + attempt, err := ab.NewAgnosticBumpAttempt(t.Context(), lggr, tx, false) + require.NoError(t, err) + assert.Equal(t, tx.ID, attempt.TxID) + assert.Equal(t, gasPrice.String(), attempt.Fee.GasPrice.String()) + assert.Equal(t, evmtypes.LegacyTxType, int(attempt.Type)) + mockEstimator.AssertExpectations(t) + }) + + t.Run("bumps once when AttemptCount is 1", func(t *testing.T) { + mockEstimator := mocks.NewEvmFeeEstimator(t) + ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100) + + tx := &types.Transaction{ + ID: 10, + FromAddress: address, + Nonce: &nonce, + AttemptCount: 1, + } + + gasPrice := assets.NewWeiI(100) + initialFee := gas.EvmFee{GasPrice: gasPrice} + bumpedFee := gas.EvmFee{GasPrice: gasPrice.Add(assets.NewWeiI(20))} + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(initialFee, uint64(21000), nil).Once() + mockEstimator.On("BumpFee", mock.Anything, initialFee, mock.Anything, mock.Anything, mock.Anything). + Return(bumpedFee, uint64(21000), nil).Once() + + attempt, err := ab.NewAgnosticBumpAttempt(t.Context(), lggr, tx, false) + require.NoError(t, err) + assert.Equal(t, tx.ID, attempt.TxID) + assert.Equal(t, bumpedFee.GasPrice.String(), attempt.Fee.GasPrice.String()) + mockEstimator.AssertExpectations(t) + }) + + t.Run("bumps N times when AttemptCount is N", func(t *testing.T) { + mockEstimator := mocks.NewEvmFeeEstimator(t) + ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100) + + tx := &types.Transaction{ + ID: 10, + FromAddress: address, + Nonce: &nonce, + AttemptCount: 3, + } + + initialFee := gas.EvmFee{GasPrice: assets.NewWeiI(100)} + firstBump := gas.EvmFee{GasPrice: assets.NewWeiI(110)} + secondBump := gas.EvmFee{GasPrice: assets.NewWeiI(121)} + thirdBump := gas.EvmFee{GasPrice: assets.NewWeiI(133)} + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(initialFee, uint64(21000), nil).Once() + mockEstimator.On("BumpFee", mock.Anything, initialFee, mock.Anything, mock.Anything, mock.Anything). + Return(firstBump, uint64(21000), nil).Once() + mockEstimator.On("BumpFee", mock.Anything, firstBump, mock.Anything, mock.Anything, mock.Anything). + Return(secondBump, uint64(21000), nil).Once() + mockEstimator.On("BumpFee", mock.Anything, secondBump, mock.Anything, mock.Anything, mock.Anything). + Return(thirdBump, uint64(21000), nil).Once() + + attempt, err := ab.NewAgnosticBumpAttempt(t.Context(), lggr, tx, false) + require.NoError(t, err) + assert.Equal(t, tx.ID, attempt.TxID) + assert.Equal(t, thirdBump.GasPrice.String(), attempt.Fee.GasPrice.String()) + mockEstimator.AssertExpectations(t) + }) + + t.Run("returns last valid attempt when BumpFee fails", func(t *testing.T) { + mockEstimator := mocks.NewEvmFeeEstimator(t) + ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100) + + tx := &types.Transaction{ + ID: 10, + FromAddress: address, + Nonce: &nonce, + AttemptCount: 3, + } + + gasPrice := assets.NewWeiI(100) + initialFee := gas.EvmFee{GasPrice: gasPrice} + firstBump := gas.EvmFee{GasPrice: gasPrice.Add(assets.NewWeiI(20))} + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(initialFee, uint64(21000), nil).Once() + mockEstimator.On("BumpFee", mock.Anything, initialFee, mock.Anything, mock.Anything, mock.Anything). + Return(firstBump, uint64(21000), nil).Once() + mockEstimator.On("BumpFee", mock.Anything, firstBump, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{}, uint64(0), fees.ErrConnectivity).Once() + + attempt, err := ab.NewAgnosticBumpAttempt(t.Context(), lggr, tx, false) + require.NoError(t, err) + assert.Equal(t, tx.ID, attempt.TxID) + // Should return the last valid bumped attempt + assert.Equal(t, firstBump.GasPrice.String(), attempt.Fee.GasPrice.String()) + mockEstimator.AssertExpectations(t) + }) + + t.Run("caps bumps at maxBumpThreshold", func(t *testing.T) { + mockEstimator := mocks.NewEvmFeeEstimator(t) + ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100) + + tx := &types.Transaction{ + ID: 10, + FromAddress: address, + Nonce: &nonce, + AttemptCount: 10, // More than maxBumpThreshold (5) + } + + initialFee := gas.EvmFee{GasPrice: assets.NewWeiI(100)} + bumpedFee := gas.EvmFee{GasPrice: initialFee.GasPrice.Add(assets.NewWeiI(20))} + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(initialFee, uint64(21000), nil).Once() + // Should only bump 5 times (maxBumpThreshold) + mockEstimator.On("BumpFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(bumpedFee, uint64(21000), nil).Times(5) + + attempt, err := ab.NewAgnosticBumpAttempt(t.Context(), lggr, tx, false) + require.NoError(t, err) + assert.Equal(t, tx.ID, attempt.TxID) + mockEstimator.AssertExpectations(t) + }) +} diff --git a/pkg/txm/clientwrappers/dualbroadcast/meta_client.go b/pkg/txm/clientwrappers/dualbroadcast/meta_client.go index a12b77d31a..2ef5fca362 100644 --- a/pkg/txm/clientwrappers/dualbroadcast/meta_client.go +++ b/pkg/txm/clientwrappers/dualbroadcast/meta_client.go @@ -27,8 +27,11 @@ import ( ) const ( - timeout = time.Second * 5 - metaABI = `[ + timeout = time.Second * 5 + NoBidsError = "no bids" + NoSolverOps = "no solver operations received" + NoSolverOpsAfterSimulation = "no valid solver operations after simulation" + metaABI = `[ { "type": "function", "name": "metacall", @@ -142,7 +145,7 @@ func NewMetaClient(lggr logger.Logger, c MetaClientRPC, ks MetaClientKeystore, c } return &MetaClient{ - lggr: logger.Sugared(logger.Named(lggr, "Txm.Txm.MetaClient")), + lggr: logger.Sugared(logger.Named(lggr, "Txm.MetaClient")), c: c, ks: ks, customURL: customURL, @@ -179,7 +182,7 @@ func (a *MetaClient) SendTransaction(ctx context.Context, tx *types.Transaction, return nil } a.lggr.Infof("No bids for transactionID(%d): ", tx.ID) - return nil + return errors.New(NoBidsError) } a.lggr.Infow("Broadcasting attempt to public mempool", "tx", tx) return a.c.SendTransaction(ctx, attempt.SignedTransaction) @@ -355,7 +358,7 @@ func (a *MetaClient) SendRequest(parentCtx context.Context, tx *types.Transactio } if response.Error.ErrorMessage != "" { - if strings.Contains(response.Error.ErrorMessage, "no solver operations received") { + if strings.Contains(response.Error.ErrorMessage, NoSolverOps) || strings.Contains(response.Error.ErrorMessage, NoSolverOpsAfterSimulation) { a.metrics.RecordBidsReceived(ctx, 0) return nil, nil } @@ -521,7 +524,7 @@ func (a *MetaClient) SendOperation(ctx context.Context, tx *types.Transaction, a if err != nil { return fmt.Errorf("failed to sign attempt for txID: %v, err: %w", tx.ID, err) } - a.lggr.Infow("Intercepted attempt for tx", "txID", tx.ID, "toAddress", meta.ToAddress, "gasLimit", meta.GasLimit, + a.lggr.Infow("Intercepted attempt for tx", "txID", tx.ID, "hash", signedTx.Hash(), "toAddress", meta.ToAddress, "gasLimit", meta.GasLimit, "TipCap", tip, "FeeCap", meta.MaxFeePerGas) return a.c.SendTransaction(ctx, signedTx) } diff --git a/pkg/txm/clientwrappers/dualbroadcast/meta_error_handler.go b/pkg/txm/clientwrappers/dualbroadcast/meta_error_handler.go new file mode 100644 index 0000000000..a15f3b001b --- /dev/null +++ b/pkg/txm/clientwrappers/dualbroadcast/meta_error_handler.go @@ -0,0 +1,31 @@ +package dualbroadcast + +import ( + "context" + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-evm/pkg/txm" + "github.com/smartcontractkit/chainlink-evm/pkg/txm/types" +) + +type errorHandler struct{} + +func NewErrorHandler() *errorHandler { + return &errorHandler{} +} + +func (e *errorHandler) HandleError(ctx context.Context, tx *types.Transaction, txErr error, txStore txm.TxStore, setNonce func(common.Address, uint64), isFromBroadcastMethod bool) error { + // If this isn't the first broadcast, don't mark the tx as fatal as other txs might be included on-chain. + if strings.Contains(txErr.Error(), NoBidsError) && tx.AttemptCount == 1 { + if err := txStore.MarkTxFatal(ctx, tx, tx.FromAddress); err != nil { + return err + } + setNonce(tx.FromAddress, *tx.Nonce) + return fmt.Errorf("transaction with txID: %d marked as fatal", tx.ID) + } + + return txErr +} diff --git a/pkg/txm/clientwrappers/dualbroadcast/meta_error_handler_test.go b/pkg/txm/clientwrappers/dualbroadcast/meta_error_handler_test.go new file mode 100644 index 0000000000..fdba838e0e --- /dev/null +++ b/pkg/txm/clientwrappers/dualbroadcast/meta_error_handler_test.go @@ -0,0 +1,84 @@ +package dualbroadcast + +import ( + "errors" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-evm/pkg/assets" + "github.com/smartcontractkit/chainlink-evm/pkg/gas" + "github.com/smartcontractkit/chainlink-evm/pkg/testutils" + "github.com/smartcontractkit/chainlink-evm/pkg/txm/storage" + "github.com/smartcontractkit/chainlink-evm/pkg/txm/types" +) + +func TestMetaErrorHandler(t *testing.T) { + errorHandler := NewErrorHandler() + require.NotNil(t, errorHandler) + + t.Run("handles no bids error for first attempt", func(t *testing.T) { + nonce := uint64(1) + address := testutils.NewAddress() + txRequest := &types.TxRequest{ + ChainID: testutils.FixtureChainID, + FromAddress: address, + ToAddress: testutils.NewAddress(), + } + setNonce := func(address common.Address, nonce uint64) {} + txStoreManager := storage.NewInMemoryStoreManager(logger.Test(t), testutils.FixtureChainID) + require.NoError(t, txStoreManager.Add(address)) + txStore := txStoreManager.InMemoryStoreMap[address] + _ = txStore.CreateTransaction(txRequest) + tx, err := txStore.UpdateUnstartedTransactionWithNonce(nonce) + require.NoError(t, err) + attempt := &types.Attempt{ + TxID: tx.ID, + Fee: gas.EvmFee{GasPrice: assets.NewWeiI(1)}, + GasLimit: 22000, + Hash: testutils.NewHash(), + } + require.NoError(t, txStore.AppendAttemptToTransaction(*tx.Nonce, attempt)) + tx, _ = txStore.FetchUnconfirmedTransactionAtNonceWithCount(nonce) + err = errorHandler.HandleError(t.Context(), tx, errors.New("no bids"), txStoreManager, setNonce, false) + require.Error(t, err) + require.ErrorContains(t, err, "transaction with txID: 0 marked as fatal") + _, unconfirmedCount := txStore.FetchUnconfirmedTransactionAtNonceWithCount(nonce) + assert.Equal(t, 0, unconfirmedCount) + }) + + t.Run("returns txErr if not the first attempt", func(t *testing.T) { + nonce := uint64(1) + address := testutils.NewAddress() + txRequest := &types.TxRequest{ + ChainID: testutils.FixtureChainID, + FromAddress: address, + ToAddress: testutils.NewAddress(), + } + txErr := errors.New("no bids") + setNonce := func(address common.Address, nonce uint64) {} + txStoreManager := storage.NewInMemoryStoreManager(logger.Test(t), testutils.FixtureChainID) + require.NoError(t, txStoreManager.Add(address)) + txStore := txStoreManager.InMemoryStoreMap[address] + _ = txStore.CreateTransaction(txRequest) + tx, err := txStore.UpdateUnstartedTransactionWithNonce(nonce) + require.NoError(t, err) + attempt := &types.Attempt{ + TxID: tx.ID, + Fee: gas.EvmFee{GasPrice: assets.NewWeiI(1)}, + GasLimit: 22000, + Hash: testutils.NewHash(), + } + require.NoError(t, txStore.AppendAttemptToTransaction(*tx.Nonce, attempt)) + require.NoError(t, txStore.AppendAttemptToTransaction(*tx.Nonce, attempt)) + tx, _ = txStore.FetchUnconfirmedTransactionAtNonceWithCount(nonce) + err = errorHandler.HandleError(t.Context(), tx, txErr, txStoreManager, setNonce, false) + require.Error(t, err) + require.ErrorIs(t, err, txErr) + _, unconfirmedCount := txStore.FetchUnconfirmedTransactionAtNonceWithCount(nonce) + assert.Equal(t, 1, unconfirmedCount) + }) +} diff --git a/pkg/txm/clientwrappers/dualbroadcast/selector.go b/pkg/txm/clientwrappers/dualbroadcast/selector.go index 89efef8bf3..91db3a6008 100644 --- a/pkg/txm/clientwrappers/dualbroadcast/selector.go +++ b/pkg/txm/clientwrappers/dualbroadcast/selector.go @@ -12,12 +12,16 @@ import ( "github.com/smartcontractkit/chainlink-evm/pkg/txm" ) -func SelectClient(lggr logger.Logger, client client.Client, keyStore keys.ChainStore, url *url.URL, chainID *big.Int) (txm.Client, error) { +func SelectClient(lggr logger.Logger, client client.Client, keyStore keys.ChainStore, url *url.URL, chainID *big.Int) (txm.Client, txm.ErrorHandler, error) { urlString := url.String() switch { case strings.Contains(urlString, "flashbots"): - return NewFlashbotsClient(client, keyStore, url), nil + return NewFlashbotsClient(client, keyStore, url), nil, nil default: - return NewMetaClient(lggr, client, keyStore, url, chainID) + mc, err := NewMetaClient(lggr, client, keyStore, url, chainID) + if err != nil { + return nil, nil, err + } + return mc, NewErrorHandler(), nil } } diff --git a/pkg/txm/mock_attempt_builder_test.go b/pkg/txm/mock_attempt_builder_test.go index d61b746157..ed4bbdd324 100644 --- a/pkg/txm/mock_attempt_builder_test.go +++ b/pkg/txm/mock_attempt_builder_test.go @@ -11,34 +11,34 @@ import ( types "github.com/smartcontractkit/chainlink-evm/pkg/txm/types" ) -// mockAttemptBuilder is an autogenerated mock type for the AttemptBuilder type -type mockAttemptBuilder struct { +// MockAttemptBuilder is an autogenerated mock type for the AttemptBuilder type +type MockAttemptBuilder struct { mock.Mock } -type mockAttemptBuilder_Expecter struct { +type MockAttemptBuilder_Expecter struct { mock *mock.Mock } -func (_m *mockAttemptBuilder) EXPECT() *mockAttemptBuilder_Expecter { - return &mockAttemptBuilder_Expecter{mock: &_m.Mock} +func (_m *MockAttemptBuilder) EXPECT() *MockAttemptBuilder_Expecter { + return &MockAttemptBuilder_Expecter{mock: &_m.Mock} } -// NewAttempt provides a mock function with given fields: _a0, _a1, _a2, _a3 -func (_m *mockAttemptBuilder) NewAttempt(_a0 context.Context, _a1 logger.Logger, _a2 *types.Transaction, _a3 bool) (*types.Attempt, error) { - ret := _m.Called(_a0, _a1, _a2, _a3) +// NewAgnosticBumpAttempt provides a mock function with given fields: ctx, lggr, tx, dynamic +func (_m *MockAttemptBuilder) NewAgnosticBumpAttempt(ctx context.Context, lggr logger.Logger, tx *types.Transaction, dynamic bool) (*types.Attempt, error) { + ret := _m.Called(ctx, lggr, tx, dynamic) if len(ret) == 0 { - panic("no return value specified for NewAttempt") + panic("no return value specified for NewAgnosticBumpAttempt") } var r0 *types.Attempt var r1 error if rf, ok := ret.Get(0).(func(context.Context, logger.Logger, *types.Transaction, bool) (*types.Attempt, error)); ok { - return rf(_a0, _a1, _a2, _a3) + return rf(ctx, lggr, tx, dynamic) } if rf, ok := ret.Get(0).(func(context.Context, logger.Logger, *types.Transaction, bool) *types.Attempt); ok { - r0 = rf(_a0, _a1, _a2, _a3) + r0 = rf(ctx, lggr, tx, dynamic) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*types.Attempt) @@ -46,7 +46,7 @@ func (_m *mockAttemptBuilder) NewAttempt(_a0 context.Context, _a1 logger.Logger, } if rf, ok := ret.Get(1).(func(context.Context, logger.Logger, *types.Transaction, bool) error); ok { - r1 = rf(_a0, _a1, _a2, _a3) + r1 = rf(ctx, lggr, tx, dynamic) } else { r1 = ret.Error(1) } @@ -54,105 +54,44 @@ func (_m *mockAttemptBuilder) NewAttempt(_a0 context.Context, _a1 logger.Logger, return r0, r1 } -// mockAttemptBuilder_NewAttempt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NewAttempt' -type mockAttemptBuilder_NewAttempt_Call struct { +// MockAttemptBuilder_NewAgnosticBumpAttempt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NewAgnosticBumpAttempt' +type MockAttemptBuilder_NewAgnosticBumpAttempt_Call struct { *mock.Call } -// NewAttempt is a helper method to define mock.On call -// - _a0 context.Context -// - _a1 logger.Logger -// - _a2 *types.Transaction -// - _a3 bool -func (_e *mockAttemptBuilder_Expecter) NewAttempt(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}) *mockAttemptBuilder_NewAttempt_Call { - return &mockAttemptBuilder_NewAttempt_Call{Call: _e.mock.On("NewAttempt", _a0, _a1, _a2, _a3)} +// NewAgnosticBumpAttempt is a helper method to define mock.On call +// - ctx context.Context +// - lggr logger.Logger +// - tx *types.Transaction +// - dynamic bool +func (_e *MockAttemptBuilder_Expecter) NewAgnosticBumpAttempt(ctx interface{}, lggr interface{}, tx interface{}, dynamic interface{}) *MockAttemptBuilder_NewAgnosticBumpAttempt_Call { + return &MockAttemptBuilder_NewAgnosticBumpAttempt_Call{Call: _e.mock.On("NewAgnosticBumpAttempt", ctx, lggr, tx, dynamic)} } -func (_c *mockAttemptBuilder_NewAttempt_Call) Run(run func(_a0 context.Context, _a1 logger.Logger, _a2 *types.Transaction, _a3 bool)) *mockAttemptBuilder_NewAttempt_Call { +func (_c *MockAttemptBuilder_NewAgnosticBumpAttempt_Call) Run(run func(ctx context.Context, lggr logger.Logger, tx *types.Transaction, dynamic bool)) *MockAttemptBuilder_NewAgnosticBumpAttempt_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(logger.Logger), args[2].(*types.Transaction), args[3].(bool)) }) return _c } -func (_c *mockAttemptBuilder_NewAttempt_Call) Return(_a0 *types.Attempt, _a1 error) *mockAttemptBuilder_NewAttempt_Call { +func (_c *MockAttemptBuilder_NewAgnosticBumpAttempt_Call) Return(_a0 *types.Attempt, _a1 error) *MockAttemptBuilder_NewAgnosticBumpAttempt_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *mockAttemptBuilder_NewAttempt_Call) RunAndReturn(run func(context.Context, logger.Logger, *types.Transaction, bool) (*types.Attempt, error)) *mockAttemptBuilder_NewAttempt_Call { +func (_c *MockAttemptBuilder_NewAgnosticBumpAttempt_Call) RunAndReturn(run func(context.Context, logger.Logger, *types.Transaction, bool) (*types.Attempt, error)) *MockAttemptBuilder_NewAgnosticBumpAttempt_Call { _c.Call.Return(run) return _c } -// NewBumpAttempt provides a mock function with given fields: _a0, _a1, _a2, _a3 -func (_m *mockAttemptBuilder) NewBumpAttempt(_a0 context.Context, _a1 logger.Logger, _a2 *types.Transaction, _a3 types.Attempt) (*types.Attempt, error) { - ret := _m.Called(_a0, _a1, _a2, _a3) - - if len(ret) == 0 { - panic("no return value specified for NewBumpAttempt") - } - - var r0 *types.Attempt - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, logger.Logger, *types.Transaction, types.Attempt) (*types.Attempt, error)); ok { - return rf(_a0, _a1, _a2, _a3) - } - if rf, ok := ret.Get(0).(func(context.Context, logger.Logger, *types.Transaction, types.Attempt) *types.Attempt); ok { - r0 = rf(_a0, _a1, _a2, _a3) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Attempt) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, logger.Logger, *types.Transaction, types.Attempt) error); ok { - r1 = rf(_a0, _a1, _a2, _a3) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockAttemptBuilder_NewBumpAttempt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NewBumpAttempt' -type mockAttemptBuilder_NewBumpAttempt_Call struct { - *mock.Call -} - -// NewBumpAttempt is a helper method to define mock.On call -// - _a0 context.Context -// - _a1 logger.Logger -// - _a2 *types.Transaction -// - _a3 types.Attempt -func (_e *mockAttemptBuilder_Expecter) NewBumpAttempt(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}) *mockAttemptBuilder_NewBumpAttempt_Call { - return &mockAttemptBuilder_NewBumpAttempt_Call{Call: _e.mock.On("NewBumpAttempt", _a0, _a1, _a2, _a3)} -} - -func (_c *mockAttemptBuilder_NewBumpAttempt_Call) Run(run func(_a0 context.Context, _a1 logger.Logger, _a2 *types.Transaction, _a3 types.Attempt)) *mockAttemptBuilder_NewBumpAttempt_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(logger.Logger), args[2].(*types.Transaction), args[3].(types.Attempt)) - }) - return _c -} - -func (_c *mockAttemptBuilder_NewBumpAttempt_Call) Return(_a0 *types.Attempt, _a1 error) *mockAttemptBuilder_NewBumpAttempt_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockAttemptBuilder_NewBumpAttempt_Call) RunAndReturn(run func(context.Context, logger.Logger, *types.Transaction, types.Attempt) (*types.Attempt, error)) *mockAttemptBuilder_NewBumpAttempt_Call { - _c.Call.Return(run) - return _c -} - -// newMockAttemptBuilder creates a new instance of mockAttemptBuilder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// NewMockAttemptBuilder creates a new instance of MockAttemptBuilder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func newMockAttemptBuilder(t interface { +func NewMockAttemptBuilder(t interface { mock.TestingT Cleanup(func()) -}) *mockAttemptBuilder { - mock := &mockAttemptBuilder{} +}) *MockAttemptBuilder { + mock := &MockAttemptBuilder{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) diff --git a/pkg/txm/mock_client_test.go b/pkg/txm/mock_client_test.go index 5c1585460e..549a1c851d 100644 --- a/pkg/txm/mock_client_test.go +++ b/pkg/txm/mock_client_test.go @@ -13,21 +13,21 @@ import ( types "github.com/smartcontractkit/chainlink-evm/pkg/txm/types" ) -// mockClient is an autogenerated mock type for the Client type -type mockClient struct { +// MockClient is an autogenerated mock type for the Client type +type MockClient struct { mock.Mock } -type mockClient_Expecter struct { +type MockClient_Expecter struct { mock *mock.Mock } -func (_m *mockClient) EXPECT() *mockClient_Expecter { - return &mockClient_Expecter{mock: &_m.Mock} +func (_m *MockClient) EXPECT() *MockClient_Expecter { + return &MockClient_Expecter{mock: &_m.Mock} } // NonceAt provides a mock function with given fields: _a0, _a1, _a2 -func (_m *mockClient) NonceAt(_a0 context.Context, _a1 common.Address, _a2 *big.Int) (uint64, error) { +func (_m *MockClient) NonceAt(_a0 context.Context, _a1 common.Address, _a2 *big.Int) (uint64, error) { ret := _m.Called(_a0, _a1, _a2) if len(ret) == 0 { @@ -54,8 +54,8 @@ func (_m *mockClient) NonceAt(_a0 context.Context, _a1 common.Address, _a2 *big. return r0, r1 } -// mockClient_NonceAt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NonceAt' -type mockClient_NonceAt_Call struct { +// MockClient_NonceAt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NonceAt' +type MockClient_NonceAt_Call struct { *mock.Call } @@ -63,29 +63,29 @@ type mockClient_NonceAt_Call struct { // - _a0 context.Context // - _a1 common.Address // - _a2 *big.Int -func (_e *mockClient_Expecter) NonceAt(_a0 interface{}, _a1 interface{}, _a2 interface{}) *mockClient_NonceAt_Call { - return &mockClient_NonceAt_Call{Call: _e.mock.On("NonceAt", _a0, _a1, _a2)} +func (_e *MockClient_Expecter) NonceAt(_a0 interface{}, _a1 interface{}, _a2 interface{}) *MockClient_NonceAt_Call { + return &MockClient_NonceAt_Call{Call: _e.mock.On("NonceAt", _a0, _a1, _a2)} } -func (_c *mockClient_NonceAt_Call) Run(run func(_a0 context.Context, _a1 common.Address, _a2 *big.Int)) *mockClient_NonceAt_Call { +func (_c *MockClient_NonceAt_Call) Run(run func(_a0 context.Context, _a1 common.Address, _a2 *big.Int)) *MockClient_NonceAt_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(common.Address), args[2].(*big.Int)) }) return _c } -func (_c *mockClient_NonceAt_Call) Return(_a0 uint64, _a1 error) *mockClient_NonceAt_Call { +func (_c *MockClient_NonceAt_Call) Return(_a0 uint64, _a1 error) *MockClient_NonceAt_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *mockClient_NonceAt_Call) RunAndReturn(run func(context.Context, common.Address, *big.Int) (uint64, error)) *mockClient_NonceAt_Call { +func (_c *MockClient_NonceAt_Call) RunAndReturn(run func(context.Context, common.Address, *big.Int) (uint64, error)) *MockClient_NonceAt_Call { _c.Call.Return(run) return _c } // PendingNonceAt provides a mock function with given fields: _a0, _a1 -func (_m *mockClient) PendingNonceAt(_a0 context.Context, _a1 common.Address) (uint64, error) { +func (_m *MockClient) PendingNonceAt(_a0 context.Context, _a1 common.Address) (uint64, error) { ret := _m.Called(_a0, _a1) if len(ret) == 0 { @@ -112,37 +112,37 @@ func (_m *mockClient) PendingNonceAt(_a0 context.Context, _a1 common.Address) (u return r0, r1 } -// mockClient_PendingNonceAt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PendingNonceAt' -type mockClient_PendingNonceAt_Call struct { +// MockClient_PendingNonceAt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PendingNonceAt' +type MockClient_PendingNonceAt_Call struct { *mock.Call } // PendingNonceAt is a helper method to define mock.On call // - _a0 context.Context // - _a1 common.Address -func (_e *mockClient_Expecter) PendingNonceAt(_a0 interface{}, _a1 interface{}) *mockClient_PendingNonceAt_Call { - return &mockClient_PendingNonceAt_Call{Call: _e.mock.On("PendingNonceAt", _a0, _a1)} +func (_e *MockClient_Expecter) PendingNonceAt(_a0 interface{}, _a1 interface{}) *MockClient_PendingNonceAt_Call { + return &MockClient_PendingNonceAt_Call{Call: _e.mock.On("PendingNonceAt", _a0, _a1)} } -func (_c *mockClient_PendingNonceAt_Call) Run(run func(_a0 context.Context, _a1 common.Address)) *mockClient_PendingNonceAt_Call { +func (_c *MockClient_PendingNonceAt_Call) Run(run func(_a0 context.Context, _a1 common.Address)) *MockClient_PendingNonceAt_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(common.Address)) }) return _c } -func (_c *mockClient_PendingNonceAt_Call) Return(_a0 uint64, _a1 error) *mockClient_PendingNonceAt_Call { +func (_c *MockClient_PendingNonceAt_Call) Return(_a0 uint64, _a1 error) *MockClient_PendingNonceAt_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *mockClient_PendingNonceAt_Call) RunAndReturn(run func(context.Context, common.Address) (uint64, error)) *mockClient_PendingNonceAt_Call { +func (_c *MockClient_PendingNonceAt_Call) RunAndReturn(run func(context.Context, common.Address) (uint64, error)) *MockClient_PendingNonceAt_Call { _c.Call.Return(run) return _c } // SendTransaction provides a mock function with given fields: ctx, tx, attempt -func (_m *mockClient) SendTransaction(ctx context.Context, tx *types.Transaction, attempt *types.Attempt) error { +func (_m *MockClient) SendTransaction(ctx context.Context, tx *types.Transaction, attempt *types.Attempt) error { ret := _m.Called(ctx, tx, attempt) if len(ret) == 0 { @@ -159,8 +159,8 @@ func (_m *mockClient) SendTransaction(ctx context.Context, tx *types.Transaction return r0 } -// mockClient_SendTransaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendTransaction' -type mockClient_SendTransaction_Call struct { +// MockClient_SendTransaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendTransaction' +type MockClient_SendTransaction_Call struct { *mock.Call } @@ -168,34 +168,34 @@ type mockClient_SendTransaction_Call struct { // - ctx context.Context // - tx *types.Transaction // - attempt *types.Attempt -func (_e *mockClient_Expecter) SendTransaction(ctx interface{}, tx interface{}, attempt interface{}) *mockClient_SendTransaction_Call { - return &mockClient_SendTransaction_Call{Call: _e.mock.On("SendTransaction", ctx, tx, attempt)} +func (_e *MockClient_Expecter) SendTransaction(ctx interface{}, tx interface{}, attempt interface{}) *MockClient_SendTransaction_Call { + return &MockClient_SendTransaction_Call{Call: _e.mock.On("SendTransaction", ctx, tx, attempt)} } -func (_c *mockClient_SendTransaction_Call) Run(run func(ctx context.Context, tx *types.Transaction, attempt *types.Attempt)) *mockClient_SendTransaction_Call { +func (_c *MockClient_SendTransaction_Call) Run(run func(ctx context.Context, tx *types.Transaction, attempt *types.Attempt)) *MockClient_SendTransaction_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(*types.Transaction), args[2].(*types.Attempt)) }) return _c } -func (_c *mockClient_SendTransaction_Call) Return(_a0 error) *mockClient_SendTransaction_Call { +func (_c *MockClient_SendTransaction_Call) Return(_a0 error) *MockClient_SendTransaction_Call { _c.Call.Return(_a0) return _c } -func (_c *mockClient_SendTransaction_Call) RunAndReturn(run func(context.Context, *types.Transaction, *types.Attempt) error) *mockClient_SendTransaction_Call { +func (_c *MockClient_SendTransaction_Call) RunAndReturn(run func(context.Context, *types.Transaction, *types.Attempt) error) *MockClient_SendTransaction_Call { _c.Call.Return(run) return _c } -// newMockClient creates a new instance of mockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func newMockClient(t interface { +func NewMockClient(t interface { mock.TestingT Cleanup(func()) -}) *mockClient { - mock := &mockClient{} +}) *MockClient { + mock := &MockClient{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) diff --git a/pkg/txm/mock_tx_store_test.go b/pkg/txm/mock_tx_store_test.go index 6ba73686d6..2c2388d93f 100644 --- a/pkg/txm/mock_tx_store_test.go +++ b/pkg/txm/mock_tx_store_test.go @@ -12,21 +12,21 @@ import ( types "github.com/smartcontractkit/chainlink-evm/pkg/txm/types" ) -// mockTxStore is an autogenerated mock type for the TxStore type -type mockTxStore struct { +// MockTxStore is an autogenerated mock type for the TxStore type +type MockTxStore struct { mock.Mock } -type mockTxStore_Expecter struct { +type MockTxStore_Expecter struct { mock *mock.Mock } -func (_m *mockTxStore) EXPECT() *mockTxStore_Expecter { - return &mockTxStore_Expecter{mock: &_m.Mock} +func (_m *MockTxStore) EXPECT() *MockTxStore_Expecter { + return &MockTxStore_Expecter{mock: &_m.Mock} } // AbandonPendingTransactions provides a mock function with given fields: _a0, _a1 -func (_m *mockTxStore) AbandonPendingTransactions(_a0 context.Context, _a1 common.Address) error { +func (_m *MockTxStore) AbandonPendingTransactions(_a0 context.Context, _a1 common.Address) error { ret := _m.Called(_a0, _a1) if len(ret) == 0 { @@ -43,37 +43,37 @@ func (_m *mockTxStore) AbandonPendingTransactions(_a0 context.Context, _a1 commo return r0 } -// mockTxStore_AbandonPendingTransactions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AbandonPendingTransactions' -type mockTxStore_AbandonPendingTransactions_Call struct { +// MockTxStore_AbandonPendingTransactions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AbandonPendingTransactions' +type MockTxStore_AbandonPendingTransactions_Call struct { *mock.Call } // AbandonPendingTransactions is a helper method to define mock.On call // - _a0 context.Context // - _a1 common.Address -func (_e *mockTxStore_Expecter) AbandonPendingTransactions(_a0 interface{}, _a1 interface{}) *mockTxStore_AbandonPendingTransactions_Call { - return &mockTxStore_AbandonPendingTransactions_Call{Call: _e.mock.On("AbandonPendingTransactions", _a0, _a1)} +func (_e *MockTxStore_Expecter) AbandonPendingTransactions(_a0 interface{}, _a1 interface{}) *MockTxStore_AbandonPendingTransactions_Call { + return &MockTxStore_AbandonPendingTransactions_Call{Call: _e.mock.On("AbandonPendingTransactions", _a0, _a1)} } -func (_c *mockTxStore_AbandonPendingTransactions_Call) Run(run func(_a0 context.Context, _a1 common.Address)) *mockTxStore_AbandonPendingTransactions_Call { +func (_c *MockTxStore_AbandonPendingTransactions_Call) Run(run func(_a0 context.Context, _a1 common.Address)) *MockTxStore_AbandonPendingTransactions_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(common.Address)) }) return _c } -func (_c *mockTxStore_AbandonPendingTransactions_Call) Return(_a0 error) *mockTxStore_AbandonPendingTransactions_Call { +func (_c *MockTxStore_AbandonPendingTransactions_Call) Return(_a0 error) *MockTxStore_AbandonPendingTransactions_Call { _c.Call.Return(_a0) return _c } -func (_c *mockTxStore_AbandonPendingTransactions_Call) RunAndReturn(run func(context.Context, common.Address) error) *mockTxStore_AbandonPendingTransactions_Call { +func (_c *MockTxStore_AbandonPendingTransactions_Call) RunAndReturn(run func(context.Context, common.Address) error) *MockTxStore_AbandonPendingTransactions_Call { _c.Call.Return(run) return _c } // AppendAttemptToTransaction provides a mock function with given fields: _a0, _a1, _a2, _a3 -func (_m *mockTxStore) AppendAttemptToTransaction(_a0 context.Context, _a1 uint64, _a2 common.Address, _a3 *types.Attempt) error { +func (_m *MockTxStore) AppendAttemptToTransaction(_a0 context.Context, _a1 uint64, _a2 common.Address, _a3 *types.Attempt) error { ret := _m.Called(_a0, _a1, _a2, _a3) if len(ret) == 0 { @@ -90,8 +90,8 @@ func (_m *mockTxStore) AppendAttemptToTransaction(_a0 context.Context, _a1 uint6 return r0 } -// mockTxStore_AppendAttemptToTransaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AppendAttemptToTransaction' -type mockTxStore_AppendAttemptToTransaction_Call struct { +// MockTxStore_AppendAttemptToTransaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AppendAttemptToTransaction' +type MockTxStore_AppendAttemptToTransaction_Call struct { *mock.Call } @@ -100,29 +100,29 @@ type mockTxStore_AppendAttemptToTransaction_Call struct { // - _a1 uint64 // - _a2 common.Address // - _a3 *types.Attempt -func (_e *mockTxStore_Expecter) AppendAttemptToTransaction(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}) *mockTxStore_AppendAttemptToTransaction_Call { - return &mockTxStore_AppendAttemptToTransaction_Call{Call: _e.mock.On("AppendAttemptToTransaction", _a0, _a1, _a2, _a3)} +func (_e *MockTxStore_Expecter) AppendAttemptToTransaction(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}) *MockTxStore_AppendAttemptToTransaction_Call { + return &MockTxStore_AppendAttemptToTransaction_Call{Call: _e.mock.On("AppendAttemptToTransaction", _a0, _a1, _a2, _a3)} } -func (_c *mockTxStore_AppendAttemptToTransaction_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 common.Address, _a3 *types.Attempt)) *mockTxStore_AppendAttemptToTransaction_Call { +func (_c *MockTxStore_AppendAttemptToTransaction_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 common.Address, _a3 *types.Attempt)) *MockTxStore_AppendAttemptToTransaction_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(uint64), args[2].(common.Address), args[3].(*types.Attempt)) }) return _c } -func (_c *mockTxStore_AppendAttemptToTransaction_Call) Return(_a0 error) *mockTxStore_AppendAttemptToTransaction_Call { +func (_c *MockTxStore_AppendAttemptToTransaction_Call) Return(_a0 error) *MockTxStore_AppendAttemptToTransaction_Call { _c.Call.Return(_a0) return _c } -func (_c *mockTxStore_AppendAttemptToTransaction_Call) RunAndReturn(run func(context.Context, uint64, common.Address, *types.Attempt) error) *mockTxStore_AppendAttemptToTransaction_Call { +func (_c *MockTxStore_AppendAttemptToTransaction_Call) RunAndReturn(run func(context.Context, uint64, common.Address, *types.Attempt) error) *MockTxStore_AppendAttemptToTransaction_Call { _c.Call.Return(run) return _c } // CreateEmptyUnconfirmedTransaction provides a mock function with given fields: _a0, _a1, _a2, _a3 -func (_m *mockTxStore) CreateEmptyUnconfirmedTransaction(_a0 context.Context, _a1 common.Address, _a2 uint64, _a3 uint64) (*types.Transaction, error) { +func (_m *MockTxStore) CreateEmptyUnconfirmedTransaction(_a0 context.Context, _a1 common.Address, _a2 uint64, _a3 uint64) (*types.Transaction, error) { ret := _m.Called(_a0, _a1, _a2, _a3) if len(ret) == 0 { @@ -151,8 +151,8 @@ func (_m *mockTxStore) CreateEmptyUnconfirmedTransaction(_a0 context.Context, _a return r0, r1 } -// mockTxStore_CreateEmptyUnconfirmedTransaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateEmptyUnconfirmedTransaction' -type mockTxStore_CreateEmptyUnconfirmedTransaction_Call struct { +// MockTxStore_CreateEmptyUnconfirmedTransaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateEmptyUnconfirmedTransaction' +type MockTxStore_CreateEmptyUnconfirmedTransaction_Call struct { *mock.Call } @@ -161,29 +161,29 @@ type mockTxStore_CreateEmptyUnconfirmedTransaction_Call struct { // - _a1 common.Address // - _a2 uint64 // - _a3 uint64 -func (_e *mockTxStore_Expecter) CreateEmptyUnconfirmedTransaction(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}) *mockTxStore_CreateEmptyUnconfirmedTransaction_Call { - return &mockTxStore_CreateEmptyUnconfirmedTransaction_Call{Call: _e.mock.On("CreateEmptyUnconfirmedTransaction", _a0, _a1, _a2, _a3)} +func (_e *MockTxStore_Expecter) CreateEmptyUnconfirmedTransaction(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}) *MockTxStore_CreateEmptyUnconfirmedTransaction_Call { + return &MockTxStore_CreateEmptyUnconfirmedTransaction_Call{Call: _e.mock.On("CreateEmptyUnconfirmedTransaction", _a0, _a1, _a2, _a3)} } -func (_c *mockTxStore_CreateEmptyUnconfirmedTransaction_Call) Run(run func(_a0 context.Context, _a1 common.Address, _a2 uint64, _a3 uint64)) *mockTxStore_CreateEmptyUnconfirmedTransaction_Call { +func (_c *MockTxStore_CreateEmptyUnconfirmedTransaction_Call) Run(run func(_a0 context.Context, _a1 common.Address, _a2 uint64, _a3 uint64)) *MockTxStore_CreateEmptyUnconfirmedTransaction_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(common.Address), args[2].(uint64), args[3].(uint64)) }) return _c } -func (_c *mockTxStore_CreateEmptyUnconfirmedTransaction_Call) Return(_a0 *types.Transaction, _a1 error) *mockTxStore_CreateEmptyUnconfirmedTransaction_Call { +func (_c *MockTxStore_CreateEmptyUnconfirmedTransaction_Call) Return(_a0 *types.Transaction, _a1 error) *MockTxStore_CreateEmptyUnconfirmedTransaction_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *mockTxStore_CreateEmptyUnconfirmedTransaction_Call) RunAndReturn(run func(context.Context, common.Address, uint64, uint64) (*types.Transaction, error)) *mockTxStore_CreateEmptyUnconfirmedTransaction_Call { +func (_c *MockTxStore_CreateEmptyUnconfirmedTransaction_Call) RunAndReturn(run func(context.Context, common.Address, uint64, uint64) (*types.Transaction, error)) *MockTxStore_CreateEmptyUnconfirmedTransaction_Call { _c.Call.Return(run) return _c } // CreateTransaction provides a mock function with given fields: _a0, _a1 -func (_m *mockTxStore) CreateTransaction(_a0 context.Context, _a1 *types.TxRequest) (*types.Transaction, error) { +func (_m *MockTxStore) CreateTransaction(_a0 context.Context, _a1 *types.TxRequest) (*types.Transaction, error) { ret := _m.Called(_a0, _a1) if len(ret) == 0 { @@ -212,37 +212,37 @@ func (_m *mockTxStore) CreateTransaction(_a0 context.Context, _a1 *types.TxReque return r0, r1 } -// mockTxStore_CreateTransaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateTransaction' -type mockTxStore_CreateTransaction_Call struct { +// MockTxStore_CreateTransaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateTransaction' +type MockTxStore_CreateTransaction_Call struct { *mock.Call } // CreateTransaction is a helper method to define mock.On call // - _a0 context.Context // - _a1 *types.TxRequest -func (_e *mockTxStore_Expecter) CreateTransaction(_a0 interface{}, _a1 interface{}) *mockTxStore_CreateTransaction_Call { - return &mockTxStore_CreateTransaction_Call{Call: _e.mock.On("CreateTransaction", _a0, _a1)} +func (_e *MockTxStore_Expecter) CreateTransaction(_a0 interface{}, _a1 interface{}) *MockTxStore_CreateTransaction_Call { + return &MockTxStore_CreateTransaction_Call{Call: _e.mock.On("CreateTransaction", _a0, _a1)} } -func (_c *mockTxStore_CreateTransaction_Call) Run(run func(_a0 context.Context, _a1 *types.TxRequest)) *mockTxStore_CreateTransaction_Call { +func (_c *MockTxStore_CreateTransaction_Call) Run(run func(_a0 context.Context, _a1 *types.TxRequest)) *MockTxStore_CreateTransaction_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(*types.TxRequest)) }) return _c } -func (_c *mockTxStore_CreateTransaction_Call) Return(_a0 *types.Transaction, _a1 error) *mockTxStore_CreateTransaction_Call { +func (_c *MockTxStore_CreateTransaction_Call) Return(_a0 *types.Transaction, _a1 error) *MockTxStore_CreateTransaction_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *mockTxStore_CreateTransaction_Call) RunAndReturn(run func(context.Context, *types.TxRequest) (*types.Transaction, error)) *mockTxStore_CreateTransaction_Call { +func (_c *MockTxStore_CreateTransaction_Call) RunAndReturn(run func(context.Context, *types.TxRequest) (*types.Transaction, error)) *MockTxStore_CreateTransaction_Call { _c.Call.Return(run) return _c } // DeleteAttemptForUnconfirmedTx provides a mock function with given fields: _a0, _a1, _a2, _a3 -func (_m *mockTxStore) DeleteAttemptForUnconfirmedTx(_a0 context.Context, _a1 uint64, _a2 *types.Attempt, _a3 common.Address) error { +func (_m *MockTxStore) DeleteAttemptForUnconfirmedTx(_a0 context.Context, _a1 uint64, _a2 *types.Attempt, _a3 common.Address) error { ret := _m.Called(_a0, _a1, _a2, _a3) if len(ret) == 0 { @@ -259,8 +259,8 @@ func (_m *mockTxStore) DeleteAttemptForUnconfirmedTx(_a0 context.Context, _a1 ui return r0 } -// mockTxStore_DeleteAttemptForUnconfirmedTx_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAttemptForUnconfirmedTx' -type mockTxStore_DeleteAttemptForUnconfirmedTx_Call struct { +// MockTxStore_DeleteAttemptForUnconfirmedTx_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAttemptForUnconfirmedTx' +type MockTxStore_DeleteAttemptForUnconfirmedTx_Call struct { *mock.Call } @@ -269,29 +269,29 @@ type mockTxStore_DeleteAttemptForUnconfirmedTx_Call struct { // - _a1 uint64 // - _a2 *types.Attempt // - _a3 common.Address -func (_e *mockTxStore_Expecter) DeleteAttemptForUnconfirmedTx(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}) *mockTxStore_DeleteAttemptForUnconfirmedTx_Call { - return &mockTxStore_DeleteAttemptForUnconfirmedTx_Call{Call: _e.mock.On("DeleteAttemptForUnconfirmedTx", _a0, _a1, _a2, _a3)} +func (_e *MockTxStore_Expecter) DeleteAttemptForUnconfirmedTx(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}) *MockTxStore_DeleteAttemptForUnconfirmedTx_Call { + return &MockTxStore_DeleteAttemptForUnconfirmedTx_Call{Call: _e.mock.On("DeleteAttemptForUnconfirmedTx", _a0, _a1, _a2, _a3)} } -func (_c *mockTxStore_DeleteAttemptForUnconfirmedTx_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 *types.Attempt, _a3 common.Address)) *mockTxStore_DeleteAttemptForUnconfirmedTx_Call { +func (_c *MockTxStore_DeleteAttemptForUnconfirmedTx_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 *types.Attempt, _a3 common.Address)) *MockTxStore_DeleteAttemptForUnconfirmedTx_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(uint64), args[2].(*types.Attempt), args[3].(common.Address)) }) return _c } -func (_c *mockTxStore_DeleteAttemptForUnconfirmedTx_Call) Return(_a0 error) *mockTxStore_DeleteAttemptForUnconfirmedTx_Call { +func (_c *MockTxStore_DeleteAttemptForUnconfirmedTx_Call) Return(_a0 error) *MockTxStore_DeleteAttemptForUnconfirmedTx_Call { _c.Call.Return(_a0) return _c } -func (_c *mockTxStore_DeleteAttemptForUnconfirmedTx_Call) RunAndReturn(run func(context.Context, uint64, *types.Attempt, common.Address) error) *mockTxStore_DeleteAttemptForUnconfirmedTx_Call { +func (_c *MockTxStore_DeleteAttemptForUnconfirmedTx_Call) RunAndReturn(run func(context.Context, uint64, *types.Attempt, common.Address) error) *MockTxStore_DeleteAttemptForUnconfirmedTx_Call { _c.Call.Return(run) return _c } // FetchUnconfirmedTransactionAtNonceWithCount provides a mock function with given fields: _a0, _a1, _a2 -func (_m *mockTxStore) FetchUnconfirmedTransactionAtNonceWithCount(_a0 context.Context, _a1 uint64, _a2 common.Address) (*types.Transaction, int, error) { +func (_m *MockTxStore) FetchUnconfirmedTransactionAtNonceWithCount(_a0 context.Context, _a1 uint64, _a2 common.Address) (*types.Transaction, int, error) { ret := _m.Called(_a0, _a1, _a2) if len(ret) == 0 { @@ -327,8 +327,8 @@ func (_m *mockTxStore) FetchUnconfirmedTransactionAtNonceWithCount(_a0 context.C return r0, r1, r2 } -// mockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FetchUnconfirmedTransactionAtNonceWithCount' -type mockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call struct { +// MockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FetchUnconfirmedTransactionAtNonceWithCount' +type MockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call struct { *mock.Call } @@ -336,29 +336,29 @@ type mockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call struct { // - _a0 context.Context // - _a1 uint64 // - _a2 common.Address -func (_e *mockTxStore_Expecter) FetchUnconfirmedTransactionAtNonceWithCount(_a0 interface{}, _a1 interface{}, _a2 interface{}) *mockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call { - return &mockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call{Call: _e.mock.On("FetchUnconfirmedTransactionAtNonceWithCount", _a0, _a1, _a2)} +func (_e *MockTxStore_Expecter) FetchUnconfirmedTransactionAtNonceWithCount(_a0 interface{}, _a1 interface{}, _a2 interface{}) *MockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call { + return &MockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call{Call: _e.mock.On("FetchUnconfirmedTransactionAtNonceWithCount", _a0, _a1, _a2)} } -func (_c *mockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 common.Address)) *mockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call { +func (_c *MockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 common.Address)) *MockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(uint64), args[2].(common.Address)) }) return _c } -func (_c *mockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call) Return(_a0 *types.Transaction, _a1 int, _a2 error) *mockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call { +func (_c *MockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call) Return(_a0 *types.Transaction, _a1 int, _a2 error) *MockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call { _c.Call.Return(_a0, _a1, _a2) return _c } -func (_c *mockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call) RunAndReturn(run func(context.Context, uint64, common.Address) (*types.Transaction, int, error)) *mockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call { +func (_c *MockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call) RunAndReturn(run func(context.Context, uint64, common.Address) (*types.Transaction, int, error)) *MockTxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call { _c.Call.Return(run) return _c } // MarkConfirmedAndReorgedTransactions provides a mock function with given fields: _a0, _a1, _a2 -func (_m *mockTxStore) MarkConfirmedAndReorgedTransactions(_a0 context.Context, _a1 uint64, _a2 common.Address) ([]*types.Transaction, []uint64, error) { +func (_m *MockTxStore) MarkConfirmedAndReorgedTransactions(_a0 context.Context, _a1 uint64, _a2 common.Address) ([]*types.Transaction, []uint64, error) { ret := _m.Called(_a0, _a1, _a2) if len(ret) == 0 { @@ -396,8 +396,8 @@ func (_m *mockTxStore) MarkConfirmedAndReorgedTransactions(_a0 context.Context, return r0, r1, r2 } -// mockTxStore_MarkConfirmedAndReorgedTransactions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarkConfirmedAndReorgedTransactions' -type mockTxStore_MarkConfirmedAndReorgedTransactions_Call struct { +// MockTxStore_MarkConfirmedAndReorgedTransactions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarkConfirmedAndReorgedTransactions' +type MockTxStore_MarkConfirmedAndReorgedTransactions_Call struct { *mock.Call } @@ -405,29 +405,29 @@ type mockTxStore_MarkConfirmedAndReorgedTransactions_Call struct { // - _a0 context.Context // - _a1 uint64 // - _a2 common.Address -func (_e *mockTxStore_Expecter) MarkConfirmedAndReorgedTransactions(_a0 interface{}, _a1 interface{}, _a2 interface{}) *mockTxStore_MarkConfirmedAndReorgedTransactions_Call { - return &mockTxStore_MarkConfirmedAndReorgedTransactions_Call{Call: _e.mock.On("MarkConfirmedAndReorgedTransactions", _a0, _a1, _a2)} +func (_e *MockTxStore_Expecter) MarkConfirmedAndReorgedTransactions(_a0 interface{}, _a1 interface{}, _a2 interface{}) *MockTxStore_MarkConfirmedAndReorgedTransactions_Call { + return &MockTxStore_MarkConfirmedAndReorgedTransactions_Call{Call: _e.mock.On("MarkConfirmedAndReorgedTransactions", _a0, _a1, _a2)} } -func (_c *mockTxStore_MarkConfirmedAndReorgedTransactions_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 common.Address)) *mockTxStore_MarkConfirmedAndReorgedTransactions_Call { +func (_c *MockTxStore_MarkConfirmedAndReorgedTransactions_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 common.Address)) *MockTxStore_MarkConfirmedAndReorgedTransactions_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(uint64), args[2].(common.Address)) }) return _c } -func (_c *mockTxStore_MarkConfirmedAndReorgedTransactions_Call) Return(_a0 []*types.Transaction, _a1 []uint64, _a2 error) *mockTxStore_MarkConfirmedAndReorgedTransactions_Call { +func (_c *MockTxStore_MarkConfirmedAndReorgedTransactions_Call) Return(_a0 []*types.Transaction, _a1 []uint64, _a2 error) *MockTxStore_MarkConfirmedAndReorgedTransactions_Call { _c.Call.Return(_a0, _a1, _a2) return _c } -func (_c *mockTxStore_MarkConfirmedAndReorgedTransactions_Call) RunAndReturn(run func(context.Context, uint64, common.Address) ([]*types.Transaction, []uint64, error)) *mockTxStore_MarkConfirmedAndReorgedTransactions_Call { +func (_c *MockTxStore_MarkConfirmedAndReorgedTransactions_Call) RunAndReturn(run func(context.Context, uint64, common.Address) ([]*types.Transaction, []uint64, error)) *MockTxStore_MarkConfirmedAndReorgedTransactions_Call { _c.Call.Return(run) return _c } // MarkTxFatal provides a mock function with given fields: _a0, _a1, _a2 -func (_m *mockTxStore) MarkTxFatal(_a0 context.Context, _a1 *types.Transaction, _a2 common.Address) error { +func (_m *MockTxStore) MarkTxFatal(_a0 context.Context, _a1 *types.Transaction, _a2 common.Address) error { ret := _m.Called(_a0, _a1, _a2) if len(ret) == 0 { @@ -444,8 +444,8 @@ func (_m *mockTxStore) MarkTxFatal(_a0 context.Context, _a1 *types.Transaction, return r0 } -// mockTxStore_MarkTxFatal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarkTxFatal' -type mockTxStore_MarkTxFatal_Call struct { +// MockTxStore_MarkTxFatal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarkTxFatal' +type MockTxStore_MarkTxFatal_Call struct { *mock.Call } @@ -453,29 +453,29 @@ type mockTxStore_MarkTxFatal_Call struct { // - _a0 context.Context // - _a1 *types.Transaction // - _a2 common.Address -func (_e *mockTxStore_Expecter) MarkTxFatal(_a0 interface{}, _a1 interface{}, _a2 interface{}) *mockTxStore_MarkTxFatal_Call { - return &mockTxStore_MarkTxFatal_Call{Call: _e.mock.On("MarkTxFatal", _a0, _a1, _a2)} +func (_e *MockTxStore_Expecter) MarkTxFatal(_a0 interface{}, _a1 interface{}, _a2 interface{}) *MockTxStore_MarkTxFatal_Call { + return &MockTxStore_MarkTxFatal_Call{Call: _e.mock.On("MarkTxFatal", _a0, _a1, _a2)} } -func (_c *mockTxStore_MarkTxFatal_Call) Run(run func(_a0 context.Context, _a1 *types.Transaction, _a2 common.Address)) *mockTxStore_MarkTxFatal_Call { +func (_c *MockTxStore_MarkTxFatal_Call) Run(run func(_a0 context.Context, _a1 *types.Transaction, _a2 common.Address)) *MockTxStore_MarkTxFatal_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(*types.Transaction), args[2].(common.Address)) }) return _c } -func (_c *mockTxStore_MarkTxFatal_Call) Return(_a0 error) *mockTxStore_MarkTxFatal_Call { +func (_c *MockTxStore_MarkTxFatal_Call) Return(_a0 error) *MockTxStore_MarkTxFatal_Call { _c.Call.Return(_a0) return _c } -func (_c *mockTxStore_MarkTxFatal_Call) RunAndReturn(run func(context.Context, *types.Transaction, common.Address) error) *mockTxStore_MarkTxFatal_Call { +func (_c *MockTxStore_MarkTxFatal_Call) RunAndReturn(run func(context.Context, *types.Transaction, common.Address) error) *MockTxStore_MarkTxFatal_Call { _c.Call.Return(run) return _c } // MarkUnconfirmedTransactionPurgeable provides a mock function with given fields: _a0, _a1, _a2 -func (_m *mockTxStore) MarkUnconfirmedTransactionPurgeable(_a0 context.Context, _a1 uint64, _a2 common.Address) error { +func (_m *MockTxStore) MarkUnconfirmedTransactionPurgeable(_a0 context.Context, _a1 uint64, _a2 common.Address) error { ret := _m.Called(_a0, _a1, _a2) if len(ret) == 0 { @@ -492,8 +492,8 @@ func (_m *mockTxStore) MarkUnconfirmedTransactionPurgeable(_a0 context.Context, return r0 } -// mockTxStore_MarkUnconfirmedTransactionPurgeable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarkUnconfirmedTransactionPurgeable' -type mockTxStore_MarkUnconfirmedTransactionPurgeable_Call struct { +// MockTxStore_MarkUnconfirmedTransactionPurgeable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarkUnconfirmedTransactionPurgeable' +type MockTxStore_MarkUnconfirmedTransactionPurgeable_Call struct { *mock.Call } @@ -501,29 +501,29 @@ type mockTxStore_MarkUnconfirmedTransactionPurgeable_Call struct { // - _a0 context.Context // - _a1 uint64 // - _a2 common.Address -func (_e *mockTxStore_Expecter) MarkUnconfirmedTransactionPurgeable(_a0 interface{}, _a1 interface{}, _a2 interface{}) *mockTxStore_MarkUnconfirmedTransactionPurgeable_Call { - return &mockTxStore_MarkUnconfirmedTransactionPurgeable_Call{Call: _e.mock.On("MarkUnconfirmedTransactionPurgeable", _a0, _a1, _a2)} +func (_e *MockTxStore_Expecter) MarkUnconfirmedTransactionPurgeable(_a0 interface{}, _a1 interface{}, _a2 interface{}) *MockTxStore_MarkUnconfirmedTransactionPurgeable_Call { + return &MockTxStore_MarkUnconfirmedTransactionPurgeable_Call{Call: _e.mock.On("MarkUnconfirmedTransactionPurgeable", _a0, _a1, _a2)} } -func (_c *mockTxStore_MarkUnconfirmedTransactionPurgeable_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 common.Address)) *mockTxStore_MarkUnconfirmedTransactionPurgeable_Call { +func (_c *MockTxStore_MarkUnconfirmedTransactionPurgeable_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 common.Address)) *MockTxStore_MarkUnconfirmedTransactionPurgeable_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(uint64), args[2].(common.Address)) }) return _c } -func (_c *mockTxStore_MarkUnconfirmedTransactionPurgeable_Call) Return(_a0 error) *mockTxStore_MarkUnconfirmedTransactionPurgeable_Call { +func (_c *MockTxStore_MarkUnconfirmedTransactionPurgeable_Call) Return(_a0 error) *MockTxStore_MarkUnconfirmedTransactionPurgeable_Call { _c.Call.Return(_a0) return _c } -func (_c *mockTxStore_MarkUnconfirmedTransactionPurgeable_Call) RunAndReturn(run func(context.Context, uint64, common.Address) error) *mockTxStore_MarkUnconfirmedTransactionPurgeable_Call { +func (_c *MockTxStore_MarkUnconfirmedTransactionPurgeable_Call) RunAndReturn(run func(context.Context, uint64, common.Address) error) *MockTxStore_MarkUnconfirmedTransactionPurgeable_Call { _c.Call.Return(run) return _c } // UpdateTransactionBroadcast provides a mock function with given fields: _a0, _a1, _a2, _a3, _a4 -func (_m *mockTxStore) UpdateTransactionBroadcast(_a0 context.Context, _a1 uint64, _a2 uint64, _a3 common.Hash, _a4 common.Address) error { +func (_m *MockTxStore) UpdateTransactionBroadcast(_a0 context.Context, _a1 uint64, _a2 uint64, _a3 common.Hash, _a4 common.Address) error { ret := _m.Called(_a0, _a1, _a2, _a3, _a4) if len(ret) == 0 { @@ -540,8 +540,8 @@ func (_m *mockTxStore) UpdateTransactionBroadcast(_a0 context.Context, _a1 uint6 return r0 } -// mockTxStore_UpdateTransactionBroadcast_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateTransactionBroadcast' -type mockTxStore_UpdateTransactionBroadcast_Call struct { +// MockTxStore_UpdateTransactionBroadcast_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateTransactionBroadcast' +type MockTxStore_UpdateTransactionBroadcast_Call struct { *mock.Call } @@ -551,29 +551,29 @@ type mockTxStore_UpdateTransactionBroadcast_Call struct { // - _a2 uint64 // - _a3 common.Hash // - _a4 common.Address -func (_e *mockTxStore_Expecter) UpdateTransactionBroadcast(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}, _a4 interface{}) *mockTxStore_UpdateTransactionBroadcast_Call { - return &mockTxStore_UpdateTransactionBroadcast_Call{Call: _e.mock.On("UpdateTransactionBroadcast", _a0, _a1, _a2, _a3, _a4)} +func (_e *MockTxStore_Expecter) UpdateTransactionBroadcast(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}, _a4 interface{}) *MockTxStore_UpdateTransactionBroadcast_Call { + return &MockTxStore_UpdateTransactionBroadcast_Call{Call: _e.mock.On("UpdateTransactionBroadcast", _a0, _a1, _a2, _a3, _a4)} } -func (_c *mockTxStore_UpdateTransactionBroadcast_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 uint64, _a3 common.Hash, _a4 common.Address)) *mockTxStore_UpdateTransactionBroadcast_Call { +func (_c *MockTxStore_UpdateTransactionBroadcast_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 uint64, _a3 common.Hash, _a4 common.Address)) *MockTxStore_UpdateTransactionBroadcast_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(uint64), args[2].(uint64), args[3].(common.Hash), args[4].(common.Address)) }) return _c } -func (_c *mockTxStore_UpdateTransactionBroadcast_Call) Return(_a0 error) *mockTxStore_UpdateTransactionBroadcast_Call { +func (_c *MockTxStore_UpdateTransactionBroadcast_Call) Return(_a0 error) *MockTxStore_UpdateTransactionBroadcast_Call { _c.Call.Return(_a0) return _c } -func (_c *mockTxStore_UpdateTransactionBroadcast_Call) RunAndReturn(run func(context.Context, uint64, uint64, common.Hash, common.Address) error) *mockTxStore_UpdateTransactionBroadcast_Call { +func (_c *MockTxStore_UpdateTransactionBroadcast_Call) RunAndReturn(run func(context.Context, uint64, uint64, common.Hash, common.Address) error) *MockTxStore_UpdateTransactionBroadcast_Call { _c.Call.Return(run) return _c } // UpdateUnstartedTransactionWithNonce provides a mock function with given fields: _a0, _a1, _a2 -func (_m *mockTxStore) UpdateUnstartedTransactionWithNonce(_a0 context.Context, _a1 common.Address, _a2 uint64) (*types.Transaction, error) { +func (_m *MockTxStore) UpdateUnstartedTransactionWithNonce(_a0 context.Context, _a1 common.Address, _a2 uint64) (*types.Transaction, error) { ret := _m.Called(_a0, _a1, _a2) if len(ret) == 0 { @@ -602,8 +602,8 @@ func (_m *mockTxStore) UpdateUnstartedTransactionWithNonce(_a0 context.Context, return r0, r1 } -// mockTxStore_UpdateUnstartedTransactionWithNonce_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUnstartedTransactionWithNonce' -type mockTxStore_UpdateUnstartedTransactionWithNonce_Call struct { +// MockTxStore_UpdateUnstartedTransactionWithNonce_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUnstartedTransactionWithNonce' +type MockTxStore_UpdateUnstartedTransactionWithNonce_Call struct { *mock.Call } @@ -611,34 +611,34 @@ type mockTxStore_UpdateUnstartedTransactionWithNonce_Call struct { // - _a0 context.Context // - _a1 common.Address // - _a2 uint64 -func (_e *mockTxStore_Expecter) UpdateUnstartedTransactionWithNonce(_a0 interface{}, _a1 interface{}, _a2 interface{}) *mockTxStore_UpdateUnstartedTransactionWithNonce_Call { - return &mockTxStore_UpdateUnstartedTransactionWithNonce_Call{Call: _e.mock.On("UpdateUnstartedTransactionWithNonce", _a0, _a1, _a2)} +func (_e *MockTxStore_Expecter) UpdateUnstartedTransactionWithNonce(_a0 interface{}, _a1 interface{}, _a2 interface{}) *MockTxStore_UpdateUnstartedTransactionWithNonce_Call { + return &MockTxStore_UpdateUnstartedTransactionWithNonce_Call{Call: _e.mock.On("UpdateUnstartedTransactionWithNonce", _a0, _a1, _a2)} } -func (_c *mockTxStore_UpdateUnstartedTransactionWithNonce_Call) Run(run func(_a0 context.Context, _a1 common.Address, _a2 uint64)) *mockTxStore_UpdateUnstartedTransactionWithNonce_Call { +func (_c *MockTxStore_UpdateUnstartedTransactionWithNonce_Call) Run(run func(_a0 context.Context, _a1 common.Address, _a2 uint64)) *MockTxStore_UpdateUnstartedTransactionWithNonce_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(common.Address), args[2].(uint64)) }) return _c } -func (_c *mockTxStore_UpdateUnstartedTransactionWithNonce_Call) Return(_a0 *types.Transaction, _a1 error) *mockTxStore_UpdateUnstartedTransactionWithNonce_Call { +func (_c *MockTxStore_UpdateUnstartedTransactionWithNonce_Call) Return(_a0 *types.Transaction, _a1 error) *MockTxStore_UpdateUnstartedTransactionWithNonce_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *mockTxStore_UpdateUnstartedTransactionWithNonce_Call) RunAndReturn(run func(context.Context, common.Address, uint64) (*types.Transaction, error)) *mockTxStore_UpdateUnstartedTransactionWithNonce_Call { +func (_c *MockTxStore_UpdateUnstartedTransactionWithNonce_Call) RunAndReturn(run func(context.Context, common.Address, uint64) (*types.Transaction, error)) *MockTxStore_UpdateUnstartedTransactionWithNonce_Call { _c.Call.Return(run) return _c } -// newMockTxStore creates a new instance of mockTxStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// NewMockTxStore creates a new instance of MockTxStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func newMockTxStore(t interface { +func NewMockTxStore(t interface { mock.TestingT Cleanup(func()) -}) *mockTxStore { - mock := &mockTxStore{} +}) *MockTxStore { + mock := &MockTxStore{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) diff --git a/pkg/txm/storage/inmemory_store.go b/pkg/txm/storage/inmemory_store.go index ca78994bce..a1123f8dcc 100644 --- a/pkg/txm/storage/inmemory_store.go +++ b/pkg/txm/storage/inmemory_store.go @@ -1,7 +1,6 @@ package storage import ( - "errors" "fmt" "math/big" "sort" @@ -122,6 +121,7 @@ func (m *InMemoryStore) CreateEmptyUnconfirmedTransaction(nonce uint64, gasLimit SpecifiedGasLimit: gasLimit, CreatedAt: time.Now(), State: txmgr.TxUnconfirmed, + IsPurgeable: true, } if _, exists := m.UnconfirmedTransactions[nonce]; exists { @@ -366,8 +366,15 @@ func (m *InMemoryStore) DeleteAttemptForUnconfirmedTx(transactionNonce uint64, a return fmt.Errorf("attempt with hash: %v for txID: %v was not found", attempt.Hash, attempt.TxID) } -func (m *InMemoryStore) MarkTxFatal(*types.Transaction) error { - return errors.New("not implemented") +func (m *InMemoryStore) MarkTxFatal(txToMark *types.Transaction) error { + m.Lock() + defer m.Unlock() + + // TODO: for now do the simple thing and drop the transaction instead of adding it to the fatal queue. + delete(m.UnconfirmedTransactions, *txToMark.Nonce) + delete(m.Transactions, txToMark.ID) + txToMark.State = txmgr.TxFatalError // update the state in case the caller needs to log + return nil } // Orchestrator diff --git a/pkg/txm/storage/inmemory_store_test.go b/pkg/txm/storage/inmemory_store_test.go index d1f1f847bf..90f1c0d196 100644 --- a/pkg/txm/storage/inmemory_store_test.go +++ b/pkg/txm/storage/inmemory_store_test.go @@ -481,6 +481,20 @@ func TestFindTxWithIdempotencyKey(t *testing.T) { assert.Nil(t, itx) } +func TestMarkTxFatal(t *testing.T) { + t.Parallel() + fromAddress := testutils.NewAddress() + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + + var nonce uint64 = 1 + tx, err := insertUnconfirmedTransaction(m, nonce) + require.NoError(t, err) + require.NoError(t, m.MarkTxFatal(tx)) + assert.Equal(t, txmgr.TxFatalError, tx.State) + assert.Empty(t, m.UnconfirmedTransactions) + assert.Empty(t, m.Transactions) +} + func TestPruneConfirmedTransactions(t *testing.T) { t.Parallel() fromAddress := testutils.NewAddress() diff --git a/pkg/txm/txm.go b/pkg/txm/txm.go index 7c5d1a369b..cd64c22178 100644 --- a/pkg/txm/txm.go +++ b/pkg/txm/txm.go @@ -18,9 +18,10 @@ import ( ) const ( + MaxInFlightTransactions int = 16 + MaxInFlightSubset int = 5 + broadcastInterval time.Duration = 30 * time.Second - maxInFlightTransactions int = 16 - maxInFlightSubset int = 5 maxAttemptsThreshold uint16 = 10 pendingNonceDefaultTimeout time.Duration = 30 * time.Second pendingNonceRecheckInterval time.Duration = 1 * time.Second @@ -49,12 +50,11 @@ type TxStore interface { } type AttemptBuilder interface { - NewAttempt(context.Context, logger.Logger, *types.Transaction, bool) (*types.Attempt, error) - NewBumpAttempt(context.Context, logger.Logger, *types.Transaction, types.Attempt) (*types.Attempt, error) + NewAgnosticBumpAttempt(ctx context.Context, lggr logger.Logger, tx *types.Transaction, dynamic bool) (*types.Attempt, error) } type ErrorHandler interface { - HandleError(*types.Transaction, error, AttemptBuilder, Client, TxStore, func(common.Address, uint64), bool) (err error) + HandleError(context.Context, *types.Transaction, error, TxStore, func(common.Address, uint64), bool) (err error) } type StuckTxDetector interface { @@ -83,7 +83,7 @@ type Txm struct { txStore TxStore keystore keys.AddressLister config Config - metrics *txmMetrics + Metrics *txmMetrics nonceMapMu sync.RWMutex nonceMap map[common.Address]uint64 @@ -93,7 +93,7 @@ type Txm struct { wg sync.WaitGroup } -func NewTxm(lggr logger.Logger, chainID *big.Int, client Client, attemptBuilder AttemptBuilder, txStore TxStore, stuckTxDetector StuckTxDetector, config Config, keystore keys.AddressLister) *Txm { +func NewTxm(lggr logger.Logger, chainID *big.Int, client Client, attemptBuilder AttemptBuilder, txStore TxStore, stuckTxDetector StuckTxDetector, config Config, keystore keys.AddressLister, errorHandler ErrorHandler) *Txm { return &Txm{ lggr: logger.Sugared(logger.Named(lggr, "Txm")), keystore: keystore, @@ -103,6 +103,7 @@ func NewTxm(lggr logger.Logger, chainID *big.Int, client Client, attemptBuilder txStore: txStore, stuckTxDetector: stuckTxDetector, config: config, + errorHandler: errorHandler, nonceMap: make(map[common.Address]uint64), triggerCh: make(map[common.Address]chan struct{}), } @@ -114,7 +115,7 @@ func (t *Txm) Start(ctx context.Context) error { if err != nil { return err } - t.metrics = tm + t.Metrics = tm t.stopCh = make(chan struct{}) addresses, err := t.keystore.EnabledAddresses(ctx) @@ -132,9 +133,8 @@ func (t *Txm) startAddress(address common.Address) { triggerCh := make(chan struct{}, 1) t.triggerCh[address] = triggerCh - t.wg.Add(2) - go t.broadcastLoop(address, triggerCh) - go t.backfillLoop(address) + t.wg.Add(1) + go t.loop(address, triggerCh) } func (t *Txm) initializeNonce(ctx context.Context, address common.Address) { @@ -152,7 +152,7 @@ func (t *Txm) initializeNonce(ctx context.Context, address common.Address) { } continue } - t.setNonce(address, pendingNonce) + t.SetNonce(address, pendingNonce) t.lggr.Debugf("Set initial nonce for address: %v to %d", address, pendingNonce) return } @@ -196,16 +196,16 @@ func (t *Txm) Abandon(address common.Address) error { return t.txStore.AbandonPendingTransactions(context.TODO(), address) } -func (t *Txm) getNonce(address common.Address) uint64 { +func (t *Txm) GetNonce(address common.Address) uint64 { t.nonceMapMu.RLock() defer t.nonceMapMu.RUnlock() return t.nonceMap[address] } -func (t *Txm) setNonce(address common.Address, nonce uint64) { +func (t *Txm) SetNonce(address common.Address, nonce uint64) { t.nonceMapMu.Lock() - t.nonceMap[address] = nonce defer t.nonceMapMu.Unlock() + t.nonceMap[address] = nonce } func newBackoff(minDuration time.Duration) backoff.Backoff { @@ -216,18 +216,19 @@ func newBackoff(minDuration time.Duration) backoff.Backoff { } } -func (t *Txm) broadcastLoop(address common.Address, triggerCh chan struct{}) { +func (t *Txm) loop(address common.Address, triggerCh chan struct{}) { defer t.wg.Done() ctx, cancel := t.stopCh.NewCtx() defer cancel() broadcastWithBackoff := newBackoff(1 * time.Second) var broadcastCh <-chan time.Time + backfillCh := time.After(utils.WithJitter(t.config.BlockTime)) t.initializeNonce(ctx, address) for { start := time.Now() - bo, err := t.broadcastTransaction(ctx, address) + bo, err := t.BroadcastTransaction(ctx, address) if err != nil { t.lggr.Errorw("Error during transaction broadcasting", "err", err) } else { @@ -246,60 +247,40 @@ func (t *Txm) broadcastLoop(address common.Address, triggerCh chan struct{}) { continue case <-broadcastCh: continue - } - } -} - -func (t *Txm) backfillLoop(address common.Address) { - defer t.wg.Done() - ctx, cancel := t.stopCh.NewCtx() - defer cancel() - backfillWithBackoff := newBackoff(t.config.BlockTime) - backfillCh := time.After(utils.WithJitter(t.config.BlockTime)) - - for { - select { - case <-ctx.Done(): - return case <-backfillCh: + backfillCh = time.After(utils.WithJitter(t.config.BlockTime)) start := time.Now() - bo, err := t.backfillTransactions(ctx, address) + err := t.BackfillTransactions(ctx, address) if err != nil { t.lggr.Errorw("Error during backfill", "err", err) } else { t.lggr.Debug("Backfill time elapsed: ", time.Since(start)) } - if bo { - backfillCh = time.After(backfillWithBackoff.Duration()) - } else { - backfillWithBackoff.Reset() - backfillCh = time.After(utils.WithJitter(t.config.BlockTime)) - } } } } -func (t *Txm) broadcastTransaction(ctx context.Context, address common.Address) (bool, error) { +func (t *Txm) BroadcastTransaction(ctx context.Context, address common.Address) (bool, error) { for { _, unconfirmedCount, err := t.txStore.FetchUnconfirmedTransactionAtNonceWithCount(ctx, 0, address) if err != nil { return false, err } - // Optimistically send up to maxInFlightSubset of the maxInFlightTransactions. After that threshold, broadcast more cautiously - // by checking the pending nonce so no more than maxInFlightSubset can get stuck simultaneously i.e. due + // Optimistically send up to MaxInFlightSubset of the MaxInFlightTransactions. After that threshold, broadcast more cautiously + // by checking the pending nonce so no more than MaxInFlightSubset can get stuck simultaneously i.e. due // to insufficient balance. We're making this trade-off to avoid storing stuck transactions and making unnecessary - // RPC calls. The upper limit is always maxInFlightTransactions regardless of the pending nonce. - if unconfirmedCount >= maxInFlightSubset { - if unconfirmedCount > maxInFlightTransactions { - t.lggr.Warnf("Reached transaction limit: %d for unconfirmed transactions", maxInFlightTransactions) + // RPC calls. The upper limit is always MaxInFlightTransactions regardless of the pending nonce. + if unconfirmedCount >= MaxInFlightSubset { + if unconfirmedCount > MaxInFlightTransactions { + t.lggr.Warnf("Reached transaction limit: %d for unconfirmed transactions", MaxInFlightTransactions) return true, nil } pendingNonce, e := t.client.PendingNonceAt(ctx, address) if e != nil { return false, e } - nonce := t.getNonce(address) + nonce := t.GetNonce(address) if nonce > pendingNonce { t.lggr.Warnf("Reached transaction limit. LocalNonce: %d, PendingNonce %d, unconfirmedCount: %d", nonce, pendingNonce, unconfirmedCount) @@ -307,7 +288,7 @@ func (t *Txm) broadcastTransaction(ctx context.Context, address common.Address) } } - nonce := t.getNonce(address) + nonce := t.GetNonce(address) tx, err := t.txStore.UpdateUnstartedTransactionWithNonce(ctx, address, nonce) if err != nil { return false, err @@ -315,7 +296,7 @@ func (t *Txm) broadcastTransaction(ctx context.Context, address common.Address) if tx == nil { return false, nil } - t.setNonce(address, nonce+1) + t.SetNonce(address, nonce+1) if err := t.createAndSendAttempt(ctx, tx, address); err != nil { return false, err @@ -324,7 +305,7 @@ func (t *Txm) broadcastTransaction(ctx context.Context, address common.Address) } func (t *Txm) createAndSendAttempt(ctx context.Context, tx *types.Transaction, address common.Address) error { - attempt, err := t.attemptBuilder.NewAttempt(ctx, t.lggr, tx, t.config.EIP1559) + attempt, err := t.attemptBuilder.NewAgnosticBumpAttempt(ctx, t.lggr, tx, t.config.EIP1559) if err != nil { return err } @@ -348,7 +329,7 @@ func (t *Txm) sendTransactionWithError(ctx context.Context, tx *types.Transactio tx.AttemptCount++ t.lggr.Infow("Broadcasted attempt", "tx", tx, "attempt", attempt, "duration", time.Since(start), "txErr: ", txErr) if txErr != nil && t.errorHandler != nil { - if err = t.errorHandler.HandleError(tx, txErr, t.attemptBuilder, t.client, t.txStore, t.setNonce, false); err != nil { + if err = t.errorHandler.HandleError(ctx, tx, txErr, t.txStore, t.SetNonce, false); err != nil { return } } else if txErr != nil { @@ -361,77 +342,77 @@ func (t *Txm) sendTransactionWithError(ctx context.Context, tx *types.Transactio } } - t.metrics.IncrementNumBroadcastedTxs(ctx) - if err = t.metrics.EmitTxMessage(ctx, attempt.Hash, fromAddress, tx); err != nil { + t.Metrics.IncrementNumBroadcastedTxs(ctx) + if err = t.Metrics.EmitTxMessage(ctx, attempt.Hash, fromAddress, tx); err != nil { t.lggr.Errorw("Beholder error emitting tx message", "err", err) } return t.txStore.UpdateTransactionBroadcast(ctx, attempt.TxID, *tx.Nonce, attempt.Hash, fromAddress) } -func (t *Txm) backfillTransactions(ctx context.Context, address common.Address) (bool, error) { +func (t *Txm) BackfillTransactions(ctx context.Context, address common.Address) error { latestNonce, err := t.client.NonceAt(ctx, address, nil) if err != nil { - return false, err + return err } confirmedTransactions, unconfirmedTransactionIDs, err := t.txStore.MarkConfirmedAndReorgedTransactions(ctx, latestNonce, address) if err != nil { - return false, err + return err } if len(confirmedTransactions) > 0 || len(unconfirmedTransactionIDs) > 0 { - t.metrics.IncrementNumConfirmedTxs(ctx, len(confirmedTransactions)) + t.Metrics.IncrementNumConfirmedTxs(ctx, len(confirmedTransactions)) confirmedTransactionIDs := t.extractMetrics(ctx, confirmedTransactions) t.lggr.Infof("Confirmed transaction IDs: %v . Re-orged transaction IDs: %v", confirmedTransactionIDs, unconfirmedTransactionIDs) } tx, unconfirmedCount, err := t.txStore.FetchUnconfirmedTransactionAtNonceWithCount(ctx, latestNonce, address) if err != nil { - return false, err + return err } if unconfirmedCount == 0 { t.lggr.Debugf("All transactions confirmed for address: %v", address) - return false, err // TODO: add backoff to optimize requests + return nil } if tx == nil || *tx.Nonce != latestNonce { t.lggr.Warnf("Nonce gap at nonce: %d - address: %v. Creating a new transaction\n", latestNonce, address) - t.metrics.IncrementNumNonceGaps(ctx) - return false, t.createAndSendEmptyTx(ctx, latestNonce, address) + t.Metrics.IncrementNumNonceGaps(ctx) + return t.createAndSendEmptyTx(ctx, latestNonce, address) } else { //nolint:revive //easier to read if !tx.IsPurgeable && t.stuckTxDetector != nil { isStuck, err := t.stuckTxDetector.DetectStuckTransaction(ctx, tx) if err != nil { - return false, err + return err } if isStuck { tx.IsPurgeable = true err = t.txStore.MarkUnconfirmedTransactionPurgeable(ctx, *tx.Nonce, address) if err != nil { - return false, err + return err } t.lggr.Infof("Marked tx as purgeable. Sending purge attempt for txID: %d", tx.ID) - return false, t.createAndSendAttempt(ctx, tx, address) + return t.createAndSendAttempt(ctx, tx, address) } } if tx.AttemptCount >= maxAttemptsThreshold { - t.metrics.ReachedMaxAttempts(ctx, true) + t.Metrics.ReachedMaxAttempts(ctx, true) t.lggr.Warnf("Reached max attempts threshold for txID: %d. TXM will broadcast more attempts but if this"+ " error persists, it means the transaction won't likely be confirmed and there is an issue with the transaction."+ "Look for any error messages from previous broadcasted attempts that may indicate why this happened, i.e. wallet is out of funds. Tx: %v", tx.ID, tx.PrintWithAttempts()) } else { - t.metrics.ReachedMaxAttempts(ctx, false) + t.Metrics.ReachedMaxAttempts(ctx, false) } - if tx.LastBroadcastAt == nil || time.Since(*tx.LastBroadcastAt) > (t.config.BlockTime*time.Duration(t.config.RetryBlockThreshold)) { + if tx.LastBroadcastAt == nil || time.Since(*tx.LastBroadcastAt) > (t.config.BlockTime*time.Duration(t.config.RetryBlockThreshold)) || tx.IsPurgeable { // TODO: add optional graceful bumping strategy t.lggr.Info("Rebroadcasting attempt for txID: ", tx.ID) - return false, t.createAndSendAttempt(ctx, tx, address) + return t.createAndSendAttempt(ctx, tx, address) } } - return false, nil + return nil } func (t *Txm) createAndSendEmptyTx(ctx context.Context, latestNonce uint64, address common.Address) error { @@ -447,7 +428,7 @@ func (t *Txm) extractMetrics(ctx context.Context, txs []*types.Transaction) []ui for _, tx := range txs { confirmedTxIDs = append(confirmedTxIDs, tx.ID) if tx.InitialBroadcastAt != nil { - t.metrics.RecordTimeUntilTxConfirmed(ctx, float64(time.Since(*tx.InitialBroadcastAt))) + t.Metrics.RecordTimeUntilTxConfirmed(ctx, float64(time.Since(*tx.InitialBroadcastAt))) } } return confirmedTxIDs diff --git a/pkg/txm/txm_test.go b/pkg/txm/txm_test.go index 0995c8f119..755faaa6ce 100644 --- a/pkg/txm/txm_test.go +++ b/pkg/txm/txm_test.go @@ -1,4 +1,4 @@ -package txm +package txm_test import ( "errors" @@ -17,17 +17,21 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/smartcontractkit/chainlink-evm/pkg/assets" "github.com/smartcontractkit/chainlink-evm/pkg/gas" + "github.com/smartcontractkit/chainlink-evm/pkg/gas/mocks" "github.com/smartcontractkit/chainlink-evm/pkg/keys/keystest" "github.com/smartcontractkit/chainlink-evm/pkg/testutils" + "github.com/smartcontractkit/chainlink-evm/pkg/txm" + "github.com/smartcontractkit/chainlink-evm/pkg/txm/clientwrappers/dualbroadcast" "github.com/smartcontractkit/chainlink-evm/pkg/txm/storage" "github.com/smartcontractkit/chainlink-evm/pkg/txm/types" + "github.com/smartcontractkit/chainlink-framework/chains/fees" ) func TestLifecycle(t *testing.T) { t.Parallel() - client := newMockClient(t) - ab := newMockAttemptBuilder(t) + client := txm.NewMockClient(t) + ab := txm.NewMockAttemptBuilder(t) address1 := testutils.NewAddress() address2 := testutils.NewAddress() assert.NotEqual(t, address1, address2) @@ -35,25 +39,25 @@ func TestLifecycle(t *testing.T) { t.Run("retries if initial pending nonce call fails", func(t *testing.T) { lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) - config := Config{BlockTime: 1 * time.Minute} + config := txm.Config{BlockTime: 1 * time.Minute} txStore := storage.NewInMemoryStoreManager(lggr, testutils.FixtureChainID) require.NoError(t, txStore.Add(address1)) keystore := keystest.Addresses{address1} - txm := NewTxm(lggr, testutils.FixtureChainID, client, nil, txStore, nil, config, keystore) + tm := txm.NewTxm(lggr, testutils.FixtureChainID, client, nil, txStore, nil, config, keystore, nil) client.On("PendingNonceAt", mock.Anything, address1).Return(uint64(0), errors.New("error")).Once() client.On("PendingNonceAt", mock.Anything, address1).Return(uint64(100), nil).Once() - servicetest.Run(t, txm) + servicetest.Run(t, tm) tests.AssertLogEventually(t, observedLogs, "Error when fetching initial nonce") tests.AssertLogEventually(t, observedLogs, fmt.Sprintf("Set initial nonce for address: %v to %d", address1, 100)) }) t.Run("tests lifecycle successfully without any transactions", func(t *testing.T) { - config := Config{BlockTime: 200 * time.Millisecond} + config := txm.Config{BlockTime: 200 * time.Millisecond} keystore := keystest.Addresses(addresses) lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) txStore := storage.NewInMemoryStoreManager(lggr, testutils.FixtureChainID) require.NoError(t, txStore.Add(addresses...)) - txm := NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, config, keystore) + tx := txm.NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, config, keystore, nil) var nonce uint64 // Start client.On("PendingNonceAt", mock.Anything, address1).Return(nonce, nil).Once() @@ -62,7 +66,7 @@ func TestLifecycle(t *testing.T) { client.On("NonceAt", mock.Anything, address1, mock.Anything).Return(nonce, nil).Maybe() client.On("NonceAt", mock.Anything, address2, mock.Anything).Return(nonce, nil).Maybe() - servicetest.Run(t, txm) + servicetest.Run(t, tx) tests.AssertLogEventually(t, observedLogs, "Backfill time elapsed") }) } @@ -74,7 +78,7 @@ func TestTrigger(t *testing.T) { t.Run("Trigger fails if Txm is unstarted", func(t *testing.T) { lggr, observedLogs := logger.TestObserved(t, zap.ErrorLevel) - txm := NewTxm(lggr, nil, nil, nil, nil, nil, Config{}, keystest.Addresses{}) + txm := txm.NewTxm(lggr, nil, nil, nil, nil, nil, txm.Config{}, keystest.Addresses{}, nil) txm.Trigger(address) tests.AssertLogEventually(t, observedLogs, "Txm unstarted") }) @@ -83,16 +87,16 @@ func TestTrigger(t *testing.T) { lggr := logger.Test(t) txStore := storage.NewInMemoryStoreManager(lggr, testutils.FixtureChainID) require.NoError(t, txStore.Add(address)) - client := newMockClient(t) - ab := newMockAttemptBuilder(t) - config := Config{BlockTime: 1 * time.Minute, RetryBlockThreshold: 10} + client := txm.NewMockClient(t) + ab := txm.NewMockAttemptBuilder(t) + config := txm.Config{BlockTime: 1 * time.Minute, RetryBlockThreshold: 10} keystore := keystest.Addresses{address} - txm := NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, config, keystore) + tm := txm.NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, config, keystore, nil) var nonce uint64 // Start client.On("PendingNonceAt", mock.Anything, address).Return(nonce, nil).Maybe() - servicetest.Run(t, txm) - txm.Trigger(address) + servicetest.Run(t, tm) + tm.Trigger(address) }) } @@ -100,17 +104,17 @@ func TestBroadcastTransaction(t *testing.T) { t.Parallel() ctx := t.Context() - client := newMockClient(t) - ab := newMockAttemptBuilder(t) - config := Config{} + client := txm.NewMockClient(t) + ab := txm.NewMockAttemptBuilder(t) + config := txm.Config{} address := testutils.NewAddress() keystore := keystest.Addresses{} t.Run("fails if FetchUnconfirmedTransactionAtNonceWithCount for unconfirmed transactions fails", func(t *testing.T) { - mTxStore := newMockTxStore(t) + mTxStore := txm.NewMockTxStore(t) mTxStore.On("FetchUnconfirmedTransactionAtNonceWithCount", mock.Anything, mock.Anything, mock.Anything).Return(nil, 0, errors.New("call failed")).Once() - txm := NewTxm(logger.Test(t), testutils.FixtureChainID, client, ab, mTxStore, nil, config, keystore) - bo, err := txm.broadcastTransaction(ctx, address) + tx := txm.NewTxm(logger.Test(t), testutils.FixtureChainID, client, ab, mTxStore, nil, config, keystore, nil) + bo, err := tx.BroadcastTransaction(ctx, address) require.Error(t, err) assert.False(t, bo) require.ErrorContains(t, err, "call failed") @@ -118,10 +122,10 @@ func TestBroadcastTransaction(t *testing.T) { t.Run("throws a warning and returns if unconfirmed transactions exceed maxInFlightTransactions", func(t *testing.T) { lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) - mTxStore := newMockTxStore(t) - mTxStore.On("FetchUnconfirmedTransactionAtNonceWithCount", mock.Anything, mock.Anything, mock.Anything).Return(nil, maxInFlightTransactions+1, nil).Once() - txm := NewTxm(lggr, testutils.FixtureChainID, client, ab, mTxStore, nil, config, keystore) - bo, err := txm.broadcastTransaction(ctx, address) + mTxStore := txm.NewMockTxStore(t) + mTxStore.On("FetchUnconfirmedTransactionAtNonceWithCount", mock.Anything, mock.Anything, mock.Anything).Return(nil, txm.MaxInFlightTransactions+1, nil).Once() + tx := txm.NewTxm(lggr, testutils.FixtureChainID, client, ab, mTxStore, nil, config, keystore, nil) + bo, err := tx.BroadcastTransaction(ctx, address) assert.True(t, bo) require.NoError(t, err) tests.AssertLogEventually(t, observedLogs, "Reached transaction limit") @@ -129,30 +133,30 @@ func TestBroadcastTransaction(t *testing.T) { t.Run("checks pending nonce if unconfirmed transactions are equal or more than maxInFlightSubset", func(t *testing.T) { lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) - mTxStore := newMockTxStore(t) - txm := NewTxm(lggr, testutils.FixtureChainID, client, ab, mTxStore, nil, config, keystore) - txm.setNonce(address, 1) - mTxStore.On("FetchUnconfirmedTransactionAtNonceWithCount", mock.Anything, mock.Anything, mock.Anything).Return(nil, maxInFlightSubset, nil).Twice() + mTxStore := txm.NewMockTxStore(t) + tm := txm.NewTxm(lggr, testutils.FixtureChainID, client, ab, mTxStore, nil, config, keystore, nil) + tm.SetNonce(address, 1) + mTxStore.On("FetchUnconfirmedTransactionAtNonceWithCount", mock.Anything, mock.Anything, mock.Anything).Return(nil, txm.MaxInFlightSubset, nil).Twice() client.On("PendingNonceAt", mock.Anything, address).Return(uint64(0), nil).Once() // LocalNonce: 1, PendingNonce: 0 - bo, err := txm.broadcastTransaction(ctx, address) + bo, err := tm.BroadcastTransaction(ctx, address) assert.True(t, bo) require.NoError(t, err) client.On("PendingNonceAt", mock.Anything, address).Return(uint64(1), nil).Once() // LocalNonce: 1, PendingNonce: 1 mTxStore.On("UpdateUnstartedTransactionWithNonce", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Once() - bo, err = txm.broadcastTransaction(ctx, address) + bo, err = tm.BroadcastTransaction(ctx, address) assert.False(t, bo) require.NoError(t, err) tests.AssertLogCountEventually(t, observedLogs, "Reached transaction limit.", 1) }) t.Run("fails if UpdateUnstartedTransactionWithNonce fails", func(t *testing.T) { - mTxStore := newMockTxStore(t) + mTxStore := txm.NewMockTxStore(t) mTxStore.On("FetchUnconfirmedTransactionAtNonceWithCount", mock.Anything, mock.Anything, mock.Anything).Return(nil, 0, nil).Once() - txm := NewTxm(logger.Test(t), testutils.FixtureChainID, client, ab, mTxStore, nil, config, keystore) + tm := txm.NewTxm(logger.Test(t), testutils.FixtureChainID, client, ab, mTxStore, nil, config, keystore, nil) mTxStore.On("UpdateUnstartedTransactionWithNonce", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("call failed")).Once() - bo, err := txm.broadcastTransaction(ctx, address) + bo, err := tm.BroadcastTransaction(ctx, address) assert.False(t, bo) require.Error(t, err) require.ErrorContains(t, err, "call failed") @@ -162,22 +166,22 @@ func TestBroadcastTransaction(t *testing.T) { lggr := logger.Test(t) txStore := storage.NewInMemoryStoreManager(lggr, testutils.FixtureChainID) require.NoError(t, txStore.Add(address)) - txm := NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, config, keystore) - bo, err := txm.broadcastTransaction(ctx, address) + tm := txm.NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, config, keystore, nil) + bo, err := tm.BroadcastTransaction(ctx, address) require.NoError(t, err) assert.False(t, bo) - assert.Equal(t, uint64(0), txm.getNonce(address)) + assert.Equal(t, uint64(0), tm.GetNonce(address)) }) t.Run("picks a new tx and creates a new attempt then sends it and updates the broadcast time", func(t *testing.T) { lggr := logger.Test(t) txStore := storage.NewInMemoryStoreManager(lggr, testutils.FixtureChainID) require.NoError(t, txStore.Add(address)) - txm := NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, config, keystore) - txm.setNonce(address, 8) - metrics, err := NewTxmMetrics(testutils.FixtureChainID) + tm := txm.NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, config, keystore, nil) + tm.SetNonce(address, 8) + metrics, err := txm.NewTxmMetrics(testutils.FixtureChainID) require.NoError(t, err) - txm.metrics = metrics + tm.Metrics = metrics IDK := "IDK" txRequest := &types.TxRequest{ Data: []byte{100, 200}, @@ -187,20 +191,20 @@ func TestBroadcastTransaction(t *testing.T) { ToAddress: testutils.NewAddress(), SpecifiedGasLimit: 22000, } - tx, err := txm.CreateTransaction(t.Context(), txRequest) + tx, err := tm.CreateTransaction(t.Context(), txRequest) require.NoError(t, err) attempt := &types.Attempt{ TxID: tx.ID, Fee: gas.EvmFee{GasPrice: assets.NewWeiI(1)}, GasLimit: 22000, } - ab.On("NewAttempt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(attempt, nil).Once() + ab.On("NewAgnosticBumpAttempt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(attempt, nil).Once() client.On("SendTransaction", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - bo, err := txm.broadcastTransaction(ctx, address) + bo, err := tm.BroadcastTransaction(ctx, address) require.NoError(t, err) assert.False(t, bo) - assert.Equal(t, uint64(9), txm.getNonce(address)) + assert.Equal(t, uint64(9), tm.GetNonce(address)) tx, err = txStore.FindTxWithIdempotencyKey(t.Context(), IDK) require.NoError(t, err) assert.Len(t, tx.Attempts, 1) @@ -214,30 +218,29 @@ func TestBroadcastTransaction(t *testing.T) { func TestBackfillTransactions(t *testing.T) { t.Parallel() - client := newMockClient(t) - ab := newMockAttemptBuilder(t) - txStore := newMockTxStore(t) - config := Config{} + client := txm.NewMockClient(t) + txStore := txm.NewMockTxStore(t) + config := txm.Config{} address := testutils.NewAddress() keystore := keystest.Addresses{} t.Run("fails if latest nonce fetching fails", func(t *testing.T) { - txm := NewTxm(logger.Test(t), testutils.FixtureChainID, client, ab, txStore, nil, config, keystore) + ab := txm.NewMockAttemptBuilder(t) + txm := txm.NewTxm(logger.Test(t), testutils.FixtureChainID, client, ab, txStore, nil, config, keystore, nil) client.On("NonceAt", mock.Anything, address, mock.Anything).Return(uint64(0), errors.New("latest nonce fail")).Once() - bo, err := txm.backfillTransactions(t.Context(), address) + err := txm.BackfillTransactions(t.Context(), address) require.Error(t, err) - assert.False(t, bo) require.ErrorContains(t, err, "latest nonce fail") }) t.Run("fails if MarkConfirmedAndReorgedTransactions fails", func(t *testing.T) { - txm := NewTxm(logger.Test(t), testutils.FixtureChainID, client, ab, txStore, nil, config, keystore) + ab := txm.NewMockAttemptBuilder(t) + txm := txm.NewTxm(logger.Test(t), testutils.FixtureChainID, client, ab, txStore, nil, config, keystore, nil) client.On("NonceAt", mock.Anything, address, mock.Anything).Return(uint64(0), nil).Once() txStore.On("MarkConfirmedAndReorgedTransactions", mock.Anything, mock.Anything, address). Return([]*types.Transaction{}, []uint64{}, errors.New("marking transactions confirmed failed")).Once() - bo, err := txm.backfillTransactions(t.Context(), address) + err := txm.BackfillTransactions(t.Context(), address) require.Error(t, err) - assert.False(t, bo) require.ErrorContains(t, err, "marking transactions confirmed failed") }) @@ -245,12 +248,12 @@ func TestBackfillTransactions(t *testing.T) { lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) txStore := storage.NewInMemoryStoreManager(lggr, testutils.FixtureChainID) require.NoError(t, txStore.Add(address)) - ab := newMockAttemptBuilder(t) - c := Config{EIP1559: false, BlockTime: 10 * time.Minute, RetryBlockThreshold: 10, EmptyTxLimitDefault: 22000} - txm := NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, c, keystore) - emptyMetrics, err := NewTxmMetrics(testutils.FixtureChainID) + ab := txm.NewMockAttemptBuilder(t) + c := txm.Config{EIP1559: false, BlockTime: 10 * time.Minute, RetryBlockThreshold: 10, EmptyTxLimitDefault: 22000} + tm := txm.NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, c, keystore, nil) + emptyMetrics, err := txm.NewTxmMetrics(testutils.FixtureChainID) require.NoError(t, err) - txm.metrics = emptyMetrics + tm.Metrics = emptyMetrics // Add a new transaction that will be assigned with nonce = 1. Nonce = 0 is not being tracked by the txStore. This will trigger a nonce gap. txRequest := &types.TxRequest{ @@ -258,7 +261,7 @@ func TestBackfillTransactions(t *testing.T) { FromAddress: address, ToAddress: testutils.NewAddress(), } - _, err = txm.CreateTransaction(t.Context(), txRequest) + _, err = tm.CreateTransaction(t.Context(), txRequest) require.NoError(t, err) _, err = txStore.UpdateUnstartedTransactionWithNonce(t.Context(), address, 1) // Create nonce gap require.NoError(t, err) @@ -272,11 +275,10 @@ func TestBackfillTransactions(t *testing.T) { Fee: gas.EvmFee{GasPrice: assets.NewWeiI(1)}, GasLimit: 22000, } - ab.On("NewAttempt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(attempt, nil).Once() + ab.On("NewAgnosticBumpAttempt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(attempt, nil).Once() client.On("SendTransaction", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - bo, err := txm.backfillTransactions(t.Context(), address) + err = tm.BackfillTransactions(t.Context(), address) require.NoError(t, err) - assert.False(t, bo) tests.AssertLogEventually(t, observedLogs, fmt.Sprintf("Nonce gap at nonce: %d - address: %v. Creating a new transaction", 0, address)) _, count, err := txStore.FetchUnconfirmedTransactionAtNonceWithCount(t.Context(), 0, address) require.NoError(t, err) @@ -287,12 +289,12 @@ func TestBackfillTransactions(t *testing.T) { lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) txStore := storage.NewInMemoryStoreManager(lggr, testutils.FixtureChainID) require.NoError(t, txStore.Add(address)) - ab := newMockAttemptBuilder(t) - c := Config{EIP1559: false, BlockTime: 1 * time.Second, RetryBlockThreshold: 1, EmptyTxLimitDefault: 22000} - txm := NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, c, keystore) - emptyMetrics, err := NewTxmMetrics(testutils.FixtureChainID) + ab := txm.NewMockAttemptBuilder(t) + c := txm.Config{EIP1559: false, BlockTime: 1 * time.Second, RetryBlockThreshold: 1, EmptyTxLimitDefault: 22000} + tm := txm.NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, c, keystore, nil) + emptyMetrics, err := txm.NewTxmMetrics(testutils.FixtureChainID) require.NoError(t, err) - txm.metrics = emptyMetrics + tm.Metrics = emptyMetrics IDK := "IDK" txRequest := &types.TxRequest{ @@ -303,7 +305,7 @@ func TestBackfillTransactions(t *testing.T) { ToAddress: testutils.NewAddress(), SpecifiedGasLimit: 22000, } - tx, err := txm.CreateTransaction(t.Context(), txRequest) + tx, err := tm.CreateTransaction(t.Context(), txRequest) require.NoError(t, err) _, err = txStore.UpdateUnstartedTransactionWithNonce(t.Context(), address, 0) require.NoError(t, err) @@ -313,11 +315,62 @@ func TestBackfillTransactions(t *testing.T) { Fee: gas.EvmFee{GasPrice: assets.NewWeiI(1)}, GasLimit: 22000, } - ab.On("NewAttempt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(attempt, nil).Once() + ab.On("NewAgnosticBumpAttempt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(attempt, nil).Once() client.On("NonceAt", mock.Anything, address, mock.Anything).Return(uint64(0), nil).Once() client.On("SendTransaction", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - _, err = txm.backfillTransactions(t.Context(), address) + err = tm.BackfillTransactions(t.Context(), address) + require.NoError(t, err) + tests.AssertLogEventually(t, observedLogs, fmt.Sprintf("Rebroadcasting attempt for txID: %d", attempt.TxID)) + }) + + t.Run("retries instantly if the attempt is purgeable", func(t *testing.T) { + lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) + txStore := storage.NewInMemoryStoreManager(lggr, testutils.FixtureChainID) + require.NoError(t, txStore.Add(address)) + ab := txm.NewMockAttemptBuilder(t) + c := txm.Config{EIP1559: false, BlockTime: 1 * time.Second, RetryBlockThreshold: 10, EmptyTxLimitDefault: 22000} + tm := txm.NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, c, keystore, nil) + emptyMetrics, err := txm.NewTxmMetrics(testutils.FixtureChainID) + require.NoError(t, err) + tm.Metrics = emptyMetrics + + IDK := "IDK" + txRequest := &types.TxRequest{ + Data: []byte{100, 200}, + IdempotencyKey: &IDK, + ChainID: testutils.FixtureChainID, + FromAddress: address, + ToAddress: testutils.NewAddress(), + SpecifiedGasLimit: 22000, + } + _, err = tm.CreateTransaction(t.Context(), txRequest) + require.NoError(t, err) + tx, err := txStore.UpdateUnstartedTransactionWithNonce(t.Context(), address, 0) + require.NoError(t, err) + + attempt := &types.Attempt{ + TxID: tx.ID, + Fee: gas.EvmFee{GasPrice: assets.NewWeiI(1)}, + GasLimit: 22000, + Hash: testutils.NewHash(), + } + require.NoError(t, txStore.AppendAttemptToTransaction(t.Context(), *tx.Nonce, address, attempt)) + require.NoError(t, txStore.UpdateTransactionBroadcast(t.Context(), tx.ID, *tx.Nonce, attempt.Hash, address)) + require.NoError(t, txStore.MarkUnconfirmedTransactionPurgeable(t.Context(), *tx.Nonce, address)) + + client.On("NonceAt", mock.Anything, address, mock.Anything).Return(uint64(0), nil).Once() + ab.On("NewAgnosticBumpAttempt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(attempt, nil).Once() + client.On("SendTransaction", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + err = tm.BackfillTransactions(t.Context(), address) + require.NoError(t, err) + tests.AssertLogEventually(t, observedLogs, fmt.Sprintf("Rebroadcasting attempt for txID: %d", attempt.TxID)) + + // Broadcasted once an empty transaction but it didn't get confirmed, so we need to broadcast again. + client.On("NonceAt", mock.Anything, address, mock.Anything).Return(uint64(0), nil).Once() + ab.On("NewAgnosticBumpAttempt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(attempt, nil).Once() + client.On("SendTransaction", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + err = tm.BackfillTransactions(t.Context(), address) require.NoError(t, err) tests.AssertLogEventually(t, observedLogs, fmt.Sprintf("Rebroadcasting attempt for txID: %d", attempt.TxID)) }) @@ -327,12 +380,13 @@ func TestBackfillTransactions(t *testing.T) { lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) txStore := storage.NewInMemoryStoreManager(lggr, testutils.FixtureChainID) require.NoError(t, txStore.Add(address)) - txm := NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, config, keystore) + ab := txm.NewMockAttemptBuilder(t) + tm := txm.NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, config, keystore, nil) var nonce uint64 = 8 - txm.setNonce(address, nonce) - metrics, err := NewTxmMetrics(testutils.FixtureChainID) + tm.SetNonce(address, nonce) + metrics, err := txm.NewTxmMetrics(testutils.FixtureChainID) require.NoError(t, err) - txm.metrics = metrics + tm.Metrics = metrics IDK := "IDK" txRequest := &types.TxRequest{ Data: []byte{100, 200}, @@ -342,7 +396,7 @@ func TestBackfillTransactions(t *testing.T) { ToAddress: testutils.NewAddress(), SpecifiedGasLimit: 22000, } - tx, err := txm.CreateTransaction(t.Context(), txRequest) + tx, err := tm.CreateTransaction(t.Context(), txRequest) require.NoError(t, err) _, err = txStore.UpdateUnstartedTransactionWithNonce(ctx, address, nonce) require.NoError(t, err) @@ -357,10 +411,10 @@ func TestBackfillTransactions(t *testing.T) { require.NoError(t, txStore.AppendAttemptToTransaction(ctx, nonce, address, attempt)) } client.On("NonceAt", mock.Anything, address, mock.Anything).Return(nonce, nil).Once() - ab.On("NewAttempt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(attempt, nil).Once() + ab.On("NewAgnosticBumpAttempt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(attempt, nil).Once() client.On("SendTransaction", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - _, err = txm.backfillTransactions(ctx, address) + err = tm.BackfillTransactions(ctx, address) require.NoError(t, err) tx2, err := txStore.FindTxWithIdempotencyKey(t.Context(), IDK) @@ -375,3 +429,171 @@ func TestBackfillTransactions(t *testing.T) { tests.AssertLogEventually(t, observedLogs, fmt.Sprintf("Reached max attempts threshold for txID: %d", 0)) }) } + +func TestFlow_ResendTransaction(t *testing.T) { + t.Parallel() + + client := txm.NewMockClient(t) + txStoreManager := storage.NewInMemoryStoreManager(logger.Test(t), testutils.FixtureChainID) + address := testutils.NewAddress() + require.NoError(t, txStoreManager.Add(address)) + config := txm.Config{EIP1559: true, EmptyTxLimitDefault: 22000, RetryBlockThreshold: 1, BlockTime: 2 * time.Second} + mockEstimator := mocks.NewEvmFeeEstimator(t) + defaultGasLimit := uint64(100000) + keystore := &keystest.FakeChainStore{} + attemptBuilder := txm.NewAttemptBuilder(func(address common.Address) *assets.Wei { return assets.NewWeiI(1) }, mockEstimator, keystore, 22000) + stuckTxDetector := txm.NewStuckTxDetector(logger.Test(t), "", txm.StuckTxDetectorConfig{BlockTime: config.BlockTime, StuckTxBlockThreshold: uint32(config.RetryBlockThreshold + 1)}) + tm := txm.NewTxm(logger.Test(t), testutils.FixtureChainID, client, attemptBuilder, txStoreManager, stuckTxDetector, config, keystore, nil) + metrics, err := txm.NewTxmMetrics(testutils.FixtureChainID) + require.NoError(t, err) + tm.Metrics = metrics + initialNonce := uint64(0) + tm.SetNonce(address, initialNonce) + IDK := "IDK" + + // Create transaction + _, err = tm.CreateTransaction(t.Context(), &types.TxRequest{ + IdempotencyKey: &IDK, + ChainID: testutils.FixtureChainID, + FromAddress: address, + ToAddress: testutils.NewAddress(), + }) + require.NoError(t, err) + + // Broadcast transaction + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{DynamicFee: gas.DynamicFee{GasTipCap: assets.NewWeiI(5), GasFeeCap: assets.NewWeiI(10)}}, defaultGasLimit, nil).Once() + client.On("SendTransaction", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + _, err = tm.BroadcastTransaction(t.Context(), address) + require.NoError(t, err) + + // Backfill transaction + client.On("NonceAt", mock.Anything, mock.Anything, mock.Anything).Return(initialNonce, nil).Maybe() // Transaction was not confirmed + require.NoError(t, tm.BackfillTransactions(t.Context(), address)) + + // Set LastBroadcastAt to a time in the past to trigger retry condition + txStore := txStoreManager.InMemoryStoreMap[address] + require.NotNil(t, txStore) + tx := txStore.UnconfirmedTransactions[initialNonce] + require.NotNil(t, tx) + pastTime := time.Now().Add(-(config.BlockTime*time.Duration(config.RetryBlockThreshold) + 1*time.Second)) + tx.LastBroadcastAt = &pastTime + + // Retry with bumped fee + client.On("NonceAt", mock.Anything, mock.Anything, mock.Anything).Return(initialNonce, nil).Maybe() // Transaction was not confirmed again + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{DynamicFee: gas.DynamicFee{GasTipCap: assets.NewWeiI(5), GasFeeCap: assets.NewWeiI(10)}}, defaultGasLimit, nil).Once() + mockEstimator.On("BumpFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{DynamicFee: gas.DynamicFee{GasTipCap: assets.NewWeiI(6), GasFeeCap: assets.NewWeiI(12)}}, defaultGasLimit, nil).Once() + client.On("SendTransaction", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + require.NoError(t, tm.BackfillTransactions(t.Context(), address)) // retry + + // Set LastBroadcastAt to a time in the past to trigger purge condition + pastTime = time.Now().Add(-(config.BlockTime*time.Duration(config.RetryBlockThreshold) + 2*time.Second)) + tx.LastBroadcastAt = &pastTime + + // Purge transaction + client.On("NonceAt", mock.Anything, mock.Anything, mock.Anything).Return(initialNonce, nil).Maybe() // Transaction was not confirmed again + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{DynamicFee: gas.DynamicFee{GasTipCap: assets.NewWeiI(5), GasFeeCap: assets.NewWeiI(10)}}, defaultGasLimit, nil).Once() + mockEstimator.On("BumpFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{DynamicFee: gas.DynamicFee{GasTipCap: assets.NewWeiI(6), GasFeeCap: assets.NewWeiI(12)}}, defaultGasLimit, nil).Once() + mockEstimator.On("BumpFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{DynamicFee: gas.DynamicFee{GasTipCap: assets.NewWeiI(7), GasFeeCap: assets.NewWeiI(14)}}, defaultGasLimit, nil).Once() + mockEstimator.On("BumpFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{}, uint64(0), fees.ErrConnectivity).Once() // Purgeable transactions bump up the connectivity percentile, where error is returned + client.On("SendTransaction", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + require.NoError(t, tm.BackfillTransactions(t.Context(), address)) // retry + + // Instant retransmission of purgeable transaction + client.On("NonceAt", mock.Anything, mock.Anything, mock.Anything).Return(initialNonce, nil).Maybe() // Transaction was not confirmed again + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{DynamicFee: gas.DynamicFee{GasTipCap: assets.NewWeiI(5), GasFeeCap: assets.NewWeiI(10)}}, defaultGasLimit, nil).Once() + mockEstimator.On("BumpFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{DynamicFee: gas.DynamicFee{GasTipCap: assets.NewWeiI(6), GasFeeCap: assets.NewWeiI(12)}}, defaultGasLimit, nil).Once() + mockEstimator.On("BumpFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{DynamicFee: gas.DynamicFee{GasTipCap: assets.NewWeiI(7), GasFeeCap: assets.NewWeiI(14)}}, defaultGasLimit, nil).Once() + mockEstimator.On("BumpFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{DynamicFee: gas.DynamicFee{GasTipCap: assets.NewWeiI(8), GasFeeCap: assets.NewWeiI(16)}}, defaultGasLimit, nil).Once() + mockEstimator.On("BumpFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{}, uint64(0), fees.ErrConnectivity).Once() // Purgeable transactions bump up the connectivity percentile, where error is returned + client.On("SendTransaction", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + require.NoError(t, tm.BackfillTransactions(t.Context(), address)) // retry +} + +func TestFlow_ErrorHandler(t *testing.T) { + t.Parallel() + + client := txm.NewMockClient(t) + txStoreManager := storage.NewInMemoryStoreManager(logger.Test(t), testutils.FixtureChainID) + address := testutils.NewAddress() + require.NoError(t, txStoreManager.Add(address)) + config := txm.Config{EIP1559: true, EmptyTxLimitDefault: 22000, RetryBlockThreshold: 0, BlockTime: 2 * time.Second} + mockEstimator := mocks.NewEvmFeeEstimator(t) + keystore := &keystest.FakeChainStore{} + attemptBuilder := txm.NewAttemptBuilder(func(address common.Address) *assets.Wei { return assets.NewWeiI(1) }, mockEstimator, keystore, 22000) + stuckTxDetector := txm.NewStuckTxDetector(logger.Test(t), "", txm.StuckTxDetectorConfig{BlockTime: config.BlockTime, StuckTxBlockThreshold: uint32(config.RetryBlockThreshold + 1)}) + errorHandler := dualbroadcast.NewErrorHandler() + tm := txm.NewTxm(logger.Test(t), testutils.FixtureChainID, client, attemptBuilder, txStoreManager, stuckTxDetector, config, keystore, errorHandler) + metrics, err := txm.NewTxmMetrics(testutils.FixtureChainID) + require.NoError(t, err) + tm.Metrics = metrics + initialNonce := uint64(0) + tm.SetNonce(address, initialNonce) + defaultGasLimit := uint64(100000) + + // Create transaction + IDK := "IDK" + _, err = tm.CreateTransaction(t.Context(), &types.TxRequest{ + IdempotencyKey: &IDK, + ChainID: testutils.FixtureChainID, + FromAddress: address, + ToAddress: testutils.NewAddress(), + }) + require.NoError(t, err) + + // Broadcast transaction + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{DynamicFee: gas.DynamicFee{GasTipCap: assets.NewWeiI(5), GasFeeCap: assets.NewWeiI(10)}}, defaultGasLimit, nil).Once() + client.On("SendTransaction", mock.Anything, mock.Anything, mock.Anything).Return(errors.New(dualbroadcast.NoBidsError)).Once() + _, err = tm.BroadcastTransaction(t.Context(), address) + require.Error(t, err) + require.ErrorContains(t, err, "transaction with txID: 0 marked as fatal") + + // Create transaction 2 + IDK2 := "IDK2" + _, err = tm.CreateTransaction(t.Context(), &types.TxRequest{ + IdempotencyKey: &IDK2, + ChainID: testutils.FixtureChainID, + FromAddress: address, + ToAddress: testutils.NewAddress(), + }) + require.NoError(t, err) + + // Broadcast transaction successfully. First transaction is marked as fatal and removed from the store. Transaction 2 takes its nonce. + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{DynamicFee: gas.DynamicFee{GasTipCap: assets.NewWeiI(5), GasFeeCap: assets.NewWeiI(10)}}, defaultGasLimit, nil).Once() + client.On("SendTransaction", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + _, err = tm.BroadcastTransaction(t.Context(), address) + require.NoError(t, err) + tx, count, err := txStoreManager.FetchUnconfirmedTransactionAtNonceWithCount(t.Context(), 0, address) + require.NoError(t, err) + require.Equal(t, 1, count) + require.NotNil(t, IDK, tx.IdempotencyKey) + + // Retry but don't mark transaction as fatal if there is already an attempt. + client.On("NonceAt", mock.Anything, mock.Anything, mock.Anything).Return(initialNonce, nil).Maybe() // Transaction was not confirmed again + mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{DynamicFee: gas.DynamicFee{GasTipCap: assets.NewWeiI(5), GasFeeCap: assets.NewWeiI(10)}}, defaultGasLimit, nil).Once() + mockEstimator.On("BumpFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{DynamicFee: gas.DynamicFee{GasTipCap: assets.NewWeiI(6), GasFeeCap: assets.NewWeiI(12)}}, defaultGasLimit, nil).Once() + client.On("SendTransaction", mock.Anything, mock.Anything, mock.Anything).Return(errors.New(dualbroadcast.NoBidsError)).Once() + err = tm.BackfillTransactions(t.Context(), address) // retry + require.Error(t, err) + require.ErrorContains(t, err, dualbroadcast.NoBidsError) + tx, count, err = txStoreManager.FetchUnconfirmedTransactionAtNonceWithCount(t.Context(), 0, address) // same transaction is still in the store + require.NoError(t, err) + require.Equal(t, 1, count) + require.NotNil(t, IDK, tx.IdempotencyKey) + require.Equal(t, uint16(2), tx.AttemptCount) +} diff --git a/pkg/txmgr/builder.go b/pkg/txmgr/builder.go index cd1c6df4fc..912115a36a 100644 --- a/pkg/txmgr/builder.go +++ b/pkg/txmgr/builder.go @@ -2,6 +2,7 @@ package txmgr import ( "context" + "errors" "fmt" "math/big" "time" @@ -113,6 +114,7 @@ func NewTxmV2( logPoller logpoller.LogPoller, keyStore keys.ChainStore, estimator gas.EvmFeeEstimator, + gasEstimatorConfig config.GasEstimator, ) (TxManager, error) { var fwdMgr *forwarders.FwdMgr if txConfig.ForwardersEnabled() { @@ -137,26 +139,32 @@ func NewTxmV2( stuckTxDetector = txm.NewStuckTxDetector(lggr, chainConfig.ChainType(), stuckTxDetectorConfig) } - attemptBuilder := txm.NewAttemptBuilder(fCfg.PriceMaxKey, estimator, keyStore) + // TODO: temporary check until we implement the required methods on the estimator interface + if gasEstimatorConfig.Mode() != "BlockHistory" || gasEstimatorConfig.BlockHistory().CheckInclusionBlocks() == 0 { + return nil, errors.New("only BlockHistory mode with CheckInclusionBlocks > 0 is supported for TXMv2") + } + + attemptBuilder := txm.NewAttemptBuilder(fCfg.PriceMaxKey, estimator, keyStore, gasEstimatorConfig.LimitTransfer()) inMemoryStoreManager := storage.NewInMemoryStoreManager(lggr, chainID) config := txm.Config{ EIP1559: fCfg.EIP1559DynamicFees(), BlockTime: *txmV2Config.BlockTime(), //nolint:gosec // reuse existing config until migration RetryBlockThreshold: uint16(fCfg.BumpThreshold()), - EmptyTxLimitDefault: fCfg.LimitDefault(), + EmptyTxLimitDefault: gasEstimatorConfig.LimitTransfer(), } + var eh txm.ErrorHandler var c txm.Client if txmV2Config.DualBroadcast() != nil && *txmV2Config.DualBroadcast() && txmV2Config.CustomURL() != nil { var err error - c, err = dualbroadcast.SelectClient(lggr, client, keyStore, txmV2Config.CustomURL(), chainID) + c, eh, err = dualbroadcast.SelectClient(lggr, client, keyStore, txmV2Config.CustomURL(), chainID) if err != nil { return nil, fmt.Errorf("failed to create dual broadcast client: %w", err) } } else { c = clientwrappers.NewChainClient(client) } - t := txm.NewTxm(lggr, chainID, c, attemptBuilder, inMemoryStoreManager, stuckTxDetector, config, keyStore) + t := txm.NewTxm(lggr, chainID, c, attemptBuilder, inMemoryStoreManager, stuckTxDetector, config, keyStore, eh) return txm.NewTxmOrchestrator(lggr, chainID, t, inMemoryStoreManager, fwdMgr, keyStore, attemptBuilder), nil }