From 98128608c04801fc7196d597421649a4bfca0dca Mon Sep 17 00:00:00 2001 From: owen-eth Date: Tue, 10 Feb 2026 13:36:07 -0500 Subject: [PATCH] fix: resolve nonce drift and duplicate tx errors --- tools/preconf-rpc/handlers/handlers.go | 4 +-- tools/preconf-rpc/sender/sender.go | 18 ------------- tools/preconf-rpc/store/store.go | 11 +++++++- tools/preconf-rpc/store/store_test.go | 37 ++++++++++++++++++++++++-- 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/tools/preconf-rpc/handlers/handlers.go b/tools/preconf-rpc/handlers/handlers.go index b9c38c52c..9d518f77b 100644 --- a/tools/preconf-rpc/handlers/handlers.go +++ b/tools/preconf-rpc/handlers/handlers.go @@ -604,10 +604,10 @@ func (h *rpcMethodHandler) handleGetTxReceipt(ctx context.Context, params ...any } switch txn.Status { - case sender.TxStatusPending, sender.TxStatusConfirmed: + case sender.TxStatusPending, sender.TxStatusConfirmed, sender.TxStatusFailed: // go to RPC proxy return nil, true, nil - case sender.TxStatusPreConfirmed, sender.TxStatusFailed: + case sender.TxStatusPreConfirmed: // continue processing } diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go index 99ae388fc..4585a05a8 100644 --- a/tools/preconf-rpc/sender/sender.go +++ b/tools/preconf-rpc/sender/sender.go @@ -580,24 +580,6 @@ func (t *TxSender) processQueuedTransactions(ctx context.Context) { txn.Status = TxStatusFailed txn.Details = err.Error() t.clearBlockAttemptHistory(txn, time.Now()) - - // Store a failed receipt to unblock the user's wallet - receipt := &types.Receipt{ - Type: txn.Transaction.Type(), - Status: types.ReceiptStatusFailed, - TxHash: txn.Hash(), - ContractAddress: common.Address{}, - GasUsed: 21000, - CumulativeGasUsed: 21000, - BlockHash: common.BytesToHash(big.NewInt(int64(t.blockTracker.LatestBlockNumber())).Bytes()), - BlockNumber: big.NewInt(int64(t.blockTracker.LatestBlockNumber())), - TransactionIndex: 0, - } - if storeErr := t.store.StoreReceipt(ctx, receipt); storeErr != nil { - t.logger.Error("Failed to store failed receipt", "error", storeErr) - } - - defer t.signalReceiptAvailable(txn.Hash()) return t.store.StoreTransaction(ctx, txn, nil, nil) } return nil diff --git a/tools/preconf-rpc/store/store.go b/tools/preconf-rpc/store/store.go index ea4dd9295..86b0b050b 100644 --- a/tools/preconf-rpc/store/store.go +++ b/tools/preconf-rpc/store/store.go @@ -137,7 +137,16 @@ func (s *rpcstore) AddQueuedTransaction(ctx context.Context, tx *sender.Transact } insertQuery := ` INSERT INTO mcTransactions (hash, nonce, raw_transaction, sender, tx_type, status, options) - VALUES ($1, $2, $3, $4, $5, $6, $7); + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (hash) DO UPDATE + SET status = EXCLUDED.status, + nonce = EXCLUDED.nonce, + sender = EXCLUDED.sender, + tx_type = EXCLUDED.tx_type, + options = EXCLUDED.options, + details = EXCLUDED.details, + raw_transaction = EXCLUDED.raw_transaction + WHERE mcTransactions.status != 'confirmed' AND mcTransactions.status != 'pre-confirmed'; ` _, err = s.db.ExecContext( ctx, diff --git a/tools/preconf-rpc/store/store_test.go b/tools/preconf-rpc/store/store_test.go index 818852ee9..8773410e3 100644 --- a/tools/preconf-rpc/store/store_test.go +++ b/tools/preconf-rpc/store/store_test.go @@ -169,9 +169,42 @@ func TestStore(t *testing.T) { } err = st.AddQueuedTransaction(context.Background(), wrappedTxn1) // Adding the same transaction again - if err == nil { - t.Fatalf("expected error when adding duplicate transaction, got nil") + if err != nil { + t.Fatalf("expected nil error when adding duplicate transaction, got %v", err) + } + + // Test retry of failed transaction: + // 1. Manually set txn1 to failed in DB + wrappedTxn1.Status = sender.TxStatusFailed + wrappedTxn1.Details = "failed simulation" + // We use StoreTransaction to update the status in DB + if err := st.StoreTransaction(context.Background(), wrappedTxn1, nil, nil); err != nil { + t.Fatalf("failed to update transaction to failed: %v", err) + } + + // 2. Retry: AddQueuedTransaction should reset status to Pending + wrappedTxn1Retry := &sender.Transaction{ + Transaction: wrappedTxn1.Transaction, + Raw: wrappedTxn1.Raw, + Sender: wrappedTxn1.Sender, + Type: wrappedTxn1.Type, + Status: sender.TxStatusPending, + } + if err := st.AddQueuedTransaction(context.Background(), wrappedTxn1Retry); err != nil { + t.Fatalf("failed to re-queue failed transaction: %v", err) + } + + // 3. Verify status is Pending + retrievedTxn1, err := st.GetTransactionByHash(context.Background(), wrappedTxn1.Hash()) + if err != nil { + t.Fatalf("failed to retrieve retried transaction: %v", err) + } + if retrievedTxn1.Status != sender.TxStatusPending { + t.Fatalf("expected status to be pending after retry, got %s", retrievedTxn1.Status) } + // Restore wrappedTxn1 status for subsequent tests + wrappedTxn1.Status = sender.TxStatusPending + wrappedTxn1.Details = "" err = st.AddQueuedTransaction(context.Background(), wrappedTxn2) if err != nil {