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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ require (
github.com/smartcontractkit/chainlink-common/keystore v0.1.0
github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251022073203-7d8ae8cf67c1
github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20250818175541-3389ac08a563
github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20251210101658-1c5c8e4c4f15
github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260114133625-907ef99f2a14
github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20251020150604-8ab84f7bad1a
github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20251021173435-e86785845942
github.com/smartcontractkit/chainlink-protos/svr v1.1.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -632,8 +632,8 @@ github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251022073203-7d8
github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251022073203-7d8ae8cf67c1/go.mod h1:oyfOm4k0uqmgZIfxk1elI/59B02shbbJQiiUdPdbMgI=
github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20250818175541-3389ac08a563 h1:ACpDbAxG4fa4sA83dbtYcrnlpE/y7thNIZfHxTv2ZLs=
github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20250818175541-3389ac08a563/go.mod h1:jP5mrOLFEYZZkl7EiCHRRIMSSHCQsYypm1OZSus//iI=
github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20251210101658-1c5c8e4c4f15 h1:Mf+IRvrXutcKAKpuOxq5Ae+AAw4Z5vc66q1xI7qimZQ=
github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20251210101658-1c5c8e4c4f15/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs=
github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260114133625-907ef99f2a14 h1:6/mKGfD04JBeG3H4BQ0NlGUesH52g6hxS+k+VQmZuEc=
github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260114133625-907ef99f2a14/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs=
github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20251020150604-8ab84f7bad1a h1:pr0VFI7AWlDVJBEkcvzXWd97V8w8QMNjRdfPVa/IQLk=
github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20251020150604-8ab84f7bad1a/go.mod h1:jo+cUqNcHwN8IF7SInQNXDZ8qzBsyMpnLdYbDswviFc=
github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20251021173435-e86785845942 h1:T/eCDsUI8EJT4n5zSP4w1mz4RHH+ap8qieA17QYfBhk=
Expand Down
6 changes: 6 additions & 0 deletions pkg/gas/block_history_estimator.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,13 @@ func (b *BlockHistoryEstimator) GetLegacyGas(_ context.Context, _ []byte, gasLim
"Using Evm.GasEstimator.PriceDefault as fallback.", "blocks", b.getBlockHistoryNumbers())
gasPrice = b.eConfig.PriceDefault()
}
b.logger.Infow("DEBUG GetLegacyGas BEFORE cap",
"estimatedGasPrice", gasPrice,
"maxGasPriceWei", maxGasPriceWei,
"PriceMax", b.eConfig.PriceMax())
gasPrice = capGasPrice(gasPrice, maxGasPriceWei, b.eConfig.PriceMax())
b.logger.Infow("DEBUG GetLegacyGas AFTER cap",
"cappedGasPrice", gasPrice)
chainSpecificGasLimit = gasLimit
return
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/gas/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ func (e *evmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit
chainSpecificFeeLimit = feeLimit
} else {
// get legacy fee
e.lggr.Infow("DEBUG GetFee BEFORE GetLegacyGas", "maxFeePrice", maxFeePrice)
fee.GasPrice, chainSpecificFeeLimit, err = e.EvmEstimator.GetLegacyGas(ctx, calldata, feeLimit, maxFeePrice, opts...)
if err != nil {
return
Expand All @@ -308,7 +309,9 @@ func (e *evmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit
}

func (e *evmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress, toAddress *common.Address, opts ...fees.Opt) (*big.Int, error) {
e.lggr.Infow("DEBUG GetMaxCost BEFORE GetFee", "maxFeePrice", maxFeePrice)
fees, gasLimit, err := e.GetFee(ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...)
e.lggr.Infow("DEBUG GetMaxCost AFTER GetFee", "fees", fees, "gasLimit", gasLimit)
if err != nil {
return nil, err
}
Expand Down
32 changes: 26 additions & 6 deletions pkg/txmgr/attempts.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ func (c *evmTxAttemptBuilder) NewTxAttempt(ctx context.Context, etx Tx, lggr log
// NewTxAttemptWithType builds a new attempt with a new fee estimation where the txType can be specified by the caller
// used for L2 re-estimation on broadcasting (note EIP1559 must be disabled otherwise this will fail with mismatched fees + tx type)
func (c *evmTxAttemptBuilder) NewTxAttemptWithType(ctx context.Context, etx Tx, lggr logger.Logger, txType int, opts ...fees.Opt) (attempt TxAttempt, fee gas.EvmFee, feeLimit uint64, retryable bool, err error) {
keySpecificMaxGasPriceWei := c.feeConfig.PriceMaxKey(etx.FromAddress)
fee, feeLimit, err = c.EvmFeeEstimator.GetFee(ctx, etx.EncodedPayload, etx.FeeLimit, keySpecificMaxGasPriceWei, &etx.FromAddress, &etx.ToAddress, opts...)
maxGasPrice := c.getEffectiveMaxGasPrice(etx, lggr)
lggr.Infow("DEBUG NewTxAttemptWithType BEFORE GetFee", "maxGasPrice", maxGasPrice)
fee, feeLimit, err = c.EvmFeeEstimator.GetFee(ctx, etx.EncodedPayload, etx.FeeLimit, maxGasPrice, &etx.FromAddress, &etx.ToAddress, opts...)
if err != nil {
return attempt, fee, feeLimit, true, pkgerrors.Wrap(err, "failed to get fee") // estimator errors are retryable
}
Expand All @@ -64,9 +65,9 @@ func (c *evmTxAttemptBuilder) NewTxAttemptWithType(ctx context.Context, etx Tx,
// NewBumpTxAttempt builds a new attempt with a bumped fee - based on the previous attempt tx type
// used in the txm broadcaster + confirmer when tx ix rejected for too low fee or is not included in a timely manner
func (c *evmTxAttemptBuilder) NewBumpTxAttempt(ctx context.Context, etx Tx, previousAttempt TxAttempt, priorAttempts []TxAttempt, lggr logger.Logger) (attempt TxAttempt, bumpedFee gas.EvmFee, bumpedFeeLimit uint64, retryable bool, err error) {
keySpecificMaxGasPriceWei := c.feeConfig.PriceMaxKey(etx.FromAddress)
maxGasPrice := c.getEffectiveMaxGasPrice(etx, lggr)
// Use the fee limit from the previous attempt to maintain limits adjusted for 2D fees or by estimation
bumpedFee, bumpedFeeLimit, err = c.EvmFeeEstimator.BumpFee(ctx, previousAttempt.TxFee, previousAttempt.ChainSpecificFeeLimit, keySpecificMaxGasPriceWei, newEvmPriorAttempts(priorAttempts))
bumpedFee, bumpedFeeLimit, err = c.EvmFeeEstimator.BumpFee(ctx, previousAttempt.TxFee, previousAttempt.ChainSpecificFeeLimit, maxGasPrice, newEvmPriorAttempts(priorAttempts))
if err != nil {
return attempt, bumpedFee, bumpedFeeLimit, true, pkgerrors.Wrap(err, "failed to bump fee") // estimator errors are retryable
}
Expand All @@ -89,8 +90,8 @@ func (c *evmTxAttemptBuilder) NewPurgeTxAttempt(ctx context.Context, etx Tx, lgg
gasLimit := c.feeConfig.LimitDefault()
// Transactions being purged will always have a previous attempt since it had to have been broadcasted before at least once
previousAttempt := etx.TxAttempts[0]
keySpecificMaxGasPriceWei := c.feeConfig.PriceMaxKey(etx.FromAddress)
bumpedFee, _, err := c.EvmFeeEstimator.BumpFee(ctx, previousAttempt.TxFee, etx.FeeLimit, keySpecificMaxGasPriceWei, newEvmPriorAttempts(etx.TxAttempts))
maxGasPrice := c.getEffectiveMaxGasPrice(etx, lggr)
bumpedFee, _, err := c.EvmFeeEstimator.BumpFee(ctx, previousAttempt.TxFee, etx.FeeLimit, maxGasPrice, newEvmPriorAttempts(etx.TxAttempts))
if err != nil {
return attempt, fmt.Errorf("failed to bump previous fee to use for the purge attempt: %w", err)
}
Expand Down Expand Up @@ -345,3 +346,22 @@ func newEvmPriorAttempts(attempts []TxAttempt) (prior []gas.EvmPriorAttempt) {
}
return
}

// getEffectiveMaxGasPrice returns the effective maximum gas price to use for a transaction.
// It takes the minimum of the key-specific configured max and the transaction's MaxGasPrice (if set).
// This ensures that per-transaction spend limits from billing are respected while still honoring
// the node's configured maximum gas price.
func (c *evmTxAttemptBuilder) getEffectiveMaxGasPrice(etx Tx, lggr logger.Logger) *assets.Wei {
keySpecificMaxGasPriceWei := c.feeConfig.PriceMaxKey(etx.FromAddress)
lggr.Infow("DEBUG getEffectiveMaxGasPrice BEFORE keySpecificMaxGasPriceWei", "keySpecificMaxGasPriceWei", keySpecificMaxGasPriceWei)
if etx.MaxGasPrice != nil {
lggr.Infow("DEBUG getEffectiveMaxGasPrice BEFORE txMaxGasPrice", "txMaxGasPrice", etx.MaxGasPrice)
txMaxGasPrice := assets.NewWei(etx.MaxGasPrice)
// Only use tx request's MaxGasPrice if it's more restrictive (lower) than the key-specific max
if txMaxGasPrice.Cmp(keySpecificMaxGasPriceWei) < 0 {
lggr.Infow("DEBUG getEffectiveMaxGasPrice AFTER txMaxGasPrice", "txMaxGasPrice", txMaxGasPrice)
return txMaxGasPrice
}
}
return keySpecificMaxGasPriceWei
}
24 changes: 19 additions & 5 deletions pkg/txmgr/evm_tx_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ type DbEthTx struct {
SignalCallback bool
// Marks tx callback as signaled
CallbackCompleted bool
// MaxGasPrice is the maximum gas price that can be used for transaction attempts
MaxGasPrice *ubig.Big
}

func (db *DbEthTx) FromTx(tx *Tx) {
Expand All @@ -225,6 +227,10 @@ func (db *DbEthTx) FromTx(tx *Tx) {
db.SignalCallback = tx.SignalCallback
db.CallbackCompleted = tx.CallbackCompleted

if tx.MaxGasPrice != nil {
db.MaxGasPrice = ubig.New(tx.MaxGasPrice)
}

if tx.ChainID != nil {
db.EVMChainID = *ubig.New(tx.ChainID)
}
Expand Down Expand Up @@ -259,6 +265,9 @@ func (db DbEthTx) ToTx(tx *Tx) {
tx.InitialBroadcastAt = db.InitialBroadcastAt
tx.SignalCallback = db.SignalCallback
tx.CallbackCompleted = db.CallbackCompleted
if db.MaxGasPrice != nil {
tx.MaxGasPrice = db.MaxGasPrice.ToInt()
}
}

func dbEthTxsToEvmEthTxs(dbEthTxs []DbEthTx) []Tx {
Expand Down Expand Up @@ -533,8 +542,8 @@ func (o *evmTxStore) InsertTx(ctx context.Context, etx *Tx) error {
if etx.CreatedAt == (time.Time{}) {
etx.CreatedAt = time.Now()
}
const insertEthTxSQL = `INSERT INTO evm.txes (nonce, from_address, to_address, encoded_payload, value, gas_limit, error, broadcast_at, initial_broadcast_at, created_at, state, meta, subject, pipeline_task_run_id, min_confirmations, evm_chain_id, transmit_checker, idempotency_key, signal_callback, callback_completed) VALUES (
:nonce, :from_address, :to_address, :encoded_payload, :value, :gas_limit, :error, :broadcast_at, :initial_broadcast_at, :created_at, :state, :meta, :subject, :pipeline_task_run_id, :min_confirmations, :evm_chain_id, :transmit_checker, :idempotency_key, :signal_callback, :callback_completed
const insertEthTxSQL = `INSERT INTO evm.txes (nonce, from_address, to_address, encoded_payload, value, gas_limit, error, broadcast_at, initial_broadcast_at, created_at, state, meta, subject, pipeline_task_run_id, min_confirmations, evm_chain_id, transmit_checker, idempotency_key, signal_callback, callback_completed, max_gas_price) VALUES (
:nonce, :from_address, :to_address, :encoded_payload, :value, :gas_limit, :error, :broadcast_at, :initial_broadcast_at, :created_at, :state, :meta, :subject, :pipeline_task_run_id, :min_confirmations, :evm_chain_id, :transmit_checker, :idempotency_key, :signal_callback, :callback_completed, :max_gas_price
) RETURNING *`
var dbTx DbEthTx
dbTx.FromTx(etx)
Expand Down Expand Up @@ -1670,13 +1679,18 @@ func (o *evmTxStore) CreateTransaction(ctx context.Context, txRequest TxRequest,
return nil
}
}
// Convert MaxGasPrice to ubig.Big for database storage
var maxGasPriceUbig *ubig.Big
if txRequest.MaxGasPrice != nil {
maxGasPriceUbig = ubig.New(txRequest.MaxGasPrice)
}
err = orm.q.GetContext(ctx, &dbEtx, `
INSERT INTO evm.txes (from_address, to_address, encoded_payload, value, gas_limit, state, created_at, meta, subject, evm_chain_id, min_confirmations, pipeline_task_run_id, transmit_checker, idempotency_key, signal_callback)
INSERT INTO evm.txes (from_address, to_address, encoded_payload, value, gas_limit, state, created_at, meta, subject, evm_chain_id, min_confirmations, pipeline_task_run_id, transmit_checker, idempotency_key, signal_callback, max_gas_price)
VALUES (
$1,$2,$3,$4,$5,'unstarted',NOW(),$6,$7,$8,$9,$10,$11,$12,$13
$1,$2,$3,$4,$5,'unstarted',NOW(),$6,$7,$8,$9,$10,$11,$12,$13,$14
)
RETURNING "txes".*
`, txRequest.FromAddress, txRequest.ToAddress, txRequest.EncodedPayload, assets.Eth(txRequest.Value), txRequest.FeeLimit, txRequest.Meta, txRequest.Strategy.Subject(), chainID.String(), txRequest.MinConfirmations, txRequest.PipelineTaskRunID, txRequest.Checker, txRequest.IdempotencyKey, txRequest.SignalCallback)
`, txRequest.FromAddress, txRequest.ToAddress, txRequest.EncodedPayload, assets.Eth(txRequest.Value), txRequest.FeeLimit, txRequest.Meta, txRequest.Strategy.Subject(), chainID.String(), txRequest.MinConfirmations, txRequest.PipelineTaskRunID, txRequest.Checker, txRequest.IdempotencyKey, txRequest.SignalCallback, maxGasPriceUbig)
if err != nil {
return pkgerrors.Wrap(err, "CreateEthTransaction failed to insert evm tx")
}
Expand Down
1 change: 1 addition & 0 deletions pkg/txmgr/evm_tx_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1650,6 +1650,7 @@ func TestORM_CreateTransaction(t *testing.T) {
FeeLimit: gasLimit,
Meta: nil,
Strategy: strategy,
MaxGasPrice: big.NewInt(1000000000),
}, ethClient.ConfiguredChainID())
assert.NoError(t, err)

Expand Down
Loading