Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions book/src/libs/seth.md
Original file line number Diff line number Diff line change
Expand Up @@ -568,13 +568,17 @@ For real networks, the estimation process differs for legacy transactions and th

##### Legacy Transactions

Unless priority is set to `auto`, when we will defer to what the RPC node suggests, following logic is used:

1. **Initial Price**: Query the network node for the current suggested gas price.
2. **Priority Adjustment**: Modify the initial price based on `gas_price_estimation_tx_priority`. Higher priority increases the price to ensure faster inclusion in a block.
3. **Congestion Analysis**: Examine the last X blocks (as specified by `gas_price_estimation_blocks`) to determine network congestion, calculating the usage rate of gas in each block and giving recent blocks more weight. Disabled if `gas_price_estimation_blocks` equals `0`.
4. **Buffering**: Add a buffer to the adjusted gas price to increase transaction reliability during high congestion.

##### EIP-1559 Transactions

Unless priority is set to `auto`, when we will defer to what the RPC node suggests, following logic is used:

1. **Tip Fee Query**: Ask the node for the current recommended tip fee.
2. **Fee History Analysis**: Gather the base fee and tip history from recent blocks to establish a fee baseline.
3. **Fee Selection**: Use the greatest of the node's suggested tip or the historical average tip for upcoming calculations.
Expand Down
36 changes: 28 additions & 8 deletions seth/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,10 @@ func NewClientRaw(
if cfg.ReadOnly {
return nil, errors.New(ErrReadOnlyEphemeralKeys)
}
gasPrice, err := c.GetSuggestedLegacyFees(context.Background(), Priority_Standard)
ctx, cancel := context.WithTimeout(context.Background(), c.Cfg.Network.TxnTimeout.D)
defer cancel()

gasPrice, err := c.GetSuggestedLegacyFees(ctx, c.Cfg.Network.GasPriceEstimationTxPriority)
if err != nil {
gasPrice = big.NewInt(c.Cfg.Network.GasPrice)
}
Expand All @@ -331,8 +334,6 @@ func NewClientRaw(
}
L.Warn().Msg("Ephemeral mode, all funds will be lost!")

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
eg, egCtx := errgroup.WithContext(ctx)
// root key is element 0 in ephemeral
for _, addr := range c.Addresses[1:] {
Expand Down Expand Up @@ -408,7 +409,7 @@ func (m *Client) checkRPCHealth() error {
ctx, cancel := context.WithTimeout(context.Background(), m.Cfg.Network.TxnTimeout.Duration())
defer cancel()

gasPrice, err := m.GetSuggestedLegacyFees(context.Background(), Priority_Standard)
gasPrice, err := m.GetSuggestedLegacyFees(context.Background(), m.Cfg.Network.GasPriceEstimationTxPriority)
if err != nil {
gasPrice = big.NewInt(m.Cfg.Network.GasPrice)
}
Expand Down Expand Up @@ -772,8 +773,11 @@ func (m *Client) AnySyncedKey() int {
}

type GasEstimations struct {
GasPrice *big.Int
// GasPrice for legacy transactions. If nil, RPC node will auto-estimate.
GasPrice *big.Int
// GasTipCap for EIP-1559 transactions. If nil, RPC node will auto-estimate.
GasTipCap *big.Int
// GasFeeCap for EIP-1559 transactions. If nil, RPC node will auto-estimate.
GasFeeCap *big.Int
}

Expand Down Expand Up @@ -825,9 +829,8 @@ func (m *Client) getProposedTransactionOptions(keyNum int) (*bind.TransactOpts,
pending nonce for key %d is higher than last nonce, there are %d pending transactions.

This issue is caused by one of two things:
1. You are using the same keyNum in multiple goroutines, which is not supported. Each goroutine should use an unique keyNum.
2. You have stuck transaction(s). Speed them up by sending replacement transactions with higher gas price before continuing, otherwise future transactions most probably will also get stuck.
`
1. You are using the same keyNum in multiple goroutines, which is not supported. Each goroutine should use an unique keyNum
2. You have stuck transaction(s). Speed them up by sending replacement transactions with higher gas price before continuing, otherwise future transactions most probably will also get stuck`
err := fmt.Errorf(errMsg, keyNum, nonceStatus.PendingNonce-nonceStatus.LastNonce)
m.Errors = append(m.Errors, err)
// can't return nil, otherwise RPC wrapper will panic, and we might lose funds on testnets/mainnets, that's why
Expand Down Expand Up @@ -890,6 +893,15 @@ func (m *Client) NewDefaultGasEstimationRequest() GasEstimationRequest {
func (m *Client) CalculateGasEstimations(request GasEstimationRequest) GasEstimations {
estimations := GasEstimations{}

if request.Priority == Priority_Auto {
// Return empty estimations with nil gas prices and fees.
// This signals to the RPC node to auto-estimate gas prices.
// The nil values will be passed to bind.TransactOpts, which treats
// nil gas fields as a request for automatic estimation.
L.Debug().Msg("Auto priority selected, skipping gas estimations")
return estimations
}

if m.Cfg.IsSimulatedNetwork() || !request.GasEstimationEnabled {
estimations.GasPrice = big.NewInt(request.FallbackGasPrice)
estimations.GasFeeCap = big.NewInt(request.FallbackGasFeeCap)
Expand Down Expand Up @@ -978,7 +990,15 @@ func (m *Client) configureTransactionOpts(
opts.GasPrice = nil
opts.GasTipCap = estimations.GasTipCap
opts.GasFeeCap = estimations.GasFeeCap

// Log when using auto-estimated gas (all nil values)
if opts.GasTipCap == nil && opts.GasFeeCap == nil {
L.Debug().Msg("Using RPC node's automatic gas estimation (EIP-1559)")
}
} else if opts.GasPrice == nil {
L.Debug().Msg("Using RPC node's automatic gas estimation (Legacy)")
}

for _, f := range o {
f(opts)
}
Expand Down
54 changes: 54 additions & 0 deletions seth/client_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,60 @@ func TestConfig_SimulatedBackend(t *testing.T) {
require.IsType(t, backend.Client(), client.Client, "expected simulated client")
}

func TestConfig_SimulatedBackend_Priority_Auto(t *testing.T) {
backend, cancelFn := StartSimulatedBackend([]common.Address{common.HexToAddress("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")})
t.Cleanup(func() {
cancelFn()
})

builder := seth.NewClientBuilder()

client, err := builder.
WithNetworkName("simulated").
WithEthClient(backend.Client()).
WithPrivateKeys([]string{"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"}).
WithGasPriceEstimations(true, 10, seth.Priority_Auto, 1).
Build()

require.NoError(t, err, "failed to build client")
require.Equal(t, 1, len(client.PrivateKeys), "expected 1 private key")
require.Equal(t, 1, len(client.Addresses), "expected 1 addresse")
require.IsType(t, backend.Client(), client.Client, "expected simulated client")

linkAbi, err := link_token.LinkTokenMetaData.GetAbi()
require.NoError(t, err, "failed to get LINK ABI")

_, err = client.DeployContract(client.NewTXOpts(), "LinkToken", *linkAbi, common.FromHex(link_token.LinkTokenMetaData.Bin))
require.NoError(t, err, "failed to deploy LINK contract")
}

func TestConfig_SimulatedBackend_No_Historical_Fees(t *testing.T) {
backend, cancelFn := StartSimulatedBackend([]common.Address{common.HexToAddress("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")})
t.Cleanup(func() {
cancelFn()
})

builder := seth.NewClientBuilder()

client, err := builder.
WithNetworkName("simulated").
WithEthClient(backend.Client()).
WithPrivateKeys([]string{"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"}).
WithGasPriceEstimations(true, 0, seth.Priority_Standard, 1).
Build()

require.NoError(t, err, "failed to build client")
require.Equal(t, 1, len(client.PrivateKeys), "expected 1 private key")
require.Equal(t, 1, len(client.Addresses), "expected 1 addresse")
require.IsType(t, backend.Client(), client.Client, "expected simulated client")

linkAbi, err := link_token.LinkTokenMetaData.GetAbi()
require.NoError(t, err, "failed to get LINK ABI")

_, err = client.DeployContract(client.NewTXOpts(), "LinkToken", *linkAbi, common.FromHex(link_token.LinkTokenMetaData.Bin))
require.NoError(t, err, "failed to deploy LINK contract")
}

func TestConfig_SimulatedBackend_ContractDeploymentHooks(t *testing.T) {
backend, cancelFn := StartSimulatedBackend([]common.Address{common.HexToAddress("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")})
t.Cleanup(func() {
Expand Down
22 changes: 11 additions & 11 deletions seth/cmd/seth.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,23 +118,23 @@ func RunCLI(args []string) error {
ge := seth.NewGasEstimator(C)
blocks := cCtx.Uint64("blocks")
tipPerc := cCtx.Float64("tipPercentile")
stats, err := ge.Stats(blocks, tipPerc)
stats, err := ge.Stats(cCtx.Context, blocks, tipPerc)
if err != nil {
return err
}
seth.L.Info().
Interface("Max", stats.GasPrice.Max).
Interface("99", stats.GasPrice.Perc99).
Interface("75", stats.GasPrice.Perc75).
Interface("50", stats.GasPrice.Perc50).
Interface("25", stats.GasPrice.Perc25).
Interface("Max", stats.BaseFeePerc.Max).
Interface("99", stats.BaseFeePerc.Perc99).
Interface("75", stats.BaseFeePerc.Perc75).
Interface("50", stats.BaseFeePerc.Perc50).
Interface("25", stats.BaseFeePerc.Perc25).
Msg("Base fee (Wei)")
seth.L.Info().
Interface("Max", stats.TipCap.Max).
Interface("99", stats.TipCap.Perc99).
Interface("75", stats.TipCap.Perc75).
Interface("50", stats.TipCap.Perc50).
Interface("25", stats.TipCap.Perc25).
Interface("Max", stats.TipCapPerc.Max).
Interface("99", stats.TipCapPerc.Perc99).
Interface("75", stats.TipCapPerc.Perc75).
Interface("50", stats.TipCapPerc.Perc50).
Interface("25", stats.TipCapPerc.Perc25).
Msg("Priority fee (Wei)")
seth.L.Info().
Interface("GasPrice", stats.SuggestedGasPrice).
Expand Down
7 changes: 6 additions & 1 deletion seth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,15 @@ func (c *Config) Validate() error {
case Priority_Fast:
case Priority_Standard:
case Priority_Slow:
case Priority_Auto:
default:
return errors.New("when automating gas estimation is enabled priority must be fast, standard or slow. fix it or disable gas estimation")
return errors.New("when automating gas estimation is enabled priority must be auto, fast, standard or slow. fix it or disable gas estimation")
}

if c.GasBump != nil && c.GasBump.Retries > 0 &&
c.Network.GasPriceEstimationTxPriority == Priority_Auto {
return errors.New("gas bumping is not compatible with auto priority gas estimation")
}
}

if c.Network.GasLimit != 0 {
Expand Down
83 changes: 55 additions & 28 deletions seth/gas.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package seth

import (
"context"
"fmt"
"math/big"

"github.com/montanaflynn/stats"
"github.com/pkg/errors"
)

// GasEstimator estimates gas prices
Expand All @@ -21,22 +23,32 @@ func NewGasEstimator(c *Client) *GasEstimator {

// Stats calculates gas price and tip cap suggestions based on historical fee data over a specified number of blocks.
// It computes quantiles for base fees and tip caps and provides suggested gas price and tip cap values.
func (m *GasEstimator) Stats(fromNumber uint64, priorityPerc float64) (GasSuggestions, error) {
bn, err := m.Client.Client.BlockNumber(context.Background())
func (m *GasEstimator) Stats(ctx context.Context, blockCount uint64, priorityPerc float64) (GasSuggestions, error) {
estimations := GasSuggestions{}

if blockCount == 0 {
return estimations, errors.New("block count must be greater than zero")
}

currentBlock, err := m.Client.Client.BlockNumber(ctx)
if err != nil {
return GasSuggestions{}, err
return GasSuggestions{}, fmt.Errorf("failed to get current block number: %w", err)
}
if fromNumber == 0 {
if bn > 100 {
fromNumber = bn - 100
} else {
fromNumber = 1
}
if currentBlock == 0 {
return GasSuggestions{}, errors.New("current block number is zero. No fee history available")
}
hist, err := m.Client.Client.FeeHistory(context.Background(), fromNumber, big.NewInt(int64(bn)), []float64{priorityPerc})
if blockCount >= currentBlock {
blockCount = currentBlock - 1
}

hist, err := m.Client.Client.FeeHistory(ctx, blockCount, big.NewInt(int64(currentBlock)), []float64{priorityPerc})
if err != nil {
return GasSuggestions{}, err
return GasSuggestions{}, fmt.Errorf("failed to get fee history: %w", err)
}
L.Trace().
Interface("History", hist).
Msg("Fee history")

baseFees := make([]float64, 0)
for _, bf := range hist.BaseFee {
if bf == nil {
Expand All @@ -48,8 +60,14 @@ func (m *GasEstimator) Stats(fromNumber uint64, priorityPerc float64) (GasSugges
}
gasPercs, err := quantilesFromFloatArray(baseFees)
if err != nil {
return GasSuggestions{}, err
return GasSuggestions{}, fmt.Errorf("failed to calculate quantiles from fee history for base fee: %w", err)
}
estimations.BaseFeePerc = gasPercs

L.Trace().
Interface("Gas percentiles ", gasPercs).
Msg("Base fees")

tips := make([]float64, 0)
for _, bf := range hist.Reward {
if len(bf) == 0 {
Expand All @@ -64,25 +82,33 @@ func (m *GasEstimator) Stats(fromNumber uint64, priorityPerc float64) (GasSugges
}
tipPercs, err := quantilesFromFloatArray(tips)
if err != nil {
return GasSuggestions{}, err
return GasSuggestions{}, fmt.Errorf("failed to calculate quantiles from fee history for tip cap: %w", err)
}
suggestedGasPrice, err := m.Client.Client.SuggestGasPrice(context.Background())
estimations.TipCapPerc = tipPercs
L.Trace().
Interface("Gas percentiles ", tipPercs).
Msg("Tip caps")

suggestedGasPrice, err := m.Client.Client.SuggestGasPrice(ctx)
if err != nil {
return GasSuggestions{}, err
return GasSuggestions{}, fmt.Errorf("failed to get suggested gas price: %w", err)
}
suggestedGasTipCap, err := m.Client.Client.SuggestGasTipCap(context.Background())
estimations.SuggestedGasPrice = suggestedGasPrice

suggestedGasTipCap, err := m.Client.Client.SuggestGasTipCap(ctx)
if err != nil {
return GasSuggestions{}, err
return GasSuggestions{}, fmt.Errorf("failed to get suggested gas tip cap: %w", err)
}
L.Trace().
Interface("History", hist).
Msg("Fee history")
return GasSuggestions{
GasPrice: gasPercs,
TipCap: tipPercs,
SuggestedGasPrice: suggestedGasPrice,
SuggestedGasTipCap: suggestedGasTipCap,
}, nil

estimations.SuggestedGasTipCap = suggestedGasTipCap

header, err := m.Client.Client.HeaderByNumber(ctx, nil)
if err != nil {
return GasSuggestions{}, fmt.Errorf("failed to get latest block header: %w", err)
}
estimations.LastBaseFee = header.BaseFee

return estimations, nil
}

// GasPercentiles contains gas percentiles
Expand All @@ -95,8 +121,9 @@ type GasPercentiles struct {
}

type GasSuggestions struct {
GasPrice *GasPercentiles
TipCap *GasPercentiles
BaseFeePerc *GasPercentiles
TipCapPerc *GasPercentiles
LastBaseFee *big.Int
SuggestedGasPrice *big.Int
SuggestedGasTipCap *big.Int
}
Expand Down
Loading
Loading