Skip to content
Merged
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
7 changes: 7 additions & 0 deletions CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ DualBroadcast = false # Example
ReadRequestsToMultipleNodes = false # Example
Bundles = false # Example
FastlaneAuctionRequestTimeout = '5s' # Example
FeeBoost = false # Example
```


Expand Down Expand Up @@ -438,6 +439,12 @@ FastlaneAuctionRequestTimeout = '5s' # Example
```
FastlaneAuctionRequestTimeout configures the HTTP request timeout for Fastlane Atlas auction requests. Defaults to 5s if not set.

### FeeBoost
```toml
FeeBoost = false # Example
```
FeeBoost enables using GetMaxFee instead of GetFee for all TxM v2 transaction attempts. This makes SVR transactions as aggressive as possible with gas pricing. Only active when DualBroadcast is also enabled.

## BalanceMonitor
```toml
[BalanceMonitor]
Expand Down
4 changes: 4 additions & 0 deletions pkg/config/chain_scoped_transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ func (t *transactionManagerV2Config) FastlaneAuctionRequestTimeout() *time.Durat
return &d
}

func (t *transactionManagerV2Config) FeeBoost() bool {
return t.c.FeeBoost != nil && *t.c.FeeBoost
}

func (t *transactionsConfig) AutoPurge() AutoPurgeConfig {
return &autoPurgeConfig{c: t.c.AutoPurge}
}
Expand Down
1 change: 1 addition & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ type TransactionManagerV2 interface {
ReadRequestsToMultipleNodes() *bool
Bundles() *bool
FastlaneAuctionRequestTimeout() *time.Duration
FeeBoost() bool
}

type GasEstimator interface {
Expand Down
4 changes: 4 additions & 0 deletions pkg/config/toml/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ type TransactionManagerV2Config struct {
ReadRequestsToMultipleNodes *bool `toml:",omitempty"`
Bundles *bool `toml:",omitempty"`
FastlaneAuctionRequestTimeout *commonconfig.Duration `toml:",omitempty"`
FeeBoost *bool `toml:",omitempty"`
}

func (t *TransactionManagerV2Config) setFrom(f *TransactionManagerV2Config) {
Expand All @@ -616,6 +617,9 @@ func (t *TransactionManagerV2Config) setFrom(f *TransactionManagerV2Config) {
if v := f.FastlaneAuctionRequestTimeout; v != nil {
t.FastlaneAuctionRequestTimeout = f.FastlaneAuctionRequestTimeout
}
if v := f.FeeBoost; v != nil {
t.FeeBoost = f.FeeBoost
}
}

func (t *TransactionManagerV2Config) ValidateConfig() (err error) {
Expand Down
3 changes: 3 additions & 0 deletions pkg/config/toml/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func TestDefaults_fieldsNotNil(t *testing.T) {
unknown.Transactions.TransactionManagerV2.ReadRequestsToMultipleNodes = ptr(false)
unknown.Transactions.TransactionManagerV2.Bundles = ptr(false)
unknown.Transactions.TransactionManagerV2.FastlaneAuctionRequestTimeout = new(config.Duration)
unknown.Transactions.TransactionManagerV2.FeeBoost = ptr(false)
unknown.Transactions.AutoPurge.Threshold = ptr(uint32(0))
unknown.Transactions.AutoPurge.MinAttempts = ptr(uint32(0))
unknown.Transactions.AutoPurge.DetectionApiUrl = new(config.URL)
Expand Down Expand Up @@ -165,6 +166,7 @@ func TestDocs(t *testing.T) {
docDefaults.Transactions.TransactionManagerV2.ReadRequestsToMultipleNodes = nil
docDefaults.Transactions.TransactionManagerV2.Bundles = nil
docDefaults.Transactions.TransactionManagerV2.FastlaneAuctionRequestTimeout = nil
docDefaults.Transactions.TransactionManagerV2.FeeBoost = nil

// Fallback DA oracle is not set
docDefaults.GasEstimator.DAOracle = DAOracle{}
Expand Down Expand Up @@ -294,6 +296,7 @@ var fullConfig = EVMConfig{
BlockTime: config.MustNewDuration(42 * time.Second),
CustomURL: config.MustParseURL("http://txs.org"),
FastlaneAuctionRequestTimeout: config.MustNewDuration(15 * time.Second),
FeeBoost: ptr(true),
},
},

Expand Down
2 changes: 2 additions & 0 deletions pkg/config/toml/docs.toml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ ReadRequestsToMultipleNodes = false # Example
Bundles = false # Example
# FastlaneAuctionRequestTimeout configures the HTTP request timeout for Fastlane Atlas auction requests. Defaults to 5s if not set.
FastlaneAuctionRequestTimeout = '5s' # Example
# FeeBoost enables using GetMaxFee instead of GetFee for all TxM v2 transaction attempts. This makes SVR transactions as aggressive as possible with gas pricing. Only active when DualBroadcast is also enabled.
FeeBoost = false # Example
Comment thread
amit-momin marked this conversation as resolved.

[BalanceMonitor]
# Enabled balance monitoring for all keys.
Expand Down
1 change: 1 addition & 0 deletions pkg/config/toml/testdata/config-full.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ DualBroadcast = true
ReadRequestsToMultipleNodes = false
Bundles = false
FastlaneAuctionRequestTimeout = '15s'
FeeBoost = true

[BalanceMonitor]
Enabled = true
Expand Down
23 changes: 17 additions & 6 deletions pkg/txm/attempt_builder.go
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test case in attempt_builder_test.go

Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,34 @@ type attemptBuilder struct {
priceMaxKey func(common.Address) *assets.Wei
keystore keys.TxSigner
emptyTxLimitDefault uint64
feeBoost bool
}

func NewAttemptBuilder(priceMaxKey func(common.Address) *assets.Wei, estimator gas.EvmFeeEstimator, keystore keys.TxSigner, emptyTxLimitDefault uint64) *attemptBuilder {
func NewAttemptBuilder(priceMaxKey func(common.Address) *assets.Wei, estimator gas.EvmFeeEstimator, keystore keys.TxSigner, emptyTxLimitDefault uint64, feeBoost bool) *attemptBuilder {
return &attemptBuilder{
priceMaxKey: priceMaxKey,
EvmFeeEstimator: estimator,
keystore: keystore,
emptyTxLimitDefault: emptyTxLimitDefault,
feeBoost: feeBoost,
}
}

func (a *attemptBuilder) NewAttempt(ctx context.Context, lggr logger.Logger, tx *types.Transaction, dynamic bool) (*types.Attempt, error) {
var fee gas.EvmFee
var estimatedGasLimit uint64
var err error
if tx.IsPurgeable {
fee, estimatedGasLimit, err = a.EvmFeeEstimator.GetMaxFee(ctx, tx.Data, a.emptyTxLimitDefault, a.priceMaxKey(tx.FromAddress), &tx.FromAddress, &tx.ToAddress)
if tx.IsPurgeable || a.feeBoost {
gasLimit := tx.SpecifiedGasLimit
if tx.IsPurgeable {
gasLimit = a.emptyTxLimitDefault
}
fee, estimatedGasLimit, err = a.GetMaxFee(ctx, tx.Data, gasLimit, a.priceMaxKey(tx.FromAddress), &tx.FromAddress, &tx.ToAddress)
if err != nil {
return nil, err
}
} else {
fee, estimatedGasLimit, err = a.EvmFeeEstimator.GetFee(ctx, tx.Data, tx.SpecifiedGasLimit, a.priceMaxKey(tx.FromAddress), &tx.FromAddress, &tx.ToAddress)
fee, estimatedGasLimit, err = a.GetFee(ctx, tx.Data, tx.SpecifiedGasLimit, a.priceMaxKey(tx.FromAddress), &tx.FromAddress, &tx.ToAddress)
if err != nil {
return nil, err
}
Expand All @@ -69,9 +75,14 @@ func (a *attemptBuilder) NewBumpAttempt(ctx context.Context, lggr logger.Logger,
}

func (a *attemptBuilder) NewAgnosticBumpAttempt(ctx context.Context, lggr logger.Logger, tx *types.Transaction, dynamic bool) (attempt *types.Attempt, err error) {
// if the transaction is purgeable, NewAttempt will return the max fee instantly, so there is no need to bump
// if the transaction is purgeable or feeBoost is enabled, NewAttempt will return the max fee instantly, so there is no need to bump
attempt, err = a.NewAttempt(ctx, lggr, tx, dynamic)
if tx.IsPurgeable || err != nil {

if err != nil {
return
}

if tx.IsPurgeable || a.feeBoost {
return
}
Comment on lines 77 to 87
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new feeBoost behavior (always using GetMaxFee and skipping bumping) isn’t covered by tests. Add a unit test that constructs an attemptBuilder with feeBoost=true and asserts (1) GetMaxFee is called with the transaction’s SpecifiedGasLimit (or 0) for non-purgeable txs, and (2) NewAgnosticBumpAttempt returns after the initial attempt without calling BumpFee.

Copilot uses AI. Check for mistakes.

Expand Down
42 changes: 33 additions & 9 deletions pkg/txm/attempt_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
)

func TestAttemptBuilder_newLegacyAttempt(t *testing.T) {
ab := NewAttemptBuilder(nil, nil, keystest.TxSigner(nil), 100)
ab := NewAttemptBuilder(nil, nil, keystest.TxSigner(nil), 100, false)
address := testutils.NewAddress()
lggr := logger.Test(t)
var gasLimit uint64 = 100
Expand Down Expand Up @@ -57,7 +57,7 @@ func TestAttemptBuilder_newLegacyAttempt(t *testing.T) {
}

func TestAttemptBuilder_newDynamicFeeAttempt(t *testing.T) {
ab := NewAttemptBuilder(nil, nil, keystest.TxSigner(nil), 100)
ab := NewAttemptBuilder(nil, nil, keystest.TxSigner(nil), 100, false)
address := testutils.NewAddress()

lggr := logger.Test(t)
Expand Down Expand Up @@ -100,7 +100,7 @@ func TestAttemptBuilder_NewAttempt(t *testing.T) {
var nonce uint64 = 1
var specifiedGasLimit uint64 = 200
var emptyGasLimit uint64 = 100
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), emptyGasLimit)
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), emptyGasLimit, false)
address := testutils.NewAddress()
lggr := logger.Test(t)

Expand Down Expand Up @@ -160,6 +160,30 @@ func TestAttemptBuilder_NewAttempt(t *testing.T) {
assert.Contains(t, err.Error(), "estimator error")
mockEstimator.AssertExpectations(t)
})

t.Run("uses SpecifiedGasLimit when feeBoost is enabled and tx is not purgeable", func(t *testing.T) {
boostEstimator := mocks.NewEvmFeeEstimator(t)
boostAb := NewAttemptBuilder(priceMaxKey, boostEstimator, keystest.TxSigner(nil), emptyGasLimit, true)
tx := &types.Transaction{ID: 10, FromAddress: address, Nonce: &nonce, SpecifiedGasLimit: specifiedGasLimit}
boostEstimator.On("GetMaxFee", mock.Anything, mock.Anything, specifiedGasLimit, mock.Anything, mock.Anything, mock.Anything).
Return(gas.EvmFee{GasPrice: assets.NewWeiI(100)}, specifiedGasLimit, nil).Once()
a, err := boostAb.NewAttempt(t.Context(), lggr, tx, false)
require.NoError(t, err)
assert.Equal(t, specifiedGasLimit, a.GasLimit)
boostEstimator.AssertExpectations(t)
})

t.Run("uses emptyTxLimitDefault when feeBoost is enabled and tx is purgeable", func(t *testing.T) {
boostEstimator := mocks.NewEvmFeeEstimator(t)
boostAb := NewAttemptBuilder(priceMaxKey, boostEstimator, keystest.TxSigner(nil), emptyGasLimit, true)
tx := &types.Transaction{ID: 10, FromAddress: address, IsPurgeable: true, Nonce: &nonce, SpecifiedGasLimit: specifiedGasLimit}
boostEstimator.On("GetMaxFee", mock.Anything, mock.Anything, emptyGasLimit, mock.Anything, mock.Anything, mock.Anything).
Return(gas.EvmFee{GasPrice: assets.NewWeiI(100)}, emptyGasLimit, nil).Once()
a, err := boostAb.NewAttempt(t.Context(), lggr, tx, false)
require.NoError(t, err)
assert.Equal(t, emptyGasLimit, a.GasLimit)
boostEstimator.AssertExpectations(t)
})
}

func TestAttemptBuilder_NewAgnosticBumpAttempt(t *testing.T) {
Expand All @@ -172,7 +196,7 @@ func TestAttemptBuilder_NewAgnosticBumpAttempt(t *testing.T) {

t.Run("returns original attempt when AttemptCount is 0", func(t *testing.T) {
mockEstimator := mocks.NewEvmFeeEstimator(t)
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100)
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100, false)

tx := &types.Transaction{
ID: 10,
Expand All @@ -196,7 +220,7 @@ func TestAttemptBuilder_NewAgnosticBumpAttempt(t *testing.T) {

t.Run("bumps once when AttemptCount is 1", func(t *testing.T) {
mockEstimator := mocks.NewEvmFeeEstimator(t)
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100)
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100, false)

tx := &types.Transaction{
ID: 10,
Expand All @@ -222,7 +246,7 @@ func TestAttemptBuilder_NewAgnosticBumpAttempt(t *testing.T) {

t.Run("bumps N times when AttemptCount is N", func(t *testing.T) {
mockEstimator := mocks.NewEvmFeeEstimator(t)
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100)
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100, false)

tx := &types.Transaction{
ID: 10,
Expand Down Expand Up @@ -253,7 +277,7 @@ func TestAttemptBuilder_NewAgnosticBumpAttempt(t *testing.T) {

t.Run("returns last valid attempt when BumpFee fails", func(t *testing.T) {
mockEstimator := mocks.NewEvmFeeEstimator(t)
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100)
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100, false)

tx := &types.Transaction{
ID: 10,
Expand Down Expand Up @@ -282,7 +306,7 @@ func TestAttemptBuilder_NewAgnosticBumpAttempt(t *testing.T) {

t.Run("caps bumps at maxBumpThreshold", func(t *testing.T) {
mockEstimator := mocks.NewEvmFeeEstimator(t)
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100)
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100, false)

tx := &types.Transaction{
ID: 10,
Expand All @@ -307,7 +331,7 @@ func TestAttemptBuilder_NewAgnosticBumpAttempt(t *testing.T) {

t.Run("returns max percentile attempt when transaction is purgeable", func(t *testing.T) {
mockEstimator := mocks.NewEvmFeeEstimator(t)
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100)
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100, false)

tx := &types.Transaction{
ID: 10,
Expand Down
4 changes: 2 additions & 2 deletions pkg/txm/integration-tests/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func setupTestnetTXM(
require.NoError(t, err, "failed to add private key to keystore")

// AttemptBuilder
ab := txm.NewAttemptBuilder(configs.PriceMaxKey, estimator, keystore, configs.LimitTransfer())
ab := txm.NewAttemptBuilder(configs.PriceMaxKey, estimator, keystore, configs.LimitTransfer(), false)

// InMemory storage
store := storage.NewInMemoryStoreManager(lggr, chainID)
Expand Down Expand Up @@ -158,7 +158,7 @@ func setupDevnetTXM(
require.NoError(t, keystore.Add(privateKeyHex), "failed to add private key to keystore")

// AttemptBuilder
ab := txm.NewAttemptBuilder(configs.PriceMaxKey, estimator, keystore, configs.LimitDefault())
ab := txm.NewAttemptBuilder(configs.PriceMaxKey, estimator, keystore, configs.LimitDefault(), false)

// InMemory storage
store := storage.NewInMemoryStoreManager(lggr, chainID)
Expand Down
4 changes: 2 additions & 2 deletions pkg/txm/txm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ func TestFlow_ResendTransaction(t *testing.T) {
mockEstimator := mocks.NewEvmFeeEstimator(t)
defaultGasLimit := uint64(100000)
keystore := &keystest.FakeChainStore{}
attemptBuilder := txm.NewAttemptBuilder(func(address common.Address) *assets.Wei { return assets.NewWeiI(1) }, mockEstimator, keystore, 22000)
attemptBuilder := txm.NewAttemptBuilder(func(address common.Address) *assets.Wei { return assets.NewWeiI(1) }, mockEstimator, keystore, 22000, false)
stuckTxDetector := txm.NewStuckTxDetector(logger.Test(t), "", txm.StuckTxDetectorConfig{BlockTime: config.BlockTime, StuckTxBlockThreshold: uint32(config.RetryBlockThreshold + 1)})
tm := txm.NewTxm(logger.Test(t), testutils.FixtureChainID, client, attemptBuilder, txStoreManager, stuckTxDetector, config, keystore, nil, txm.NewNoopTxmMetrics())
initialNonce := uint64(0)
Expand Down Expand Up @@ -501,7 +501,7 @@ func TestFlow_ErrorHandler(t *testing.T) {
config := txm.Config{EIP1559: true, EmptyTxLimitDefault: 22000, RetryBlockThreshold: 0, BlockTime: 2 * time.Second}
mockEstimator := mocks.NewEvmFeeEstimator(t)
keystore := &keystest.FakeChainStore{}
attemptBuilder := txm.NewAttemptBuilder(func(address common.Address) *assets.Wei { return assets.NewWeiI(1) }, mockEstimator, keystore, 22000)
attemptBuilder := txm.NewAttemptBuilder(func(address common.Address) *assets.Wei { return assets.NewWeiI(1) }, mockEstimator, keystore, 22000, false)
stuckTxDetector := txm.NewStuckTxDetector(lggr, "", txm.StuckTxDetectorConfig{BlockTime: config.BlockTime, StuckTxBlockThreshold: uint32(config.RetryBlockThreshold + 1)})
errorHandler := dualbroadcast.NewErrorHandler()
tm := txm.NewTxm(lggr, testutils.FixtureChainID, client, attemptBuilder, txStoreManager, stuckTxDetector, config, keystore, errorHandler, txm.NewNoopTxmMetrics())
Expand Down
3 changes: 2 additions & 1 deletion pkg/txmgr/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ func NewTxmV2(
stuckTxDetector = txm.NewStuckTxDetector(lggr, chainConfig.ChainType(), stuckTxDetectorConfig)
}

attemptBuilder := txm.NewAttemptBuilder(fCfg.PriceMaxKey, estimator, keyStore, gasEstimatorConfig.LimitTransfer())
feeBoost := txmV2Config.DualBroadcast() != nil && *txmV2Config.DualBroadcast() && txmV2Config.FeeBoost()
attemptBuilder := txm.NewAttemptBuilder(fCfg.PriceMaxKey, estimator, keyStore, gasEstimatorConfig.LimitTransfer(), feeBoost)
inMemoryStoreManager := storage.NewInMemoryStoreManager(lggr, chainID)
readRequestsToMultipleNodes := false
if txmV2Config.ReadRequestsToMultipleNodes() != nil && *txmV2Config.ReadRequestsToMultipleNodes() {
Expand Down
Loading