diff --git a/go.mod b/go.mod index ec8eae5a4d..8425ea02b6 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 86a24a9a37..3999a8f1d9 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/gas/block_history_estimator.go b/pkg/gas/block_history_estimator.go index d9ad35cc4e..5b6423a4b8 100644 --- a/pkg/gas/block_history_estimator.go +++ b/pkg/gas/block_history_estimator.go @@ -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 } diff --git a/pkg/gas/models.go b/pkg/gas/models.go index d10109295c..30c8bd86a5 100644 --- a/pkg/gas/models.go +++ b/pkg/gas/models.go @@ -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 @@ -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 } diff --git a/pkg/txmgr/attempts.go b/pkg/txmgr/attempts.go index 5e2c2aac36..c264710a3c 100644 --- a/pkg/txmgr/attempts.go +++ b/pkg/txmgr/attempts.go @@ -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 } @@ -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 } @@ -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) } @@ -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 +} diff --git a/pkg/txmgr/evm_tx_store.go b/pkg/txmgr/evm_tx_store.go index b0506b0625..718eeebf06 100644 --- a/pkg/txmgr/evm_tx_store.go +++ b/pkg/txmgr/evm_tx_store.go @@ -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) { @@ -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) } @@ -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 { @@ -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) @@ -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") } diff --git a/pkg/txmgr/evm_tx_store_test.go b/pkg/txmgr/evm_tx_store_test.go index c1594fd829..e3de857958 100644 --- a/pkg/txmgr/evm_tx_store_test.go +++ b/pkg/txmgr/evm_tx_store_test.go @@ -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)