Skip to content

Commit d328541

Browse files
author
corey
committed
fix tx manager
1 parent 71d75d9 commit d328541

File tree

1 file changed

+150
-6
lines changed

1 file changed

+150
-6
lines changed

token-price-oracle/updater/tx_manager.go

Lines changed: 150 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package updater
33
import (
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
1735
type 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
3151
func (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
75219
func (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

Comments
 (0)