Skip to content

Commit c4dac05

Browse files
author
corey
committed
add nonce checking
1 parent d328541 commit c4dac05

File tree

1 file changed

+83
-159
lines changed

1 file changed

+83
-159
lines changed

token-price-oracle/updater/tx_manager.go

Lines changed: 83 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,23 @@ package updater
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
6-
"math/big"
77
"sync"
88
"time"
99

10+
"github.com/morph-l2/go-ethereum"
1011
"github.com/morph-l2/go-ethereum/accounts/abi/bind"
1112
"github.com/morph-l2/go-ethereum/common"
1213
"github.com/morph-l2/go-ethereum/core/types"
1314
"github.com/morph-l2/go-ethereum/log"
1415
"morph-l2/token-price-oracle/client"
1516
)
1617

17-
const (
18-
// GasPriceBumpPercent is the percentage to increase gas price for replacement tx
19-
// EIP-1559 requires at least 10% bump, we use 15% to be safe
20-
GasPriceBumpPercent = 15
21-
// MaxGasPriceBumpMultiplier limits how much we can bump gas price (e.g., 3x original)
22-
MaxGasPriceBumpMultiplier = 3
23-
)
24-
25-
// PendingTxInfo stores information about a pending transaction
26-
type PendingTxInfo struct {
27-
TxHash common.Hash
28-
Nonce uint64
29-
GasFeeCap *big.Int
30-
GasTipCap *big.Int
31-
SentAt time.Time
32-
}
33-
3418
// TxManager manages transaction sending to avoid nonce conflicts
3519
type TxManager struct {
36-
l2Client *client.L2Client
37-
mu sync.Mutex
38-
pendingTx *PendingTxInfo // Track the last pending transaction
20+
l2Client *client.L2Client
21+
mu sync.Mutex
3922
}
4023

