@@ -2,40 +2,23 @@ package updater
22
33import (
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
3519type 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
5134func (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