@@ -3,6 +3,7 @@ package updater
33import (
44 "context"
55 "fmt"
6+ "math/big"
67 "sync"
78 "time"
89
@@ -13,10 +14,28 @@ import (
1314 "morph-l2/token-price-oracle/client"
1415)
1516
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+
1634// TxManager manages transaction sending to avoid nonce conflicts
1735type TxManager struct {
18- l2Client * client.L2Client
19- mu sync.Mutex
36+ l2Client * client.L2Client
37+ mu sync.Mutex
38+ pendingTx * PendingTxInfo // Track the last pending transaction
2039}
2140
2241// NewTxManager creates a new transaction manager
@@ -28,49 +47,174 @@ func NewTxManager(l2Client *client.L2Client) *TxManager {
2847
2948// SendTransaction sends a transaction in a thread-safe manner
3049// 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
3151func (m * TxManager ) SendTransaction (ctx context.Context , txFunc func (* bind.TransactOpts ) (* types.Transaction , error )) (* types.Receipt , error ) {
3252 m .mu .Lock ()
3353 defer m .mu .Unlock ()
3454
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+ }
70+ }
71+
3572 // Get transaction options (returns a copy)
3673 auth := m .l2Client .GetOpts ()
3774 auth .Context = ctx
38-
75+
3976 // First, estimate gas with GasLimit = 0
4077 auth .GasLimit = 0
4178 auth .NoSend = true
4279 tx , err := txFunc (auth )
4380 if err != nil {
4481 return nil , fmt .Errorf ("failed to estimate gas: %w" , err )
4582 }
46-
83+
4784 // Use 1.5x of estimated gas as the actual gas limit
4885 estimatedGas := tx .Gas ()
4986 auth .GasLimit = estimatedGas * 3 / 2
5087 log .Info ("Gas estimation completed" , "estimated" , estimatedGas , "actual_limit" , auth .GasLimit )
51-
88+
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+
52115 // Now send the actual transaction
53116 auth .NoSend = false
54117 tx , err = txFunc (auth )
55118 if err != nil {
56119 return nil , err
57120 }
58121
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+
59131 log .Info ("Transaction sent" ,
60132 "tx_hash" , tx .Hash ().Hex (),
61- "gas_limit" , tx .Gas ())
133+ "nonce" , tx .Nonce (),
134+ "gas_limit" , tx .Gas (),
135+ "gas_fee_cap" , tx .GasFeeCap (),
136+ "gas_tip_cap" , tx .GasTipCap ())
62137
63138 // Wait for transaction to be mined with custom timeout and retry logic
64139 receipt , err := m .waitForReceipt (ctx , tx .Hash (), 60 * time .Second , 2 * time .Second )
65140 if err != nil {
66141 log .Error ("Failed to wait for transaction receipt" ,
67142 "tx_hash" , tx .Hash ().Hex (),
68143 "error" , err )
144+ // Don't clear pendingTx here - let next round handle it
69145 return nil , err
70146 }
147+
148+ // Transaction confirmed, clear pending tx
149+ m .pendingTx = nil
71150 return receipt , nil
72151}
73152
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+
74218// waitForReceipt waits for a transaction receipt with timeout and custom polling interval
75219func (m * TxManager ) waitForReceipt (ctx context.Context , txHash common.Hash , timeout , pollInterval time.Duration ) (* types.Receipt , error ) {
76220 deadline := time .Now ().Add (timeout )
0 commit comments