4124
// NewTxManager creates a new transaction manager
@@ -47,28 +30,38 @@ func NewTxManager(l2Client *client.L2Client) *TxManager {
4730

4831
// SendTransaction sends a transaction in a thread-safe manner
4932
// It ensures only one transaction is sent at a time to avoid nonce conflicts
50-
// If there's a pending transaction, it will wait for it or replace it
33+
// Before sending, it checks if there are any pending transactions by comparing nonces
5134
func (m *TxManager) SendTransaction(ctx context.Context, txFunc func(*bind.TransactOpts) (*types.Transaction, error)) (*types.Receipt, error) {
5235
m.mu.Lock()
5336
defer m.mu.Unlock()
5437

55-
// Check if there's a pending transaction that needs to be handled
56-
if m.pendingTx != nil {
57-
receipt, err := m.handlePendingTx(ctx)
58-
if err != nil {
59-
log.Warn("Failed to handle pending transaction, will try to replace",
60-
"pending_tx", m.pendingTx.TxHash.Hex(),
61-
"error", err)
62-
// Continue to send new transaction which may replace the pending one
63-
} else if receipt != nil {
64-
log.Info("Previous pending transaction confirmed",
65-
"tx_hash", m.pendingTx.TxHash.Hex(),
66-
"status", receipt.Status)
67-
m.pendingTx = nil
68-
// Previous tx confirmed, continue to send new transaction
69-
}
38+
fromAddr := m.l2Client.WalletAddress()
39+
40+
// Check if there are pending transactions by comparing nonces
41+
confirmedNonce, err := m.l2Client.GetClient().NonceAt(ctx, fromAddr, nil)
42+
if err != nil {
43+
return nil, fmt.Errorf("failed to get confirmed nonce: %w", err)
44+
}
45+
46+
pendingNonce, err := m.l2Client.GetClient().PendingNonceAt(ctx, fromAddr)
47+
if err != nil {
48+
return nil, fmt.Errorf("failed to get pending nonce: %w", err)
49+
}
50+
51+
if pendingNonce > confirmedNonce {
52+
// There are pending transactions, don't send new one
53+
log.Warn("Found pending transactions, skipping this round",
54+
"address", fromAddr.Hex(),
55+
"confirmed_nonce", confirmedNonce,
56+
"pending_nonce", pendingNonce,
57+
"pending_count", pendingNonce-confirmedNonce)
58+
return nil, fmt.Errorf("pending transactions exist (confirmed: %d, pending: %d)", confirmedNonce, pendingNonce)
7059
}
7160

61+
log.Info("No pending transactions, proceeding to send",
62+
"address", fromAddr.Hex(),
63+
"nonce", confirmedNonce)
64+
7265
// Get transaction options (returns a copy)
7366
auth := m.l2Client.GetOpts()
7467
auth.Context = ctx
@@ -86,173 +79,104 @@ func (m *TxManager) SendTransaction(ctx context.Context, txFunc func(*bind.Trans
8679
auth.GasLimit = estimatedGas * 3 / 2
8780
log.Info("Gas estimation completed", "estimated", estimatedGas, "actual_limit", auth.GasLimit)
8881

89-
// Check if we need to replace a pending transaction (same nonce)
90-
if m.pendingTx != nil {
91-
// Get current nonce from network
92-
fromAddr := m.l2Client.WalletAddress()
93-
pendingNonce, err := m.l2Client.GetClient().PendingNonceAt(ctx, fromAddr)
94-
if err != nil {
95-
log.Warn("Failed to get pending nonce", "error", err)
96-
} else if pendingNonce <= m.pendingTx.Nonce {
97-
// There's still a pending tx with this nonce, need to replace it
98-
log.Info("Replacing pending transaction",
99-
"old_tx", m.pendingTx.TxHash.Hex(),
100-
"old_nonce", m.pendingTx.Nonce,
101-
"pending_nonce", pendingNonce)
102-
103-
// Bump gas price for replacement
104-
auth.Nonce = big.NewInt(int64(m.pendingTx.Nonce))
105-
auth.GasFeeCap, auth.GasTipCap = m.bumpGasPrice(m.pendingTx.GasFeeCap, m.pendingTx.GasTipCap)
106-
107-
log.Info("Gas price bumped for replacement",
108-
"old_fee_cap", m.pendingTx.GasFeeCap,
109-
"new_fee_cap", auth.GasFeeCap,
110-
"old_tip_cap", m.pendingTx.GasTipCap,
111-
"new_tip_cap", auth.GasTipCap)
112-
}
113-
}
114-
11582
// Now send the actual transaction
11683
auth.NoSend = false
11784
tx, err = txFunc(auth)
11885
if err != nil {
11986
return nil, err
12087
}
12188

122-
// Store pending transaction info
123-
m.pendingTx = &PendingTxInfo{
124-
TxHash: tx.Hash(),
125-
Nonce: tx.Nonce(),
126-
GasFeeCap: tx.GasFeeCap(),
127-
GasTipCap: tx.GasTipCap(),
128-
SentAt: time.Now(),
129-
}
130-
13189
log.Info("Transaction sent",
13290
"tx_hash", tx.Hash().Hex(),
13391
"nonce", tx.Nonce(),
134-
"gas_limit", tx.Gas(),
135-
"gas_fee_cap", tx.GasFeeCap(),
136-
"gas_tip_cap", tx.GasTipCap())
92+
"gas_limit", tx.Gas())
13793

138-
// Wait for transaction to be mined with custom timeout and retry logic
139-
receipt, err := m.waitForReceipt(ctx, tx.Hash(), 60*time.Second, 2*time.Second)
94+
// Wait for transaction to be mined - will keep waiting until confirmed or dropped
95+
receipt, err := m.waitForReceipt(ctx, tx.Hash(), 2*time.Second)
14096
if err != nil {
14197
log.Error("Failed to wait for transaction receipt",
14298
"tx_hash", tx.Hash().Hex(),
14399
"error", err)
144-
// Don't clear pendingTx here - let next round handle it
145100
return nil, err
146101
}
147102

148-
// Transaction confirmed, clear pending tx
149-
m.pendingTx = nil
150103
return receipt, nil
151104
}
152105

153-
// handlePendingTx checks if the pending transaction has been confirmed
154-
func (m *TxManager) handlePendingTx(ctx context.Context) (*types.Receipt, error) {
155-
if m.pendingTx == nil {
156-
return nil, nil
157-
}
158-
159-
// Try to get receipt for pending tx
160-
receipt, err := m.l2Client.GetClient().TransactionReceipt(ctx, m.pendingTx.TxHash)
161-
if err == nil && receipt != nil {
162-
return receipt, nil
163-
}
164-
165-
// Check if the nonce has been used (tx might have been replaced)
166-
fromAddr := m.l2Client.WalletAddress()
167-
confirmedNonce, err := m.l2Client.GetClient().NonceAt(ctx, fromAddr, nil)
168-
if err != nil {
169-
return nil, fmt.Errorf("failed to get confirmed nonce: %w", err)
170-
}
171-
172-
if confirmedNonce > m.pendingTx.Nonce {
173-
// Nonce has been used, the tx (or a replacement) was confirmed
174-
log.Info("Pending nonce has been confirmed (possibly replaced)",
175-
"pending_nonce", m.pendingTx.Nonce,
176-
"confirmed_nonce", confirmedNonce)
177-
m.pendingTx = nil
178-
return nil, nil
179-
}
180-
181-
// Transaction still pending
182-
return nil, fmt.Errorf("transaction %s still pending (nonce: %d)", m.pendingTx.TxHash.Hex(), m.pendingTx.Nonce)
183-
}
184-
185-
// bumpGasPrice increases gas price by GasPriceBumpPercent, capped at MaxGasPriceBumpMultiplier
186-
func (m *TxManager) bumpGasPrice(oldFeeCap, oldTipCap *big.Int) (*big.Int, *big.Int) {
187-
// Calculate bump: oldPrice * (100 + GasPriceBumpPercent) / 100
188-
bumpMultiplier := big.NewInt(100 + GasPriceBumpPercent)
189-
hundred := big.NewInt(100)
190-
191-
newFeeCap := new(big.Int).Mul(oldFeeCap, bumpMultiplier)
192-
newFeeCap.Div(newFeeCap, hundred)
193-
194-
newTipCap := new(big.Int).Mul(oldTipCap, bumpMultiplier)
195-
newTipCap.Div(newTipCap, hundred)
196-
197-
// Cap at MaxGasPriceBumpMultiplier times original
198-
maxFeeCap := new(big.Int).Mul(oldFeeCap, big.NewInt(MaxGasPriceBumpMultiplier))
199-
maxTipCap := new(big.Int).Mul(oldTipCap, big.NewInt(MaxGasPriceBumpMultiplier))
200-
201-
if newFeeCap.Cmp(maxFeeCap) > 0 {
202-
log.Warn("Gas fee cap bump capped at max multiplier",
203-
"calculated", newFeeCap,
204-
"capped", maxFeeCap)
205-
newFeeCap = maxFeeCap
206-
}
207-
208-
if newTipCap.Cmp(maxTipCap) > 0 {
209-
log.Warn("Gas tip cap bump capped at max multiplier",
210-
"calculated", newTipCap,
211-
"capped", maxTipCap)
212-
newTipCap = maxTipCap
213-
}
214-
215-
return newFeeCap, newTipCap
216-
}
217-
218-
// waitForReceipt waits for a transaction receipt with timeout and custom polling interval
219-
func (m *TxManager) waitForReceipt(ctx context.Context, txHash common.Hash, timeout, pollInterval time.Duration) (*types.Receipt, error) {
220-
deadline := time.Now().Add(timeout)
106+
// waitForReceipt waits for a transaction receipt indefinitely until:
107+
// 1. Receipt is received (transaction confirmed)
108+
// 2. Transaction is not found (dropped from pool) - exits immediately
109+
// Network errors will cause retry, NOT exit
110+
func (m *TxManager) waitForReceipt(ctx context.Context, txHash common.Hash, pollInterval time.Duration) (*types.Receipt, error) {
221111
ticker := time.NewTicker(pollInterval)
222112
defer ticker.Stop()
223113

224-
log.Debug("Waiting for transaction receipt",
114+
startTime := time.Now()
115+
116+
log.Info("Waiting for transaction receipt (will wait indefinitely)",
225117
"tx_hash", txHash.Hex(),
226-
"timeout", timeout,
227118
"poll_interval", pollInterval)
228119

229120
for {
230-
// Check if we've exceeded the timeout
231-
if time.Now().After(deadline) {
232-
return nil, fmt.Errorf("timeout waiting for transaction %s after %v", txHash.Hex(), timeout)
121+
// Check context cancellation first
122+
select {
123+
case <-ctx.Done():
124+
return nil, fmt.Errorf("context cancelled while waiting for transaction %s (waited %v): %w",
125+
txHash.Hex(), time.Since(startTime), ctx.Err())
126+
default:
233127
}
234128

235-
// Try to get the receipt
129+
// Try to get the receipt first
236130
receipt, err := m.l2Client.GetClient().TransactionReceipt(ctx, txHash)
237131
if err == nil && receipt != nil {
238-
log.Debug("Receipt received",
132+
log.Info("Receipt received",
239133
"tx_hash", txHash.Hex(),
240134
"status", receipt.Status,
241135
"gas_used", receipt.GasUsed,
242-
"block_number", receipt.BlockNumber)
136+
"block_number", receipt.BlockNumber,
137+
"waited", time.Since(startTime))
243138
return receipt, nil
244139
}
245140

141+
// No receipt yet, check if transaction is still in the pool
142+
tx, isPending, err := m.l2Client.GetClient().TransactionByHash(ctx, txHash)
143+
246144
if err != nil {
247-
log.Trace("Receipt retrieval failed, will retry",
145+
// Check if it's a "not found" error - transaction dropped
146+
if errors.Is(err, ethereum.NotFound) {
147+
log.Error("Transaction not found, dropped from pool",
148+
"tx_hash", txHash.Hex(),
149+
"waited", time.Since(startTime))
150+
return nil, fmt.Errorf("transaction %s dropped from pool (not found)", txHash.Hex())
151+
}
152+
153+
// Other errors (network, etc.) - just log and retry
154+
log.Warn("Transaction query failed, will retry",
248155
"tx_hash", txHash.Hex(),
156+
"waited", time.Since(startTime),
249157
"error", err)
158+
} else if tx == nil {
159+
// tx is nil but no error - treat as not found
160+
log.Error("Transaction returned nil, dropped from pool",
161+
"tx_hash", txHash.Hex(),
162+
"waited", time.Since(startTime))
163+
return nil, fmt.Errorf("transaction %s dropped from pool (returned nil)", txHash.Hex())
164+
} else {
165+
// Transaction found, log progress every minute
166+
elapsed := time.Since(startTime)
167+
if int(elapsed.Seconds()) > 0 && int(elapsed.Seconds())%60 == 0 {
168+
log.Info("Still waiting for transaction receipt",
169+
"tx_hash", txHash.Hex(),
170+
"is_pending", isPending,
171+
"waited", elapsed)
172+
}
250173
}
251174

252-
// Wait for next poll or context cancellation
175+
// Wait for next poll
253176
select {
254177
case <-ctx.Done():
255-
return nil, fmt.Errorf("context cancelled while waiting for transaction %s: %w", txHash.Hex(), ctx.Err())
178+
return nil, fmt.Errorf("context cancelled while waiting for transaction %s (waited %v): %w",
179+
txHash.Hex(), time.Since(startTime), ctx.Err())
256180
case <-ticker.C:
257181
// Continue to next iteration
258182
}

0 commit comments

Comments
 (0)