Skip to content

Commit

Permalink
eth transactor: improve gas and nonce control (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaozhou authored Apr 5, 2024
1 parent b651276 commit c153e70
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 35 deletions.
29 changes: 27 additions & 2 deletions eth/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ import (
type txOptions struct {
// Transact
ethValue *big.Int
nonce uint64
// Legacy Tx gas price
minGasGwei uint64
maxGasGwei uint64
addGasGwei uint64
forceGasGwei *uint64 // use pointer to allow forcing zero gas
// EIP-1559 Tx gas price
maxFeePerGasGwei uint64
maxPriorityFeePerGasGwei float64
maxFeePerGasGwei uint64 // aka GasFeeCap in gwei
maxPriorityFeePerGasGwei float64 // aka GasTipCap in gwei
addPriorityFeePerGasGwei float64
addPriorityFeeRatio float64
// Gas limit
gasLimit uint64
addGasEstimateRatio float64
Expand All @@ -43,6 +46,8 @@ const (
defaultTxTimeout = 6 * time.Hour
defaultTxQueryTimeout = 2 * time.Minute
defaultTxQueryRetryInterval = 10 * time.Second
defaultMaxPendingTxNum = 10
defaultMaxSubmittingTxNum = 5
)

// do not return pointer here as defaultTxOptions is always deep copied when used
Expand All @@ -52,6 +57,8 @@ func defaultTxOptions() txOptions {
timeout: defaultTxTimeout,
queryTimeout: defaultTxQueryTimeout,
queryRetryInterval: defaultTxQueryRetryInterval,
maxPendingTxNum: defaultMaxPendingTxNum,
maxSubmittingTxNum: defaultMaxSubmittingTxNum,
}
}

Expand Down Expand Up @@ -79,6 +86,12 @@ func WithEthValue(v *big.Int) TxOption {
})
}

func WithNonce(n uint64) TxOption {
return newFuncTxOption(func(o *txOptions) {
o.nonce = n
})
}

