diff --git a/core/chains/evm/txmgr/evm_tx_store.go b/core/chains/evm/txmgr/evm_tx_store.go index 95756790cf3..66e0cc84971 100644 --- a/core/chains/evm/txmgr/evm_tx_store.go +++ b/core/chains/evm/txmgr/evm_tx_store.go @@ -1786,8 +1786,17 @@ func (o *evmTxStore) Abandon(ctx context.Context, chainID *big.Int, addr common. var cancel context.CancelFunc ctx, cancel = o.stopCh.Ctx(ctx) defer cancel() - _, err := o.q.ExecContext(ctx, `UPDATE evm.txes SET state='fatal_error', nonce = NULL, error = 'abandoned' WHERE state IN ('unconfirmed', 'in_progress', 'unstarted') AND evm_chain_id = $1 AND from_address = $2`, chainID.String(), addr) - return err + return o.Transact(ctx, false, func(orm *evmTxStore) error { + var abandonedIDs []string + err := orm.q.SelectContext(ctx, &abandonedIDs, `UPDATE evm.txes SET state='fatal_error', nonce = NULL, error = 'abandoned' WHERE state IN ('unconfirmed', 'in_progress', 'unstarted') AND evm_chain_id = $1 AND from_address = $2 RETURNING id`, chainID.String(), addr) + if err != nil { + return fmt.Errorf("failed to mark transactions as abandoned: %w", err) + } + if _, err := orm.q.ExecContext(ctx, `DELETE FROM evm.tx_attempts WHERE eth_tx_id = ANY($1)`, pq.Array(abandonedIDs)); err != nil { + return fmt.Errorf("failed to delete attempts related to abandoned transactions: %w", err) + } + return nil + }) } // Find transactions by a field in the TxMeta blob and transaction states @@ -1916,7 +1925,7 @@ func (o *evmTxStore) FindAttemptsRequiringReceiptFetch(ctx context.Context, chai query := ` SELECT evm.tx_attempts.* FROM evm.tx_attempts JOIN evm.txes ON evm.txes.ID = evm.tx_attempts.eth_tx_id - WHERE evm.tx_attempts.state = 'broadcast' AND evm.txes.state IN ('confirmed', 'confirmed_missing_receipt', 'fatal_error') AND evm.txes.evm_chain_id = $1 AND evm.txes.ID NOT IN ( + WHERE evm.tx_attempts.state = 'broadcast' AND evm.txes.nonce IS NOT NULL AND evm.txes.state IN ('confirmed', 'confirmed_missing_receipt', 'fatal_error') AND evm.txes.evm_chain_id = $1 AND evm.txes.ID NOT IN ( SELECT DISTINCT evm.txes.ID FROM evm.txes JOIN evm.tx_attempts ON evm.tx_attempts.eth_tx_id = evm.txes.ID JOIN evm.receipts ON evm.receipts.tx_hash = evm.tx_attempts.hash diff --git a/core/chains/evm/txmgr/evm_tx_store_test.go b/core/chains/evm/txmgr/evm_tx_store_test.go index a05cf3f9010..9f515c22be4 100644 --- a/core/chains/evm/txmgr/evm_tx_store_test.go +++ b/core/chains/evm/txmgr/evm_tx_store_test.go @@ -1796,11 +1796,16 @@ func TestORM_FindAttemptsRequiringReceiptFetch(t *testing.T) { // Terminally stuck transaction with receipt should NOT be picked up for receipt fetch stuckTx := mustInsertTerminallyStuckTxWithAttempt(t, txStore, fromAddress, 1, blockNum) mustInsertEthReceipt(t, txStore, blockNum, utils.NewHash(), stuckTx.TxAttempts[0].Hash) + // Fatal transactions with nil nonce and stored attempts should NOT be picked up for receipt fetch + fatalTxWithAttempt := mustInsertFatalErrorEthTx(t, txStore, fromAddress) + attempt := newBroadcastLegacyEthTxAttempt(t, fatalTxWithAttempt.ID) + err := txStore.InsertTxAttempt(ctx, &attempt) + require.NoError(t, err) // Confirmed transaction without receipt should be picked up for receipt fetch confirmedTx := mustInsertConfirmedEthTx(t, txStore, 0, fromAddress) - attempt := newBroadcastLegacyEthTxAttempt(t, confirmedTx.ID) - err := txStore.InsertTxAttempt(ctx, &attempt) + attempt = newBroadcastLegacyEthTxAttempt(t, confirmedTx.ID) + err = txStore.InsertTxAttempt(ctx, &attempt) require.NoError(t, err) attempts, err := txStore.FindAttemptsRequiringReceiptFetch(ctx, testutils.FixtureChainID) @@ -1823,6 +1828,12 @@ func TestORM_FindAttemptsRequiringReceiptFetch(t *testing.T) { // Terminally stuck transaction with receipt should NOT be picked up for receipt fetch stuckTxWithReceipt := mustInsertTerminallyStuckTxWithAttempt(t, txStore, fromAddress, 1, blockNum) mustInsertEthReceipt(t, txStore, blockNum, utils.NewHash(), stuckTxWithReceipt.TxAttempts[0].Hash) + // Fatal transactions with nil nonce and stored attempts should NOT be picked up for receipt fetch + fatalTxWithAttempt := mustInsertFatalErrorEthTx(t, txStore, fromAddress) + attempt := newBroadcastLegacyEthTxAttempt(t, fatalTxWithAttempt.ID) + err := txStore.InsertTxAttempt(ctx, &attempt) + require.NoError(t, err) + // Terminally stuck transaction without receipt should be picked up for receipt fetch stuckTxWoutReceipt := mustInsertTerminallyStuckTxWithAttempt(t, txStore, fromAddress, 0, blockNum) @@ -2008,6 +2019,41 @@ func TestORM_DeleteReceiptsByTxHash(t *testing.T) { require.Len(t, etx2.TxAttempts[0].Receipts, 1) } +func TestORM_Abandon(t *testing.T) { + t.Parallel() + + db := pgtest.NewSqlxDB(t) + txStore := cltest.NewTestTxStore(t, db) + ctx := tests.Context(t) + ethKeyStore := cltest.NewKeyStore(t, db).Eth() + _, fromAddress := cltest.MustInsertRandomKeyReturningState(t, ethKeyStore) + etx1 := mustCreateUnstartedGeneratedTx(t, txStore, fromAddress, testutils.FixtureChainID) + etx2 := mustInsertInProgressEthTxWithAttempt(t, txStore, 1, fromAddress) + etx3 := mustInsertUnconfirmedEthTxWithAttemptState(t, txStore, 0, fromAddress, txmgrtypes.TxAttemptBroadcast) + + err := txStore.Abandon(ctx, testutils.FixtureChainID, fromAddress) + require.NoError(t, err) + + // transactions marked as fatal error with abandon reason, nil sequence, and no attempts + etx1, err = txStore.FindTxWithAttempts(ctx, etx1.ID) + require.NoError(t, err) + require.Equal(t, txmgrcommon.TxFatalError, etx1.State) + require.Nil(t, etx1.Sequence) + require.Empty(t, etx1.TxAttempts) + + etx2, err = txStore.FindTxWithAttempts(ctx, etx2.ID) + require.NoError(t, err) + require.Equal(t, txmgrcommon.TxFatalError, etx2.State) + require.Nil(t, etx2.Sequence) + require.Empty(t, etx2.TxAttempts) + + etx3, err = txStore.FindTxWithAttempts(ctx, etx3.ID) + require.NoError(t, err) + require.Equal(t, txmgrcommon.TxFatalError, etx3.State) + require.Nil(t, etx3.Sequence) + require.Empty(t, etx3.TxAttempts) +} + func mustInsertTerminallyStuckTxWithAttempt(t *testing.T, txStore txmgr.TestEvmTxStore, fromAddress common.Address, nonceInt int64, broadcastBeforeBlockNum int64) txmgr.Tx { ctx := tests.Context(t) broadcast := time.Now()