func WithMinGasGwei(g uint64) TxOption {
return newFuncTxOption(func(o *txOptions) {
o.minGasGwei = g
Expand Down Expand Up @@ -122,6 +135,18 @@ func WithMaxPriorityFeePerGasGwei(g float64) TxOption {
})
}

func WithAddPriorityFeePerGasGwei(g float64) TxOption {
return newFuncTxOption(func(o *txOptions) {
o.addPriorityFeePerGasGwei = g
})
}

func WithAddPriorityFeeRatio(r float64) TxOption {
return newFuncTxOption(func(o *txOptions) {
o.addPriorityFeeRatio = r
})
}

func WithGasLimit(l uint64) TxOption {
return newFuncTxOption(func(o *txOptions) {
o.gasLimit = l
Expand Down
89 changes: 56 additions & 33 deletions eth/transactor.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,29 +143,9 @@ func (t *Transactor) transact(
return nil, fmt.Errorf("determineGas err: %w", err)
}
// Set nonce
pendingNonce, err := t.client.PendingNonceAt(context.Background(), t.address)
nonce, pendingNonce, err := t.determineNonce(txopts)
if err != nil {
return nil, fmt.Errorf("PendingNonceAt err: %w", err)
}
if txopts.maxPendingTxNum > 0 {
accountNonce, err := t.client.NonceAt(context.Background(), t.address, nil)
if err != nil {
return nil, fmt.Errorf("NonceAt err: %w", err)
}
if pendingNonce-accountNonce >= txopts.maxPendingTxNum {
return nil, fmt.Errorf("%w, pendingNonce:%d accountNonce:%d limit:%d",
ErrTooManyPendingTx, pendingNonce, accountNonce, txopts.maxPendingTxNum)
}
}
nonce := t.nonce
if pendingNonce > nonce || !t.sentTx || txopts.maxSubmittingTxNum == 1 {
nonce = pendingNonce
} else {
nonce++
}
if txopts.maxSubmittingTxNum > 0 && nonce-pendingNonce >= txopts.maxSubmittingTxNum {
return nil, fmt.Errorf("%w, submittingNonce:%d pendingNonce:%d limit:%d",
ErrTooManySubmittingTx, nonce, pendingNonce, txopts.maxSubmittingTxNum)
return nil, err
}
for {
nonceInt := big.NewInt(0)
Expand Down Expand Up @@ -223,12 +203,46 @@ func (t *Transactor) transact(
}
}

func (t *Transactor) determineNonce(txopts txOptions) (uint64, uint64, error) {
pendingNonce, err := t.client.PendingNonceAt(context.Background(), t.address)
if err != nil {
return 0, 0, fmt.Errorf("PendingNonceAt err: %w", err)
}
nonce := txopts.nonce // init as given by the txOption (should be zero in most cases)

if txopts.nonce == 0 { // nonce given by the txOption is zero, which is the common case
nonce = t.nonce
if pendingNonce > nonce || !t.sentTx || txopts.maxSubmittingTxNum == 1 {
nonce = pendingNonce
} else {
nonce++
}
if txopts.maxPendingTxNum > 0 {
accountNonce, err := t.client.NonceAt(context.Background(), t.address, nil)
if err != nil {
return 0, 0, fmt.Errorf("NonceAt err: %w", err)
}
if pendingNonce-accountNonce >= txopts.maxPendingTxNum {
return 0, 0, fmt.Errorf("%w, pendingNonce:%d accountNonce:%d limit:%d",
ErrTooManyPendingTx, pendingNonce, accountNonce, txopts.maxPendingTxNum)
}
}
}

if txopts.maxSubmittingTxNum > 0 && nonce-pendingNonce >= txopts.maxSubmittingTxNum {
return 0, 0, fmt.Errorf("%w, submittingNonce:%d pendingNonce:%d limit:%d",
ErrTooManySubmittingTx, nonce, pendingNonce, txopts.maxSubmittingTxNum)
}
return nonce, pendingNonce, nil
}

// determineGas sets the gas price and gas limit on the signer
func (t *Transactor) determineGas(method TxMethod, signer *bind.TransactOpts, txopts txOptions, client *ethclient.Client) error {
// 1. Determine gas price
// Only accept legacy flags or EIP-1559 flags, not both
hasLegacyFlags := txopts.forceGasGwei != nil || txopts.minGasGwei > 0 || txopts.maxGasGwei > 0 || txopts.addGasGwei > 0
has1559Flags := txopts.maxFeePerGasGwei > 0 || txopts.maxPriorityFeePerGasGwei > 0
has1559Flags := txopts.maxFeePerGasGwei > 0 || txopts.maxPriorityFeePerGasGwei > 0 ||
txopts.addPriorityFeePerGasGwei > 0 || txopts.addPriorityFeeRatio > 0
if hasLegacyFlags && has1559Flags {
return ErrConflictingGasFlags
}
Expand All @@ -240,13 +254,13 @@ func (t *Transactor) determineGas(method TxMethod, signer *bind.TransactOpts, tx
return fmt.Errorf("failed to call HeaderByNumber: %w", err)
}
if head.BaseFee != nil && !hasLegacyFlags {
err = determine1559GasPrice(ctx, signer, txopts, client, head)
err = determine1559GasPrice(signer, txopts, client)
if err != nil {
return fmt.Errorf("failed to determine EIP-1559 gas price: %w", err)
}
} else {
// Legacy pricing
err = determineLegacyGasPrice(ctx, signer, txopts, client)
err = determineLegacyGasPrice(signer, txopts, client)
if err != nil {
return fmt.Errorf("failed to determine legacy gas price: %w", err)
}
Expand Down Expand Up @@ -275,20 +289,29 @@ func (t *Transactor) determineGas(method TxMethod, signer *bind.TransactOpts, tx
}

// determine1559GasPrice sets the gas price on the signer based on the EIP-1559 fee model
func determine1559GasPrice(
ctx context.Context, signer *bind.TransactOpts, txopts txOptions, client *ethclient.Client, head *types.Header) error {
if txopts.maxPriorityFeePerGasGwei > 0 {
signer.GasTipCap = new(big.Int).SetUint64(uint64(txopts.maxPriorityFeePerGasGwei * 1e9))
}
func determine1559GasPrice(signer *bind.TransactOpts, txopts txOptions, client *ethclient.Client) error {
if txopts.maxFeePerGasGwei > 0 {
signer.GasFeeCap = new(big.Int).SetUint64(txopts.maxFeePerGasGwei * 1e9)
}
if txopts.maxPriorityFeePerGasGwei > 0 {
signer.GasTipCap = new(big.Int).SetUint64(uint64(txopts.maxPriorityFeePerGasGwei * 1e9))
} else if txopts.addPriorityFeePerGasGwei > 0 || txopts.addPriorityFeeRatio > 0 {
suggestedGasTipCap, err := client.SuggestGasTipCap(context.Background())
if err != nil {
return fmt.Errorf("failed to call SuggestGasTipCap: %w", err)
}
if txopts.addPriorityFeePerGasGwei > 0 {
signer.GasTipCap = new(big.Int).SetUint64(uint64(txopts.addPriorityFeePerGasGwei*1e9) + suggestedGasTipCap.Uint64())
} else if txopts.addPriorityFeeRatio > 0 {
signer.GasTipCap = new(big.Int).SetUint64(uint64(float64(suggestedGasTipCap.Uint64()) * (1 + txopts.addGasEstimateRatio)))
}
}
return nil
}

// determineLegacyGasPrice sets the gas price on the signer based on the legacy fee model
func determineLegacyGasPrice(
ctx context.Context, signer *bind.TransactOpts, txopts txOptions, client *ethclient.Client) error {
signer *bind.TransactOpts, txopts txOptions, client *ethclient.Client) error {
if txopts.forceGasGwei != nil {
signer.GasPrice = new(big.Int).SetUint64(*txopts.forceGasGwei * 1e9)
return nil
Expand Down Expand Up @@ -326,13 +349,13 @@ func printGasGwei(signer *bind.TransactOpts) string {
}
res := ""
if signer.GasPrice != nil && signer.GasPrice.Sign() > 0 {
res += fmt.Sprintf("price %d ", signer.GasPrice.Uint64()/1e9)
res += fmt.Sprintf("price %.2f ", float64(signer.GasPrice.Uint64())/1e9)
}
if signer.GasFeeCap != nil && signer.GasFeeCap.Sign() > 0 {
res += fmt.Sprintf("feecap %d ", signer.GasFeeCap.Uint64()/1e9)
}
if signer.GasTipCap != nil && signer.GasTipCap.Sign() > 0 {
res += fmt.Sprintf("tipcap %d ", signer.GasTipCap.Uint64()/1e9)
res += fmt.Sprintf("tipcap %.2f ", float64(signer.GasTipCap.Uint64())/1e9)
}
return strings.TrimSpace(res)
}
Expand Down

0 comments on commit c153e70

Please sign in to comment.