diff --git a/common/txmgr/types/fee_estimator.go b/common/txmgr/types/fee_estimator.go new file mode 100644 index 00000000000..c9f2b66872e --- /dev/null +++ b/common/txmgr/types/fee_estimator.go @@ -0,0 +1,32 @@ +package types + +import "context" + +// Opt is an option for a gas estimator +type Opt int + +const ( + // OptForceRefetch forces the estimator to bust a cache if necessary + OptForceRefetch Opt = iota +) + +// PriorAttempt provides a generic interface for reading tx data to be used in the fee esimators +type PriorAttempt[FEE any, HASH any] interface { + Fee() FEE + GetChainSpecificGasLimit() uint32 + GetBroadcastBeforeBlockNum() *int64 + GetHash() HASH + GetTxType() int +} + +// FeeEstimator provides a generic interface for fee estimation +// +//go:generate mockery --quiet --name FeeEstimator --output ./mocks/ --case=underscore +type FeeEstimator[HEAD any, FEE any, MAXPRICE any, HASH any] interface { + OnNewLongestChain(context.Context, HEAD) + Start(context.Context) error + Close() error + + GetFee(ctx context.Context, calldata []byte, feeLimit uint32, maxFeePrice MAXPRICE, opts ...Opt) (fee FEE, chainSpecificFeeLimit uint32, err error) + BumpFee(ctx context.Context, originalFee FEE, feeLimit uint32, maxFeePrice MAXPRICE, attempts []PriorAttempt[FEE, HASH]) (bumpedFee FEE, chainSpecificFeeLimit uint32, err error) +} diff --git a/common/txmgr/types/mocks/fee_estimator.go b/common/txmgr/types/mocks/fee_estimator.go new file mode 100644 index 00000000000..9b1c497d424 --- /dev/null +++ b/common/txmgr/types/mocks/fee_estimator.go @@ -0,0 +1,132 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + types "github.com/smartcontractkit/chainlink/common/txmgr/types" + mock "github.com/stretchr/testify/mock" +) + +// FeeEstimator is an autogenerated mock type for the FeeEstimator type +type FeeEstimator[HEAD interface{}, FEE interface{}, MAXPRICE interface{}, HASH interface{}] struct { + mock.Mock +} + +// BumpFee provides a mock function with given fields: ctx, originalFee, feeLimit, maxFeePrice, attempts +func (_m *FeeEstimator[HEAD, FEE, MAXPRICE, HASH]) BumpFee(ctx context.Context, originalFee FEE, feeLimit uint32, maxFeePrice MAXPRICE, attempts []types.PriorAttempt[FEE, HASH]) (FEE, uint32, error) { + ret := _m.Called(ctx, originalFee, feeLimit, maxFeePrice, attempts) + + var r0 FEE + var r1 uint32 + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, FEE, uint32, MAXPRICE, []types.PriorAttempt[FEE, HASH]) (FEE, uint32, error)); ok { + return rf(ctx, originalFee, feeLimit, maxFeePrice, attempts) + } + if rf, ok := ret.Get(0).(func(context.Context, FEE, uint32, MAXPRICE, []types.PriorAttempt[FEE, HASH]) FEE); ok { + r0 = rf(ctx, originalFee, feeLimit, maxFeePrice, attempts) + } else { + r0 = ret.Get(0).(FEE) + } + + if rf, ok := ret.Get(1).(func(context.Context, FEE, uint32, MAXPRICE, []types.PriorAttempt[FEE, HASH]) uint32); ok { + r1 = rf(ctx, originalFee, feeLimit, maxFeePrice, attempts) + } else { + r1 = ret.Get(1).(uint32) + } + + if rf, ok := ret.Get(2).(func(context.Context, FEE, uint32, MAXPRICE, []types.PriorAttempt[FEE, HASH]) error); ok { + r2 = rf(ctx, originalFee, feeLimit, maxFeePrice, attempts) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// Close provides a mock function with given fields: +func (_m *FeeEstimator[HEAD, FEE, MAXPRICE, HASH]) Close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetFee provides a mock function with given fields: ctx, calldata, feeLimit, maxFeePrice, opts +func (_m *FeeEstimator[HEAD, FEE, MAXPRICE, HASH]) GetFee(ctx context.Context, calldata []byte, feeLimit uint32, maxFeePrice MAXPRICE, opts ...types.Opt) (FEE, uint32, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, calldata, feeLimit, maxFeePrice) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 FEE + var r1 uint32 + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, []byte, uint32, MAXPRICE, ...types.Opt) (FEE, uint32, error)); ok { + return rf(ctx, calldata, feeLimit, maxFeePrice, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, []byte, uint32, MAXPRICE, ...types.Opt) FEE); ok { + r0 = rf(ctx, calldata, feeLimit, maxFeePrice, opts...) + } else { + r0 = ret.Get(0).(FEE) + } + + if rf, ok := ret.Get(1).(func(context.Context, []byte, uint32, MAXPRICE, ...types.Opt) uint32); ok { + r1 = rf(ctx, calldata, feeLimit, maxFeePrice, opts...) + } else { + r1 = ret.Get(1).(uint32) + } + + if rf, ok := ret.Get(2).(func(context.Context, []byte, uint32, MAXPRICE, ...types.Opt) error); ok { + r2 = rf(ctx, calldata, feeLimit, maxFeePrice, opts...) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// OnNewLongestChain provides a mock function with given fields: _a0, _a1 +func (_m *FeeEstimator[HEAD, FEE, MAXPRICE, HASH]) OnNewLongestChain(_a0 context.Context, _a1 HEAD) { + _m.Called(_a0, _a1) +} + +// Start provides a mock function with given fields: _a0 +func (_m *FeeEstimator[HEAD, FEE, MAXPRICE, HASH]) Start(_a0 context.Context) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewFeeEstimator interface { + mock.TestingT + Cleanup(func()) +} + +// NewFeeEstimator creates a new instance of FeeEstimator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewFeeEstimator[HEAD interface{}, FEE interface{}, MAXPRICE interface{}, HASH interface{}](t mockConstructorTestingTNewFeeEstimator) *FeeEstimator[HEAD, FEE, MAXPRICE, HASH] { + mock := &FeeEstimator[HEAD, FEE, MAXPRICE, HASH]{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/chainlink.Dockerfile b/core/chainlink.Dockerfile index b3fbe0b7ac1..369c8b28464 100644 --- a/core/chainlink.Dockerfile +++ b/core/chainlink.Dockerfile @@ -12,6 +12,7 @@ RUN go mod download # Env vars needed for chainlink build ARG COMMIT_SHA +COPY common common COPY core core COPY operator_ui operator_ui diff --git a/core/chains/evm/gas/arbitrum_estimator.go b/core/chains/evm/gas/arbitrum_estimator.go index cc8485839d6..4e51ec93560 100644 --- a/core/chains/evm/gas/arbitrum_estimator.go +++ b/core/chains/evm/gas/arbitrum_estimator.go @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" "golang.org/x/exp/slices" + txmgrtypes "github.com/smartcontractkit/chainlink/common/txmgr/types" "github.com/smartcontractkit/chainlink/core/assets" evmclient "github.com/smartcontractkit/chainlink/core/chains/evm/client" "github.com/smartcontractkit/chainlink/core/logger" @@ -34,7 +35,7 @@ type arbitrumEstimator struct { cfg ArbConfig - Estimator // *l2SuggestedPriceEstimator + EvmEstimator // *l2SuggestedPriceEstimator client ethClient pollPeriod time.Duration @@ -50,11 +51,11 @@ type arbitrumEstimator struct { chDone chan struct{} } -func NewArbitrumEstimator(lggr logger.Logger, cfg ArbConfig, rpcClient rpcClient, ethClient ethClient) Estimator { +func NewArbitrumEstimator(lggr logger.Logger, cfg ArbConfig, rpcClient rpcClient, ethClient ethClient) EvmEstimator { lggr = lggr.Named("ArbitrumEstimator") return &arbitrumEstimator{ cfg: cfg, - Estimator: NewL2SuggestedPriceEstimator(lggr, rpcClient), + EvmEstimator: NewL2SuggestedPriceEstimator(lggr, rpcClient), client: ethClient, pollPeriod: 10 * time.Second, logger: lggr, @@ -67,7 +68,7 @@ func NewArbitrumEstimator(lggr logger.Logger, cfg ArbConfig, rpcClient rpcClient func (a *arbitrumEstimator) Start(ctx context.Context) error { return a.StartOnce("ArbitrumEstimator", func() error { - if err := a.Estimator.Start(ctx); err != nil { + if err := a.EvmEstimator.Start(ctx); err != nil { return errors.Wrap(err, "failed to start gas price estimator") } go a.run() @@ -78,7 +79,7 @@ func (a *arbitrumEstimator) Start(ctx context.Context) error { func (a *arbitrumEstimator) Close() error { return a.StopOnce("ArbitrumEstimator", func() (err error) { close(a.chStop) - err = errors.Wrap(a.Estimator.Close(), "failed to stop gas price estimator") + err = errors.Wrap(a.EvmEstimator.Close(), "failed to stop gas price estimator") <-a.chDone return }) @@ -89,14 +90,14 @@ func (a *arbitrumEstimator) Close() error { // - Limit is computed from the dynamic values perL2Tx and perL1CalldataUnit, provided by the getPricesInArbGas() method // of the precompilie contract at ArbGasInfoAddress. perL2Tx is a constant amount of gas, and perL1CalldataUnit is // multiplied by the length of the tx calldata. The sum of these two values plus the original l2GasLimit is returned. -func (a *arbitrumEstimator) GetLegacyGas(ctx context.Context, calldata []byte, l2GasLimit uint32, maxGasPriceWei *assets.Wei, opts ...Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) { - gasPrice, _, err = a.Estimator.GetLegacyGas(ctx, calldata, l2GasLimit, maxGasPriceWei, opts...) +func (a *arbitrumEstimator) GetLegacyGas(ctx context.Context, calldata []byte, l2GasLimit uint32, maxGasPriceWei *assets.Wei, opts ...txmgrtypes.Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) { + gasPrice, _, err = a.EvmEstimator.GetLegacyGas(ctx, calldata, l2GasLimit, maxGasPriceWei, opts...) if err != nil { return } gasPrice = a.gasPriceWithBuffer(gasPrice, maxGasPriceWei) ok := a.IfStarted(func() { - if slices.Contains(opts, OptForceRefetch) { + if slices.Contains(opts, txmgrtypes.OptForceRefetch) { ch := make(chan struct{}) select { case a.chForceRefetch <- ch: diff --git a/core/chains/evm/gas/block_history_estimator.go b/core/chains/evm/gas/block_history_estimator.go index 6c2ada127ca..250130deb66 100644 --- a/core/chains/evm/gas/block_history_estimator.go +++ b/core/chains/evm/gas/block_history_estimator.go @@ -15,6 +15,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + txmgrtypes "github.com/smartcontractkit/chainlink/common/txmgr/types" "github.com/smartcontractkit/chainlink/core/assets" evmclient "github.com/smartcontractkit/chainlink/core/chains/evm/client" evmtypes "github.com/smartcontractkit/chainlink/core/chains/evm/types" @@ -74,7 +75,7 @@ const BumpingHaltedLabel = "Tx gas bumping halted since price exceeds current bl var ErrConnectivity = errors.New("transaction propagation issue: transactions are not being mined") -var _ Estimator = &BlockHistoryEstimator{} +var _ EvmEstimator = &BlockHistoryEstimator{} //go:generate mockery --quiet --name Config --output ./mocks/ --case=underscore type ( @@ -107,7 +108,7 @@ type ( // NewBlockHistoryEstimator returns a new BlockHistoryEstimator that listens // for new heads and updates the base gas price dynamically based on the // configured percentile of gas prices in that block -func NewBlockHistoryEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg Config, chainID big.Int) Estimator { +func NewBlockHistoryEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg Config, chainID big.Int) EvmEstimator { ctx, cancel := context.WithCancel(context.Background()) b := &BlockHistoryEstimator{ ethClient: ethClient, @@ -225,7 +226,7 @@ func (b *BlockHistoryEstimator) HealthReport() map[string]error { return map[string]error{b.Name(): b.Healthy()} } -func (b *BlockHistoryEstimator) GetLegacyGas(_ context.Context, _ []byte, gasLimit uint32, maxGasPriceWei *assets.Wei, _ ...Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) { +func (b *BlockHistoryEstimator) GetLegacyGas(_ context.Context, _ []byte, gasLimit uint32, maxGasPriceWei *assets.Wei, _ ...txmgrtypes.Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) { ok := b.IfStarted(func() { chainSpecificGasLimit = applyMultiplier(gasLimit, b.config.EvmGasLimitMultiplier()) gasPrice = b.getGasPrice() @@ -264,7 +265,7 @@ func (b *BlockHistoryEstimator) getTipCap() *assets.Wei { return b.tipCap } -func (b *BlockHistoryEstimator) BumpLegacyGas(_ context.Context, originalGasPrice *assets.Wei, gasLimit uint32, maxGasPriceWei *assets.Wei, attempts []PriorAttempt) (bumpedGasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) { +func (b *BlockHistoryEstimator) BumpLegacyGas(_ context.Context, originalGasPrice *assets.Wei, gasLimit uint32, maxGasPriceWei *assets.Wei, attempts []EvmPriorAttempt) (bumpedGasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) { if b.config.BlockHistoryEstimatorCheckInclusionBlocks() > 0 { if err = b.checkConnectivity(attempts); err != nil { if errors.Is(err, ErrConnectivity) { @@ -280,7 +281,7 @@ func (b *BlockHistoryEstimator) BumpLegacyGas(_ context.Context, originalGasPric // checkConnectivity detects if the transaction is not being included due to // some kind of mempool propagation or connectivity issue rather than // insufficiently high pricing and returns error if so -func (b *BlockHistoryEstimator) checkConnectivity(attempts []PriorAttempt) error { +func (b *BlockHistoryEstimator) checkConnectivity(attempts []EvmPriorAttempt) error { percentile := int(b.config.BlockHistoryEstimatorCheckInclusionPercentile()) // how many blocks since broadcast? latestBlockNum := b.getCurrentBlockNum() @@ -438,7 +439,7 @@ func calcFeeCap(latestAvailableBaseFeePerGas *assets.Wei, cfg Config, tipCap *as return feeCap } -func (b *BlockHistoryEstimator) BumpDynamicFee(_ context.Context, originalFee DynamicFee, originalGasLimit uint32, maxGasPriceWei *assets.Wei, attempts []PriorAttempt) (bumped DynamicFee, chainSpecificGasLimit uint32, err error) { +func (b *BlockHistoryEstimator) BumpDynamicFee(_ context.Context, originalFee DynamicFee, originalGasLimit uint32, maxGasPriceWei *assets.Wei, attempts []EvmPriorAttempt) (bumped DynamicFee, chainSpecificGasLimit uint32, err error) { if b.config.BlockHistoryEstimatorCheckInclusionBlocks() > 0 { if err = b.checkConnectivity(attempts); err != nil { if errors.Is(err, ErrConnectivity) { diff --git a/core/chains/evm/gas/block_history_estimator_test.go b/core/chains/evm/gas/block_history_estimator_test.go index 676d25e338b..7e9b7f24b75 100644 --- a/core/chains/evm/gas/block_history_estimator_test.go +++ b/core/chains/evm/gas/block_history_estimator_test.go @@ -18,6 +18,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap/zapcore" + txmgrtypes "github.com/smartcontractkit/chainlink/common/txmgr/types" "github.com/smartcontractkit/chainlink/core/assets" evmclient "github.com/smartcontractkit/chainlink/core/chains/evm/client" "github.com/smartcontractkit/chainlink/core/chains/evm/gas" @@ -43,7 +44,7 @@ func newConfigWithEIP1559DynamicFeesDisabled(t *testing.T) *gas.MockConfig { return cfg } -func newBlockHistoryEstimatorWithChainID(t *testing.T, c evmclient.Client, cfg gas.Config, cid big.Int) gas.Estimator { +func newBlockHistoryEstimatorWithChainID(t *testing.T, c evmclient.Client, cfg gas.Config, cid big.Int) gas.EvmEstimator { return gas.NewBlockHistoryEstimator(logger.TestLogger(t), c, cfg, cid) } @@ -1806,7 +1807,7 @@ func TestBlockHistoryEstimator_GetDynamicFee(t *testing.T) { }) } -var _ gas.PriorAttempt = &MockAttempt{} +var _ txmgrtypes.PriorAttempt[gas.EvmFee, common.Hash] = &MockAttempt{} type MockAttempt struct { BroadcastBeforeBlockNum *int64 @@ -1817,11 +1818,21 @@ type MockAttempt struct { GasTipCap *assets.Wei } -func (m *MockAttempt) GetGasPrice() *assets.Wei { +func (m *MockAttempt) Fee() (f gas.EvmFee) { + f.Legacy = m.getGasPrice() + + d := m.dynamicFee() + if d.FeeCap != nil && d.TipCap != nil { + f.Dynamic = &d + } + return f +} + +func (m *MockAttempt) getGasPrice() *assets.Wei { return m.GasPrice } -func (m *MockAttempt) DynamicFee() gas.DynamicFee { +func (m *MockAttempt) dynamicFee() gas.DynamicFee { return gas.DynamicFee{ FeeCap: m.GasFeeCap, TipCap: m.GasTipCap, @@ -1853,7 +1864,7 @@ func TestBlockHistoryEstimator_CheckConnectivity(t *testing.T) { gas.NewBlockHistoryEstimator(lggr, nil, cfg, *testutils.NewRandomEVMChainID()), ) - attempts := []gas.PriorAttempt{ + attempts := []txmgrtypes.PriorAttempt[gas.EvmFee, common.Hash]{ &MockAttempt{TxType: 0x0, Hash: utils.NewHash()}, } @@ -1918,7 +1929,7 @@ func TestBlockHistoryEstimator_CheckConnectivity(t *testing.T) { num := int64(0) hash := utils.NewHash() - attempts = []gas.PriorAttempt{ + attempts = []txmgrtypes.PriorAttempt[gas.EvmFee, common.Hash]{ &MockAttempt{TxType: 0x3, BroadcastBeforeBlockNum: &num, Hash: hash}, } @@ -1928,7 +1939,7 @@ func TestBlockHistoryEstimator_CheckConnectivity(t *testing.T) { assert.Contains(t, err.Error(), fmt.Sprintf("attempt %s has unknown transaction type 0x3", hash)) }) - attempts = []gas.PriorAttempt{ + attempts = []txmgrtypes.PriorAttempt[gas.EvmFee, common.Hash]{ &MockAttempt{TxType: 0x0, BroadcastBeforeBlockNum: &num, Hash: hash}, } @@ -1965,7 +1976,7 @@ func TestBlockHistoryEstimator_CheckConnectivity(t *testing.T) { } gas.SetRollingBlockHistory(bhe, []evmtypes.Block{b0, b1, b2, b3}) - attempts = []gas.PriorAttempt{ + attempts = []txmgrtypes.PriorAttempt[gas.EvmFee, common.Hash]{ &MockAttempt{TxType: 0x0, Hash: utils.NewHash(), GasPrice: assets.NewWeiI(1000), BroadcastBeforeBlockNum: testutils.Ptr(int64(4))}, // This is very expensive but will be ignored due to BroadcastBeforeBlockNum being too recent &MockAttempt{TxType: 0x0, Hash: utils.NewHash(), GasPrice: assets.NewWeiI(3), BroadcastBeforeBlockNum: testutils.Ptr(int64(0))}, &MockAttempt{TxType: 0x0, Hash: utils.NewHash(), GasPrice: assets.NewWeiI(5), BroadcastBeforeBlockNum: testutils.Ptr(int64(1))}, @@ -2014,7 +2025,7 @@ func TestBlockHistoryEstimator_CheckConnectivity(t *testing.T) { } gas.SetRollingBlockHistory(bhe, []evmtypes.Block{b0}) - attempts = []gas.PriorAttempt{ + attempts = []txmgrtypes.PriorAttempt[gas.EvmFee, common.Hash]{ &MockAttempt{TxType: 0x2, Hash: utils.NewHash(), GasFeeCap: assets.NewWeiI(1), GasTipCap: assets.NewWeiI(3), BroadcastBeforeBlockNum: testutils.Ptr(int64(0))}, &MockAttempt{TxType: 0x0, Hash: utils.NewHash(), GasPrice: assets.NewWeiI(10), BroadcastBeforeBlockNum: testutils.Ptr(int64(0))}, } @@ -2035,7 +2046,7 @@ func TestBlockHistoryEstimator_CheckConnectivity(t *testing.T) { assert.Contains(t, err.Error(), fmt.Sprintf("transaction %s has gas price of 10 wei, which is above percentile=60%% (percentile price: 7 wei) for blocks 3 thru 3 (checking 1 blocks)", attempts[1].GetHash())) }) - attempts = []gas.PriorAttempt{ + attempts = []txmgrtypes.PriorAttempt[gas.EvmFee, common.Hash]{ &MockAttempt{TxType: 0x2, Hash: utils.NewHash(), GasFeeCap: assets.NewWeiI(11), GasTipCap: assets.NewWeiI(10), BroadcastBeforeBlockNum: testutils.Ptr(int64(0))}, &MockAttempt{TxType: 0x0, Hash: utils.NewHash(), GasPrice: assets.NewWeiI(3), BroadcastBeforeBlockNum: testutils.Ptr(int64(0))}, } @@ -2083,7 +2094,7 @@ func TestBlockHistoryEstimator_CheckConnectivity(t *testing.T) { blocks := []evmtypes.Block{b0, b1, b2, b3} gas.SetRollingBlockHistory(bhe, blocks) - attempts = []gas.PriorAttempt{ + attempts = []txmgrtypes.PriorAttempt[gas.EvmFee, common.Hash]{ &MockAttempt{TxType: 0x2, Hash: utils.NewHash(), GasFeeCap: assets.NewWeiI(30), GasTipCap: assets.NewWeiI(1000), BroadcastBeforeBlockNum: testutils.Ptr(int64(4))}, // This is very expensive but will be ignored due to BroadcastBeforeBlockNum being too recent &MockAttempt{TxType: 0x2, Hash: utils.NewHash(), GasFeeCap: assets.NewWeiI(30), GasTipCap: assets.NewWeiI(3), BroadcastBeforeBlockNum: testutils.Ptr(int64(0))}, &MockAttempt{TxType: 0x2, Hash: utils.NewHash(), GasFeeCap: assets.NewWeiI(30), GasTipCap: assets.NewWeiI(5), BroadcastBeforeBlockNum: testutils.Ptr(int64(1))}, @@ -2118,7 +2129,7 @@ func TestBlockHistoryEstimator_CheckConnectivity(t *testing.T) { cfg.BlockHistoryEstimatorCheckInclusionBlocksF = 3 cfg.BlockHistoryEstimatorCheckInclusionPercentileF = 5 - attempts = []gas.PriorAttempt{ + attempts = []txmgrtypes.PriorAttempt[gas.EvmFee, common.Hash]{ &MockAttempt{TxType: 0x2, Hash: utils.NewHash(), GasFeeCap: assets.NewWeiI(4), GasTipCap: assets.NewWeiI(7), BroadcastBeforeBlockNum: testutils.Ptr(int64(1))}, } @@ -2151,11 +2162,11 @@ func TestBlockHistoryEstimator_Bumps(t *testing.T) { head := cltest.Head(1) bhe.OnNewLongestChain(testutils.Context(t), head) - attempts := []gas.PriorAttempt{ + attempts := []txmgrtypes.PriorAttempt[gas.EvmFee, common.Hash]{ &MockAttempt{TxType: 0x0, Hash: utils.NewHash(), GasPrice: assets.NewWeiI(1000), BroadcastBeforeBlockNum: testutils.Ptr(int64(0))}, } - _, _, err := bhe.BumpLegacyGas(testutils.Context(t), assets.NewWeiI(42), 100000, maxGasPrice, attempts) + _, _, err := bhe.BumpLegacyGas(testutils.Context(t), assets.NewWeiI(42), 100000, maxGasPrice, gas.MakeEvmPriorAttempts(attempts)) require.Error(t, err) assert.True(t, errors.Is(err, gas.ErrConnectivity)) assert.Contains(t, err.Error(), fmt.Sprintf("transaction %s has gas price of 1 kwei, which is above percentile=10%% (percentile price: 1 wei) for blocks 1 thru 1 (checking 1 blocks)", attempts[0].GetHash())) @@ -2258,11 +2269,11 @@ func TestBlockHistoryEstimator_Bumps(t *testing.T) { bhe.OnNewLongestChain(testutils.Context(t), head) originalFee := gas.DynamicFee{FeeCap: assets.NewWeiI(100), TipCap: assets.NewWeiI(25)} - attempts := []gas.PriorAttempt{ + attempts := []txmgrtypes.PriorAttempt[gas.EvmFee, common.Hash]{ &MockAttempt{TxType: 0x2, Hash: utils.NewHash(), GasTipCap: originalFee.TipCap, GasFeeCap: originalFee.FeeCap, BroadcastBeforeBlockNum: testutils.Ptr(int64(0))}, } - _, _, err := bhe.BumpDynamicFee(testutils.Context(t), originalFee, 100000, maxGasPrice, attempts) + _, _, err := bhe.BumpDynamicFee(testutils.Context(t), originalFee, 100000, maxGasPrice, gas.MakeEvmPriorAttempts(attempts)) require.Error(t, err) assert.True(t, errors.Is(err, gas.ErrConnectivity)) assert.Contains(t, err.Error(), fmt.Sprintf("transaction %s has tip cap of 25 wei, which is above percentile=10%% (percentile tip cap: 1 wei) for blocks 1 thru 1 (checking 1 blocks)", attempts[0].GetHash())) diff --git a/core/chains/evm/gas/cmd/arbgas/main.go b/core/chains/evm/gas/cmd/arbgas/main.go index 10f1f864865..a14752d082d 100644 --- a/core/chains/evm/gas/cmd/arbgas/main.go +++ b/core/chains/evm/gas/cmd/arbgas/main.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/rpc" "go.uber.org/zap/zapcore" + txmgrtypes "github.com/smartcontractkit/chainlink/common/txmgr/types" "github.com/smartcontractkit/chainlink/core/assets" "github.com/smartcontractkit/chainlink/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/core/logger" @@ -26,14 +27,14 @@ func main() { lggr.SetLogLevel(zapcore.DebugLevel) ctx := context.Background() - withEstimator(ctx, logger.Sugared(lggr), url, func(e gas.Estimator) { + withEstimator(ctx, logger.Sugared(lggr), url, func(e gas.EvmEstimator) { printGetLegacyGas(ctx, e, make([]byte, 10), 500_000, assets.GWei(1)) - printGetLegacyGas(ctx, e, make([]byte, 10), 500_000, assets.GWei(1), gas.OptForceRefetch) + printGetLegacyGas(ctx, e, make([]byte, 10), 500_000, assets.GWei(1), txmgrtypes.OptForceRefetch) printGetLegacyGas(ctx, e, make([]byte, 10), max, assets.GWei(1)) }) } -func printGetLegacyGas(ctx context.Context, e gas.Estimator, calldata []byte, l2GasLimit uint32, maxGasPrice *assets.Wei, opts ...gas.Opt) { +func printGetLegacyGas(ctx context.Context, e gas.EvmEstimator, calldata []byte, l2GasLimit uint32, maxGasPrice *assets.Wei, opts ...txmgrtypes.Opt) { price, limit, err := e.GetLegacyGas(ctx, calldata, l2GasLimit, maxGasPrice, opts...) if err != nil { log.Println("failed to get legacy gas:", err) @@ -45,7 +46,7 @@ func printGetLegacyGas(ctx context.Context, e gas.Estimator, calldata []byte, l2 const max = 50_000_000 -func withEstimator(ctx context.Context, lggr logger.SugaredLogger, url string, f func(e gas.Estimator)) { +func withEstimator(ctx context.Context, lggr logger.SugaredLogger, url string, f func(e gas.EvmEstimator)) { rc, err := rpc.Dial(url) if err != nil { log.Fatal(err) diff --git a/core/chains/evm/gas/fixed_price_estimator.go b/core/chains/evm/gas/fixed_price_estimator.go index 0c970ccd870..cae703446e9 100644 --- a/core/chains/evm/gas/fixed_price_estimator.go +++ b/core/chains/evm/gas/fixed_price_estimator.go @@ -5,12 +5,13 @@ import ( "github.com/pkg/errors" + txmgrtypes "github.com/smartcontractkit/chainlink/common/txmgr/types" "github.com/smartcontractkit/chainlink/core/assets" evmtypes "github.com/smartcontractkit/chainlink/core/chains/evm/types" "github.com/smartcontractkit/chainlink/core/logger" ) -var _ Estimator = &fixedPriceEstimator{} +var _ EvmEstimator = &fixedPriceEstimator{} type fixedPriceEstimator struct { config Config @@ -19,7 +20,7 @@ type fixedPriceEstimator struct { // NewFixedPriceEstimator returns a new "FixedPrice" estimator which will // always use the config default values for gas prices and limits -func NewFixedPriceEstimator(cfg Config, lggr logger.Logger) Estimator { +func NewFixedPriceEstimator(cfg Config, lggr logger.Logger) EvmEstimator { return &fixedPriceEstimator{cfg, logger.Sugared(lggr.Named("FixedPriceEstimator"))} } @@ -35,14 +36,14 @@ func (f *fixedPriceEstimator) Start(context.Context) error { func (f *fixedPriceEstimator) Close() error { return nil } func (f *fixedPriceEstimator) OnNewLongestChain(_ context.Context, _ *evmtypes.Head) {} -func (f *fixedPriceEstimator) GetLegacyGas(_ context.Context, _ []byte, gasLimit uint32, maxGasPriceWei *assets.Wei, _ ...Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) { +func (f *fixedPriceEstimator) GetLegacyGas(_ context.Context, _ []byte, gasLimit uint32, maxGasPriceWei *assets.Wei, _ ...txmgrtypes.Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) { gasPrice = f.config.EvmGasPriceDefault() chainSpecificGasLimit = applyMultiplier(gasLimit, f.config.EvmGasLimitMultiplier()) gasPrice = capGasPrice(gasPrice, maxGasPriceWei, f.config) return } -func (f *fixedPriceEstimator) BumpLegacyGas(_ context.Context, originalGasPrice *assets.Wei, originalGasLimit uint32, maxGasPriceWei *assets.Wei, _ []PriorAttempt) (gasPrice *assets.Wei, gasLimit uint32, err error) { +func (f *fixedPriceEstimator) BumpLegacyGas(_ context.Context, originalGasPrice *assets.Wei, originalGasLimit uint32, maxGasPriceWei *assets.Wei, _ []EvmPriorAttempt) (gasPrice *assets.Wei, gasLimit uint32, err error) { return BumpLegacyGasPriceOnly(f.config, f.lggr, f.config.EvmGasPriceDefault(), originalGasPrice, originalGasLimit, maxGasPriceWei) } @@ -68,6 +69,6 @@ func (f *fixedPriceEstimator) GetDynamicFee(_ context.Context, originalGasLimit }, chainSpecificGasLimit, nil } -func (f *fixedPriceEstimator) BumpDynamicFee(_ context.Context, originalFee DynamicFee, originalGasLimit uint32, maxGasPriceWei *assets.Wei, _ []PriorAttempt) (bumped DynamicFee, chainSpecificGasLimit uint32, err error) { +func (f *fixedPriceEstimator) BumpDynamicFee(_ context.Context, originalFee DynamicFee, originalGasLimit uint32, maxGasPriceWei *assets.Wei, _ []EvmPriorAttempt) (bumped DynamicFee, chainSpecificGasLimit uint32, err error) { return BumpDynamicFeeOnly(f.config, f.lggr, f.config.EvmGasTipCapDefault(), nil, originalFee, originalGasLimit, maxGasPriceWei) } diff --git a/core/chains/evm/gas/helpers_test.go b/core/chains/evm/gas/helpers_test.go index b4608220b10..0b89ef1b4e6 100644 --- a/core/chains/evm/gas/helpers_test.go +++ b/core/chains/evm/gas/helpers_test.go @@ -4,8 +4,10 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" + txmgrtypes "github.com/smartcontractkit/chainlink/common/txmgr/types" "github.com/smartcontractkit/chainlink/core/assets" evmtypes "github.com/smartcontractkit/chainlink/core/chains/evm/types" "github.com/smartcontractkit/chainlink/core/config" @@ -16,21 +18,21 @@ func init() { MaxStartTime = 1 * time.Second } -func (b *BlockHistoryEstimator) CheckConnectivity(attempts []PriorAttempt) error { - return b.checkConnectivity(attempts) +func (b *BlockHistoryEstimator) CheckConnectivity(attempts []txmgrtypes.PriorAttempt[EvmFee, common.Hash]) error { + return b.checkConnectivity(MakeEvmPriorAttempts(attempts)) } -func BlockHistoryEstimatorFromInterface(bhe Estimator) *BlockHistoryEstimator { +func BlockHistoryEstimatorFromInterface(bhe EvmEstimator) *BlockHistoryEstimator { return bhe.(*BlockHistoryEstimator) } -func SetRollingBlockHistory(bhe Estimator, blocks []evmtypes.Block) { +func SetRollingBlockHistory(bhe EvmEstimator, blocks []evmtypes.Block) { bhe.(*BlockHistoryEstimator).blocksMu.Lock() defer bhe.(*BlockHistoryEstimator).blocksMu.Unlock() bhe.(*BlockHistoryEstimator).blocks = blocks } -func GetRollingBlockHistory(bhe Estimator) []evmtypes.Block { +func GetRollingBlockHistory(bhe EvmEstimator) []evmtypes.Block { return bhe.(*BlockHistoryEstimator).getBlocks() } diff --git a/core/chains/evm/gas/l2_suggested_estimator.go b/core/chains/evm/gas/l2_suggested_estimator.go index 5f1e78d5340..e40d5e22f10 100644 --- a/core/chains/evm/gas/l2_suggested_estimator.go +++ b/core/chains/evm/gas/l2_suggested_estimator.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "golang.org/x/exp/slices" + txmgrtypes "github.com/smartcontractkit/chainlink/common/txmgr/types" "github.com/smartcontractkit/chainlink/core/assets" evmclient "github.com/smartcontractkit/chainlink/core/chains/evm/client" evmtypes "github.com/smartcontractkit/chainlink/core/chains/evm/types" @@ -17,7 +18,7 @@ import ( ) var ( - _ Estimator = &l2SuggestedPriceEstimator{} + _ EvmEstimator = &l2SuggestedPriceEstimator{} ) //go:generate mockery --quiet --name rpcClient --output ./mocks/ --case=underscore --structname RPCClient @@ -43,7 +44,7 @@ type l2SuggestedPriceEstimator struct { } // NewL2SuggestedPriceEstimator returns a new Estimator which uses the L2 suggested gas price. -func NewL2SuggestedPriceEstimator(lggr logger.Logger, client rpcClient) Estimator { +func NewL2SuggestedPriceEstimator(lggr logger.Logger, client rpcClient) EvmEstimator { return &l2SuggestedPriceEstimator{ client: client, pollPeriod: 10 * time.Second, @@ -118,16 +119,16 @@ func (*l2SuggestedPriceEstimator) GetDynamicFee(_ context.Context, _ uint32, _ * return } -func (*l2SuggestedPriceEstimator) BumpDynamicFee(_ context.Context, _ DynamicFee, _ uint32, _ *assets.Wei, _ []PriorAttempt) (bumped DynamicFee, chainSpecificGasLimit uint32, err error) { +func (*l2SuggestedPriceEstimator) BumpDynamicFee(_ context.Context, _ DynamicFee, _ uint32, _ *assets.Wei, _ []EvmPriorAttempt) (bumped DynamicFee, chainSpecificGasLimit uint32, err error) { err = errors.New("dynamic fees are not implemented for this layer 2") return } -func (o *l2SuggestedPriceEstimator) GetLegacyGas(ctx context.Context, _ []byte, l2GasLimit uint32, maxGasPriceWei *assets.Wei, opts ...Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) { +func (o *l2SuggestedPriceEstimator) GetLegacyGas(ctx context.Context, _ []byte, l2GasLimit uint32, maxGasPriceWei *assets.Wei, opts ...txmgrtypes.Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) { chainSpecificGasLimit = l2GasLimit ok := o.IfStarted(func() { - if slices.Contains(opts, OptForceRefetch) { + if slices.Contains(opts, txmgrtypes.OptForceRefetch) { ch := make(chan struct{}) select { case o.chForceRefetch <- ch: @@ -166,7 +167,7 @@ func (o *l2SuggestedPriceEstimator) GetLegacyGas(ctx context.Context, _ []byte, return } -func (o *l2SuggestedPriceEstimator) BumpLegacyGas(_ context.Context, _ *assets.Wei, _ uint32, _ *assets.Wei, _ []PriorAttempt) (bumpedGasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) { +func (o *l2SuggestedPriceEstimator) BumpLegacyGas(_ context.Context, _ *assets.Wei, _ uint32, _ *assets.Wei, _ []EvmPriorAttempt) (bumpedGasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) { return nil, 0, errors.New("bump gas is not supported for this l2") } diff --git a/core/chains/evm/gas/mocks/estimator.go b/core/chains/evm/gas/mocks/evm_estimator.go similarity index 68% rename from core/chains/evm/gas/mocks/estimator.go rename to core/chains/evm/gas/mocks/evm_estimator.go index f33da3f8519..f5a269de144 100644 --- a/core/chains/evm/gas/mocks/estimator.go +++ b/core/chains/evm/gas/mocks/evm_estimator.go @@ -7,41 +7,43 @@ import ( assets "github.com/smartcontractkit/chainlink/core/assets" + evmtypes "github.com/smartcontractkit/chainlink/core/chains/evm/types" + gas "github.com/smartcontractkit/chainlink/core/chains/evm/gas" mock "github.com/stretchr/testify/mock" - types "github.com/smartcontractkit/chainlink/core/chains/evm/types" + types "github.com/smartcontractkit/chainlink/common/txmgr/types" ) -// Estimator is an autogenerated mock type for the Estimator type -type Estimator struct { +// EvmEstimator is an autogenerated mock type for the EvmEstimator type +type EvmEstimator struct { mock.Mock } // BumpDynamicFee provides a mock function with given fields: ctx, original, gasLimit, maxGasPriceWei, attempts -func (_m *Estimator) BumpDynamicFee(ctx context.Context, original gas.DynamicFee, gasLimit uint32, maxGasPriceWei *assets.Wei, attempts []gas.PriorAttempt) (gas.DynamicFee, uint32, error) { +func (_m *EvmEstimator) BumpDynamicFee(ctx context.Context, original gas.DynamicFee, gasLimit uint32, maxGasPriceWei *assets.Wei, attempts []gas.EvmPriorAttempt) (gas.DynamicFee, uint32, error) { ret := _m.Called(ctx, original, gasLimit, maxGasPriceWei, attempts) var r0 gas.DynamicFee var r1 uint32 var r2 error - if rf, ok := ret.Get(0).(func(context.Context, gas.DynamicFee, uint32, *assets.Wei, []gas.PriorAttempt) (gas.DynamicFee, uint32, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, gas.DynamicFee, uint32, *assets.Wei, []gas.EvmPriorAttempt) (gas.DynamicFee, uint32, error)); ok { return rf(ctx, original, gasLimit, maxGasPriceWei, attempts) } - if rf, ok := ret.Get(0).(func(context.Context, gas.DynamicFee, uint32, *assets.Wei, []gas.PriorAttempt) gas.DynamicFee); ok { + if rf, ok := ret.Get(0).(func(context.Context, gas.DynamicFee, uint32, *assets.Wei, []gas.EvmPriorAttempt) gas.DynamicFee); ok { r0 = rf(ctx, original, gasLimit, maxGasPriceWei, attempts) } else { r0 = ret.Get(0).(gas.DynamicFee) } - if rf, ok := ret.Get(1).(func(context.Context, gas.DynamicFee, uint32, *assets.Wei, []gas.PriorAttempt) uint32); ok { + if rf, ok := ret.Get(1).(func(context.Context, gas.DynamicFee, uint32, *assets.Wei, []gas.EvmPriorAttempt) uint32); ok { r1 = rf(ctx, original, gasLimit, maxGasPriceWei, attempts) } else { r1 = ret.Get(1).(uint32) } - if rf, ok := ret.Get(2).(func(context.Context, gas.DynamicFee, uint32, *assets.Wei, []gas.PriorAttempt) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, gas.DynamicFee, uint32, *assets.Wei, []gas.EvmPriorAttempt) error); ok { r2 = rf(ctx, original, gasLimit, maxGasPriceWei, attempts) } else { r2 = ret.Error(2) @@ -51,16 +53,16 @@ func (_m *Estimator) BumpDynamicFee(ctx context.Context, original gas.DynamicFee } // BumpLegacyGas provides a mock function with given fields: ctx, originalGasPrice, gasLimit, maxGasPriceWei, attempts -func (_m *Estimator) BumpLegacyGas(ctx context.Context, originalGasPrice *assets.Wei, gasLimit uint32, maxGasPriceWei *assets.Wei, attempts []gas.PriorAttempt) (*assets.Wei, uint32, error) { +func (_m *EvmEstimator) BumpLegacyGas(ctx context.Context, originalGasPrice *assets.Wei, gasLimit uint32, maxGasPriceWei *assets.Wei, attempts []gas.EvmPriorAttempt) (*assets.Wei, uint32, error) { ret := _m.Called(ctx, originalGasPrice, gasLimit, maxGasPriceWei, attempts) var r0 *assets.Wei var r1 uint32 var r2 error - if rf, ok := ret.Get(0).(func(context.Context, *assets.Wei, uint32, *assets.Wei, []gas.PriorAttempt) (*assets.Wei, uint32, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *assets.Wei, uint32, *assets.Wei, []gas.EvmPriorAttempt) (*assets.Wei, uint32, error)); ok { return rf(ctx, originalGasPrice, gasLimit, maxGasPriceWei, attempts) } - if rf, ok := ret.Get(0).(func(context.Context, *assets.Wei, uint32, *assets.Wei, []gas.PriorAttempt) *assets.Wei); ok { + if rf, ok := ret.Get(0).(func(context.Context, *assets.Wei, uint32, *assets.Wei, []gas.EvmPriorAttempt) *assets.Wei); ok { r0 = rf(ctx, originalGasPrice, gasLimit, maxGasPriceWei, attempts) } else { if ret.Get(0) != nil { @@ -68,13 +70,13 @@ func (_m *Estimator) BumpLegacyGas(ctx context.Context, originalGasPrice *assets } } - if rf, ok := ret.Get(1).(func(context.Context, *assets.Wei, uint32, *assets.Wei, []gas.PriorAttempt) uint32); ok { + if rf, ok := ret.Get(1).(func(context.Context, *assets.Wei, uint32, *assets.Wei, []gas.EvmPriorAttempt) uint32); ok { r1 = rf(ctx, originalGasPrice, gasLimit, maxGasPriceWei, attempts) } else { r1 = ret.Get(1).(uint32) } - if rf, ok := ret.Get(2).(func(context.Context, *assets.Wei, uint32, *assets.Wei, []gas.PriorAttempt) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *assets.Wei, uint32, *assets.Wei, []gas.EvmPriorAttempt) error); ok { r2 = rf(ctx, originalGasPrice, gasLimit, maxGasPriceWei, attempts) } else { r2 = ret.Error(2) @@ -84,7 +86,7 @@ func (_m *Estimator) BumpLegacyGas(ctx context.Context, originalGasPrice *assets } // Close provides a mock function with given fields: -func (_m *Estimator) Close() error { +func (_m *EvmEstimator) Close() error { ret := _m.Called() var r0 error @@ -98,7 +100,7 @@ func (_m *Estimator) Close() error { } // GetDynamicFee provides a mock function with given fields: ctx, gasLimit, maxGasPriceWei -func (_m *Estimator) GetDynamicFee(ctx context.Context, gasLimit uint32, maxGasPriceWei *assets.Wei) (gas.DynamicFee, uint32, error) { +func (_m *EvmEstimator) GetDynamicFee(ctx context.Context, gasLimit uint32, maxGasPriceWei *assets.Wei) (gas.DynamicFee, uint32, error) { ret := _m.Called(ctx, gasLimit, maxGasPriceWei) var r0 gas.DynamicFee @@ -129,7 +131,7 @@ func (_m *Estimator) GetDynamicFee(ctx context.Context, gasLimit uint32, maxGasP } // GetLegacyGas provides a mock function with given fields: ctx, calldata, gasLimit, maxGasPriceWei, opts -func (_m *Estimator) GetLegacyGas(ctx context.Context, calldata []byte, gasLimit uint32, maxGasPriceWei *assets.Wei, opts ...gas.Opt) (*assets.Wei, uint32, error) { +func (_m *EvmEstimator) GetLegacyGas(ctx context.Context, calldata []byte, gasLimit uint32, maxGasPriceWei *assets.Wei, opts ...types.Opt) (*assets.Wei, uint32, error) { _va := make([]interface{}, len(opts)) for _i := range opts { _va[_i] = opts[_i] @@ -142,10 +144,10 @@ func (_m *Estimator) GetLegacyGas(ctx context.Context, calldata []byte, gasLimit var r0 *assets.Wei var r1 uint32 var r2 error - if rf, ok := ret.Get(0).(func(context.Context, []byte, uint32, *assets.Wei, ...gas.Opt) (*assets.Wei, uint32, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, []byte, uint32, *assets.Wei, ...types.Opt) (*assets.Wei, uint32, error)); ok { return rf(ctx, calldata, gasLimit, maxGasPriceWei, opts...) } - if rf, ok := ret.Get(0).(func(context.Context, []byte, uint32, *assets.Wei, ...gas.Opt) *assets.Wei); ok { + if rf, ok := ret.Get(0).(func(context.Context, []byte, uint32, *assets.Wei, ...types.Opt) *assets.Wei); ok { r0 = rf(ctx, calldata, gasLimit, maxGasPriceWei, opts...) } else { if ret.Get(0) != nil { @@ -153,13 +155,13 @@ func (_m *Estimator) GetLegacyGas(ctx context.Context, calldata []byte, gasLimit } } - if rf, ok := ret.Get(1).(func(context.Context, []byte, uint32, *assets.Wei, ...gas.Opt) uint32); ok { + if rf, ok := ret.Get(1).(func(context.Context, []byte, uint32, *assets.Wei, ...types.Opt) uint32); ok { r1 = rf(ctx, calldata, gasLimit, maxGasPriceWei, opts...) } else { r1 = ret.Get(1).(uint32) } - if rf, ok := ret.Get(2).(func(context.Context, []byte, uint32, *assets.Wei, ...gas.Opt) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, []byte, uint32, *assets.Wei, ...types.Opt) error); ok { r2 = rf(ctx, calldata, gasLimit, maxGasPriceWei, opts...) } else { r2 = ret.Error(2) @@ -169,12 +171,12 @@ func (_m *Estimator) GetLegacyGas(ctx context.Context, calldata []byte, gasLimit } // OnNewLongestChain provides a mock function with given fields: _a0, _a1 -func (_m *Estimator) OnNewLongestChain(_a0 context.Context, _a1 *types.Head) { +func (_m *EvmEstimator) OnNewLongestChain(_a0 context.Context, _a1 *evmtypes.Head) { _m.Called(_a0, _a1) } // Start provides a mock function with given fields: _a0 -func (_m *Estimator) Start(_a0 context.Context) error { +func (_m *EvmEstimator) Start(_a0 context.Context) error { ret := _m.Called(_a0) var r0 error @@ -187,14 +189,14 @@ func (_m *Estimator) Start(_a0 context.Context) error { return r0 } -type mockConstructorTestingTNewEstimator interface { +type mockConstructorTestingTNewEvmEstimator interface { mock.TestingT Cleanup(func()) } -// NewEstimator creates a new instance of Estimator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewEstimator(t mockConstructorTestingTNewEstimator) *Estimator { - mock := &Estimator{} +// NewEvmEstimator creates a new instance of EvmEstimator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewEvmEstimator(t mockConstructorTestingTNewEvmEstimator) *EvmEstimator { + mock := &EvmEstimator{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) diff --git a/core/chains/evm/gas/models.go b/core/chains/evm/gas/models.go index f650e5b2cae..0f45b46bd82 100644 --- a/core/chains/evm/gas/models.go +++ b/core/chains/evm/gas/models.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/shopspring/decimal" + txmgrtypes "github.com/smartcontractkit/chainlink/common/txmgr/types" "github.com/smartcontractkit/chainlink/core/assets" evmclient "github.com/smartcontractkit/chainlink/core/chains/evm/client" "github.com/smartcontractkit/chainlink/core/chains/evm/label" @@ -28,7 +29,7 @@ func IsBumpErr(err error) bool { } // NewEstimator returns the estimator for a given config -func NewEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg Config) Estimator { +func NewEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg Config) txmgrtypes.FeeEstimator[*evmtypes.Head, EvmFee, *assets.Wei, common.Hash] { s := cfg.GasEstimatorMode() lggr.Infow(fmt.Sprintf("Initializing EVM gas estimator in mode: %s", s), "estimatorMode", s, @@ -51,16 +52,16 @@ func NewEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg Config) Es ) switch s { case "Arbitrum": - return NewArbitrumEstimator(lggr, cfg, ethClient, ethClient) + return NewWrappedEvmEstimator(NewArbitrumEstimator(lggr, cfg, ethClient, ethClient), cfg) case "BlockHistory": - return NewBlockHistoryEstimator(lggr, ethClient, cfg, *ethClient.ChainID()) + return NewWrappedEvmEstimator(NewBlockHistoryEstimator(lggr, ethClient, cfg, *ethClient.ChainID()), cfg) case "FixedPrice": - return NewFixedPriceEstimator(cfg, lggr) + return NewWrappedEvmEstimator(NewFixedPriceEstimator(cfg, lggr), cfg) case "Optimism2", "L2Suggested": - return NewL2SuggestedPriceEstimator(lggr, ethClient) + return NewWrappedEvmEstimator(NewL2SuggestedPriceEstimator(lggr, ethClient), cfg) default: lggr.Warnf("GasEstimator: unrecognised mode '%s', falling back to FixedPriceEstimator", s) - return NewFixedPriceEstimator(cfg, lggr) + return NewWrappedEvmEstimator(NewFixedPriceEstimator(cfg, lggr), cfg) } } @@ -70,31 +71,57 @@ type DynamicFee struct { TipCap *assets.Wei } -type PriorAttempt interface { +type EvmPriorAttempt interface { + txmgrtypes.PriorAttempt[EvmFee, common.Hash] + GetGasPrice() *assets.Wei DynamicFee() DynamicFee - GetChainSpecificGasLimit() uint32 - GetBroadcastBeforeBlockNum() *int64 - GetHash() common.Hash - GetTxType() int +} + +type evmPriorAttempt struct { + txmgrtypes.PriorAttempt[EvmFee, common.Hash] +} + +func (e evmPriorAttempt) GetGasPrice() *assets.Wei { + return e.Fee().Legacy +} + +func (e evmPriorAttempt) DynamicFee() DynamicFee { + fee := e.Fee().Dynamic + if fee == nil { + return DynamicFee{} + } + return *fee +} + +func MakeEvmPriorAttempts(attempts []txmgrtypes.PriorAttempt[EvmFee, common.Hash]) (out []EvmPriorAttempt) { + for i := range attempts { + out = append(out, MakeEvmPriorAttempt(attempts[i])) + } + return out +} + +func MakeEvmPriorAttempt(a txmgrtypes.PriorAttempt[EvmFee, common.Hash]) EvmPriorAttempt { + e := evmPriorAttempt{a} + return &e } // Estimator provides an interface for estimating gas price and limit // -//go:generate mockery --quiet --name Estimator --output ./mocks/ --case=underscore -type Estimator interface { +//go:generate mockery --quiet --name EvmEstimator --output ./mocks/ --case=underscore +type EvmEstimator interface { OnNewLongestChain(context.Context, *evmtypes.Head) Start(context.Context) error Close() error // GetLegacyGas Calculates initial gas fee for non-EIP1559 transaction // maxGasPriceWei parameter is the highest possible gas fee cap that the function will return - GetLegacyGas(ctx context.Context, calldata []byte, gasLimit uint32, maxGasPriceWei *assets.Wei, opts ...Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) + GetLegacyGas(ctx context.Context, calldata []byte, gasLimit uint32, maxGasPriceWei *assets.Wei, opts ...txmgrtypes.Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) // BumpLegacyGas Increases gas price and/or limit for non-EIP1559 transactions // if the bumped gas fee is greater than maxGasPriceWei, the method returns an error // attempts must: // - be sorted in order from highest price to lowest price // - all be of transaction type 0x0 or 0x1 - BumpLegacyGas(ctx context.Context, originalGasPrice *assets.Wei, gasLimit uint32, maxGasPriceWei *assets.Wei, attempts []PriorAttempt) (bumpedGasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) + BumpLegacyGas(ctx context.Context, originalGasPrice *assets.Wei, gasLimit uint32, maxGasPriceWei *assets.Wei, attempts []EvmPriorAttempt) (bumpedGasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) // GetDynamicFee Calculates initial gas fee for gas for EIP1559 transactions // maxGasPriceWei parameter is the highest possible gas fee cap that the function will return GetDynamicFee(ctx context.Context, gasLimit uint32, maxGasPriceWei *assets.Wei) (fee DynamicFee, chainSpecificGasLimit uint32, err error) @@ -103,16 +130,67 @@ type Estimator interface { // attempts must: // - be sorted in order from highest price to lowest price // - all be of transaction type 0x2 - BumpDynamicFee(ctx context.Context, original DynamicFee, gasLimit uint32, maxGasPriceWei *assets.Wei, attempts []PriorAttempt) (bumped DynamicFee, chainSpecificGasLimit uint32, err error) + BumpDynamicFee(ctx context.Context, original DynamicFee, gasLimit uint32, maxGasPriceWei *assets.Wei, attempts []EvmPriorAttempt) (bumped DynamicFee, chainSpecificGasLimit uint32, err error) } -// Opt is an option for a gas estimator -type Opt int +type EvmFee struct { + Legacy *assets.Wei + Dynamic *DynamicFee +} -const ( - // OptForceRefetch forces the estimator to bust a cache if necessary - OptForceRefetch Opt = iota -) +// WrappedEvmEstimator provides a struct that wraps the EVM specific dynamic and legacy estimators into one estimator that conforms to the generic FeeEstimator +type WrappedEvmEstimator struct { + EvmEstimator + EIP1559Enabled bool +} + +// var _ FeeEstimator = (*WrappedEvmEstimator)(nil) +var _ txmgrtypes.FeeEstimator[*evmtypes.Head, EvmFee, *assets.Wei, common.Hash] = (*WrappedEvmEstimator)(nil) + +func NewWrappedEvmEstimator(e EvmEstimator, cfg Config) txmgrtypes.FeeEstimator[*evmtypes.Head, EvmFee, *assets.Wei, common.Hash] { + return &WrappedEvmEstimator{ + EvmEstimator: e, + EIP1559Enabled: cfg.EvmEIP1559DynamicFees(), + } +} + +func (e WrappedEvmEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint32, maxFeePrice *assets.Wei, opts ...txmgrtypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint32, err error) { + // get dynamic fee + if e.EIP1559Enabled { + var dynamicFee DynamicFee + dynamicFee, chainSpecificFeeLimit, err = e.EvmEstimator.GetDynamicFee(ctx, feeLimit, maxFeePrice) + fee.Dynamic = &dynamicFee + return + } + + // get legacy fee + fee.Legacy, chainSpecificFeeLimit, err = e.EvmEstimator.GetLegacyGas(ctx, calldata, feeLimit, maxFeePrice, opts...) + return +} + +func (e WrappedEvmEstimator) BumpFee(ctx context.Context, originalFee EvmFee, feeLimit uint32, maxFeePrice *assets.Wei, attempts []txmgrtypes.PriorAttempt[EvmFee, common.Hash]) (bumpedFee EvmFee, chainSpecificFeeLimit uint32, err error) { + // validate only 1 fee type is present + if (originalFee.Dynamic == nil && originalFee.Legacy == nil) || (originalFee.Dynamic != nil && originalFee.Legacy != nil) { + err = errors.New("only one dynamic or legacy fee can be defined") + return + } + + // convert PriorAttempts to EvmPriorAttempts + evmAttempts := MakeEvmPriorAttempts(attempts) + + // bump fee based on what fee the tx has previously used (not based on config) + // bump dynamic original + if originalFee.Dynamic != nil { + var bumpedDynamic DynamicFee + bumpedDynamic, chainSpecificFeeLimit, err = e.EvmEstimator.BumpDynamicFee(ctx, *originalFee.Dynamic, feeLimit, maxFeePrice, evmAttempts) + bumpedFee.Dynamic = &bumpedDynamic + return + } + + // bump legacy fee + bumpedFee.Legacy, chainSpecificFeeLimit, err = e.EvmEstimator.BumpLegacyGas(ctx, originalFee.Legacy, feeLimit, maxFeePrice, evmAttempts) + return +} func applyMultiplier(gasLimit uint32, multiplier float32) uint32 { return uint32(decimal.NewFromBigInt(big.NewInt(0).SetUint64(uint64(gasLimit)), 0).Mul(decimal.NewFromFloat32(multiplier)).IntPart()) diff --git a/core/chains/evm/gas/models_test.go b/core/chains/evm/gas/models_test.go index 0a836004d03..61fc5c3d211 100644 --- a/core/chains/evm/gas/models_test.go +++ b/core/chains/evm/gas/models_test.go @@ -1 +1,91 @@ -package gas +package gas_test + +import ( + "context" + "testing" + + "github.com/smartcontractkit/chainlink/core/assets" + "github.com/smartcontractkit/chainlink/core/chains/evm/gas" + "github.com/smartcontractkit/chainlink/core/chains/evm/gas/mocks" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestWrappedEvmEstimator(t *testing.T) { + t.Parallel() + ctx := context.Background() + + // fee values + gasLimit := uint32(10) + legacyFee := assets.NewWeiI(10) + dynamicFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(21), + } + + cfg := mocks.NewConfig(t) + e := mocks.NewEvmEstimator(t) + e.On("GetDynamicFee", mock.Anything, mock.Anything, mock.Anything). + Return(dynamicFee, gasLimit, nil).Once() + e.On("GetLegacyGas", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(legacyFee, gasLimit, nil).Once() + e.On("BumpDynamicFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(dynamicFee, gasLimit, nil).Once() + e.On("BumpLegacyGas", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(legacyFee, gasLimit, nil).Once() + + // GetFee returns gas estimation based on configuration value + t.Run("GetFee", func(t *testing.T) { + // expect legacy fee data + cfg.On("EvmEIP1559DynamicFees").Return(false).Once() + estimator := gas.NewWrappedEvmEstimator(e, cfg) + fee, max, err := estimator.GetFee(ctx, nil, 0, nil) + require.NoError(t, err) + assert.Equal(t, gasLimit, max) + assert.True(t, legacyFee.Equal(fee.Legacy)) + assert.Nil(t, fee.Dynamic) + + // expect dynamic fee data + cfg.On("EvmEIP1559DynamicFees").Return(true).Once() + estimator = gas.NewWrappedEvmEstimator(e, cfg) + fee, max, err = estimator.GetFee(ctx, nil, 0, nil) + require.NoError(t, err) + assert.Equal(t, gasLimit, max) + assert.True(t, dynamicFee.FeeCap.Equal(fee.Dynamic.FeeCap)) + assert.True(t, dynamicFee.TipCap.Equal(fee.Dynamic.TipCap)) + assert.Nil(t, fee.Legacy) + }) + + // BumpFee returns bumped fee type based on original fee calculation + t.Run("BumpFee", func(t *testing.T) { + cfg.On("EvmEIP1559DynamicFees").Return(false).Once().Maybe() + estimator := gas.NewWrappedEvmEstimator(e, cfg) + + // expect legacy fee data + fee, max, err := estimator.BumpFee(ctx, gas.EvmFee{Legacy: assets.NewWeiI(0)}, 0, nil, nil) + require.NoError(t, err) + assert.Equal(t, gasLimit, max) + assert.True(t, legacyFee.Equal(fee.Legacy)) + assert.Nil(t, fee.Dynamic) + + // expect dynamic fee data + var d gas.DynamicFee + fee, max, err = estimator.BumpFee(ctx, gas.EvmFee{Dynamic: &d}, 0, nil, nil) + require.NoError(t, err) + assert.Equal(t, gasLimit, max) + assert.True(t, dynamicFee.FeeCap.Equal(fee.Dynamic.FeeCap)) + assert.True(t, dynamicFee.TipCap.Equal(fee.Dynamic.TipCap)) + assert.Nil(t, fee.Legacy) + + // expect error + _, _, err = estimator.BumpFee(ctx, gas.EvmFee{}, 0, nil, nil) + assert.Error(t, err) + _, _, err = estimator.BumpFee(ctx, gas.EvmFee{ + Legacy: legacyFee, + Dynamic: &dynamicFee, + }, 0, nil, nil) + assert.Error(t, err) + }) +} \ No newline at end of file diff --git a/core/chains/evm/txmgr/eth_broadcaster.go b/core/chains/evm/txmgr/eth_broadcaster.go index 79d6300c084..6e0ad21393e 100644 --- a/core/chains/evm/txmgr/eth_broadcaster.go +++ b/core/chains/evm/txmgr/eth_broadcaster.go @@ -17,10 +17,12 @@ import ( "go.uber.org/multierr" "gopkg.in/guregu/null.v4" + txmgrtypes "github.com/smartcontractkit/chainlink/common/txmgr/types" "github.com/smartcontractkit/chainlink/core/assets" evmclient "github.com/smartcontractkit/chainlink/core/chains/evm/client" "github.com/smartcontractkit/chainlink/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/core/chains/evm/label" + evmtypes "github.com/smartcontractkit/chainlink/core/chains/evm/types" "github.com/smartcontractkit/chainlink/core/logger" "github.com/smartcontractkit/chainlink/core/services/keystore/keys/ethkey" "github.com/smartcontractkit/chainlink/core/services/pg" @@ -90,7 +92,7 @@ type EthBroadcaster struct { orm ORM ethClient evmclient.Client ChainKeyStore - estimator gas.Estimator + estimator txmgrtypes.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, gethCommon.Hash] resumeCallback ResumeCallback // autoSyncNonce, if set, will cause EthBroadcaster to fast-forward the nonce @@ -118,7 +120,7 @@ type EthBroadcaster struct { // NewEthBroadcaster returns a new concrete EthBroadcaster func NewEthBroadcaster(orm ORM, ethClient evmclient.Client, config Config, keystore KeyStore, eventBroadcaster pg.EventBroadcaster, - keyStates []ethkey.State, estimator gas.Estimator, resumeCallback ResumeCallback, + keyStates []ethkey.State, estimator txmgrtypes.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, gethCommon.Hash], resumeCallback ResumeCallback, logger logger.Logger, checkerFactory TransmitCheckerFactory, autoSyncNonce bool) *EthBroadcaster { triggers := make(map[gethCommon.Address]chan struct{}) @@ -378,21 +380,18 @@ func (eb *EthBroadcaster) processUnstartedEthTxs(ctx context.Context, fromAddres n++ var a EthTxAttempt keySpecificMaxGasPriceWei := eb.config.KeySpecificMaxGasPriceWei(etx.FromAddress) + fee, gasLimit, err := eb.estimator.GetFee(ctx, etx.EncodedPayload, etx.GasLimit, keySpecificMaxGasPriceWei) + if err != nil { + return errors.Wrap(err, "failed to get fee"), true + } + if eb.config.EvmEIP1559DynamicFees() { - fee, gasLimit, err := eb.estimator.GetDynamicFee(ctx, etx.GasLimit, keySpecificMaxGasPriceWei) - if err != nil { - return errors.Wrap(err, "failed to get dynamic gas fee"), true - } - a, err = eb.NewDynamicFeeAttempt(*etx, fee, gasLimit) + a, err = eb.NewDynamicFeeAttempt(*etx, *fee.Dynamic, gasLimit) if err != nil { return errors.Wrap(err, "processUnstartedEthTxs failed on NewDynamicFeeAttempt"), true } } else { - gasPrice, gasLimit, err := eb.estimator.GetLegacyGas(ctx, etx.EncodedPayload, etx.GasLimit, keySpecificMaxGasPriceWei) - if err != nil { - return errors.Wrap(err, "failed to estimate gas"), true - } - a, err = eb.NewLegacyAttempt(*etx, gasPrice, gasLimit) + a, err = eb.NewLegacyAttempt(*etx, fee.Legacy, gasLimit) if err != nil { return errors.Wrap(err, "processUnstartedEthTxs failed on NewLegacyAttempt"), true } @@ -683,11 +682,23 @@ func (eb *EthBroadcaster) tryAgainBumpingGas(ctx context.Context, lgr logger.Log "Will bump and retry. ACTION REQUIRED: This is a configuration error. "+ "Consider increasing ETH_GAS_PRICE_DEFAULT (current value: %s)", attempt.GasPrice, sendError.Error(), eb.config.EvmGasPriceDefault().String()) + + keySpecificMaxGasPriceWei := eb.config.KeySpecificMaxGasPriceWei(etx.FromAddress) + bumpedFee, bumpedFeeLimit, err := eb.estimator.BumpFee(ctx, attempt.Fee(), etx.GasLimit, keySpecificMaxGasPriceWei, nil) + if err != nil { + return errors.Wrap(err, "tryAgainBumpFee failed"), true + } + switch attempt.TxType { case 0x0: - return eb.tryAgainBumpingLegacyGas(ctx, lgr, etx, attempt, initialBroadcastAt) + return eb.tryAgainWithNewLegacyGas(ctx, lgr, etx, attempt, initialBroadcastAt, bumpedFee.Legacy, bumpedFeeLimit) case 0x2: - return eb.tryAgainBumpingDynamicFeeGas(ctx, lgr, etx, attempt, initialBroadcastAt) + if bumpedFee.Dynamic == nil { + err = errors.Errorf("Attempt %v is a type 2 transaction but estimator did not return dynamic fee bump", attempt.ID) + logger.Sugared(eb.logger).AssumptionViolation(err.Error()) + return err, false + } + return eb.tryAgainWithNewDynamicFeeGas(ctx, lgr, etx, attempt, initialBroadcastAt, *bumpedFee.Dynamic, bumpedFeeLimit) default: err = errors.Errorf("invariant violation: Attempt %v had unrecognised transaction type %v"+ "This is a bug! Please report to https://github.com/smartcontractkit/chainlink/issues", attempt.ID, attempt.TxType) @@ -696,30 +707,6 @@ func (eb *EthBroadcaster) tryAgainBumpingGas(ctx context.Context, lgr logger.Log } } -func (eb *EthBroadcaster) tryAgainBumpingLegacyGas(ctx context.Context, lgr logger.Logger, etx EthTx, attempt EthTxAttempt, initialBroadcastAt time.Time) (err error, retryable bool) { - keySpecificMaxGasPriceWei := eb.config.KeySpecificMaxGasPriceWei(etx.FromAddress) - bumpedGasPrice, bumpedGasLimit, err := eb.estimator.BumpLegacyGas(ctx, attempt.GasPrice, etx.GasLimit, keySpecificMaxGasPriceWei, nil) - if err != nil { - return errors.Wrap(err, "tryAgainBumpingLegacyGas failed"), true - } - if bumpedGasPrice.Cmp(attempt.GasPrice) == 0 || bumpedGasPrice.Cmp(eb.config.EvmMaxGasPriceWei()) >= 0 { - return errors.Errorf("hit gas price bump ceiling, will not bump further"), true // TODO: Is this terminal or retryable? Is it possible to send unsaved attempts here? - } - return eb.tryAgainWithNewLegacyGas(ctx, lgr, etx, attempt, initialBroadcastAt, bumpedGasPrice, bumpedGasLimit) -} - -func (eb *EthBroadcaster) tryAgainBumpingDynamicFeeGas(ctx context.Context, lgr logger.Logger, etx EthTx, attempt EthTxAttempt, initialBroadcastAt time.Time) (err error, retryable bool) { - keySpecificMaxGasPriceWei := eb.config.KeySpecificMaxGasPriceWei(etx.FromAddress) - bumpedFee, bumpedGasLimit, err := eb.estimator.BumpDynamicFee(ctx, attempt.DynamicFee(), etx.GasLimit, keySpecificMaxGasPriceWei, nil) - if err != nil { - return errors.Wrap(err, "tryAgainBumpingDynamicFeeGas failed"), true - } - if bumpedFee.TipCap.Cmp(attempt.GasTipCap) == 0 || bumpedFee.FeeCap.Cmp(attempt.GasFeeCap) == 0 || bumpedFee.TipCap.Cmp(eb.config.EvmMaxGasPriceWei()) >= 0 || bumpedFee.TipCap.Cmp(eb.config.EvmMaxGasPriceWei()) >= 0 { - return errors.Errorf("hit gas price bump ceiling, will not bump further"), true // TODO: Is this terminal or retryable? Is it possible to send unsaved attempts here? - } - return eb.tryAgainWithNewDynamicFeeGas(ctx, lgr, etx, attempt, initialBroadcastAt, bumpedFee, bumpedGasLimit) -} - func (eb *EthBroadcaster) tryAgainWithNewEstimation(ctx context.Context, lgr logger.Logger, sendError *evmclient.SendError, etx EthTx, attempt EthTxAttempt, initialBroadcastAt time.Time) (err error, retryable bool) { if attempt.TxType == 0x2 { err = errors.Errorf("re-estimation is not supported for EIP-1559 transactions. Eth node returned error: %v. This is a bug", sendError.Error()) @@ -727,13 +714,15 @@ func (eb *EthBroadcaster) tryAgainWithNewEstimation(ctx context.Context, lgr log return err, false } keySpecificMaxGasPriceWei := eb.config.KeySpecificMaxGasPriceWei(etx.FromAddress) - gasPrice, gasLimit, err := eb.estimator.GetLegacyGas(ctx, etx.EncodedPayload, etx.GasLimit, keySpecificMaxGasPriceWei, gas.OptForceRefetch) + gasPrice, gasLimit, err := eb.estimator.GetFee(ctx, etx.EncodedPayload, etx.GasLimit, keySpecificMaxGasPriceWei, txmgrtypes.OptForceRefetch) if err != nil { return errors.Wrap(err, "tryAgainWithNewEstimation failed to estimate gas"), true } lgr.Warnw("L2 rejected transaction due to incorrect fee, re-estimated and will try again", "etxID", etx.ID, "err", err, "newGasPrice", gasPrice, "newGasLimit", gasLimit) - return eb.tryAgainWithNewLegacyGas(ctx, lgr, etx, attempt, initialBroadcastAt, gasPrice, gasLimit) + + // TODO: nil handling for gasPrice.Legacy? will this ever be reached where EIP1559 is enabled on a L2 but a legacy tx? + return eb.tryAgainWithNewLegacyGas(ctx, lgr, etx, attempt, initialBroadcastAt, gasPrice.Legacy, gasLimit) } func (eb *EthBroadcaster) tryAgainWithNewLegacyGas(ctx context.Context, lgr logger.Logger, etx EthTx, attempt EthTxAttempt, initialBroadcastAt time.Time, newGasPrice *assets.Wei, newGasLimit uint32) (err error, retyrable bool) { @@ -758,7 +747,7 @@ func (eb *EthBroadcaster) tryAgainWithNewDynamicFeeGas(ctx context.Context, lgr if err = eb.orm.SaveReplacementInProgressAttempt(attempt, &replacementAttempt); err != nil { return errors.Wrap(err, "tryAgainWithNewDynamicFeeGas failed"), true } - lgr.Debugw("Bumped dynamic fee gas on initial send", "oldFee", attempt.DynamicFee(), "newFee", newDynamicFee) + lgr.Debugw("Bumped dynamic fee gas on initial send", "oldFee", gas.MakeEvmPriorAttempt(attempt).DynamicFee(), "newFee", newDynamicFee) return eb.handleInProgressEthTx(ctx, etx, replacementAttempt, initialBroadcastAt) } diff --git a/core/chains/evm/txmgr/eth_broadcaster_test.go b/core/chains/evm/txmgr/eth_broadcaster_test.go index 29b552911c4..ea0291eb976 100644 --- a/core/chains/evm/txmgr/eth_broadcaster_test.go +++ b/core/chains/evm/txmgr/eth_broadcaster_test.go @@ -23,11 +23,12 @@ import ( "go.uber.org/zap/zapcore" "gopkg.in/guregu/null.v4" + txmgrmocks "github.com/smartcontractkit/chainlink/common/txmgr/types/mocks" "github.com/smartcontractkit/chainlink/core/assets" evmclient "github.com/smartcontractkit/chainlink/core/chains/evm/client" "github.com/smartcontractkit/chainlink/core/chains/evm/gas" - gasmocks "github.com/smartcontractkit/chainlink/core/chains/evm/gas/mocks" "github.com/smartcontractkit/chainlink/core/chains/evm/txmgr" + evmtypes "github.com/smartcontractkit/chainlink/core/chains/evm/types" "github.com/smartcontractkit/chainlink/core/internal/cltest" "github.com/smartcontractkit/chainlink/core/internal/cltest/heavyweight" "github.com/smartcontractkit/chainlink/core/internal/testutils" @@ -576,8 +577,8 @@ func TestEthBroadcaster_ProcessUnstartedEthTxs_OptimisticLockingOnEthTx(t *testi chStartEstimate := make(chan struct{}) chBlock := make(chan struct{}) - estimator := gasmocks.NewEstimator(t) - estimator.On("GetLegacyGas", mock.Anything, mock.Anything, mock.Anything, evmcfg.KeySpecificMaxGasPriceWei(fromAddress)).Return(assets.GWei(32), uint32(500), nil).Run(func(_ mock.Arguments) { + estimator := txmgrmocks.NewFeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, gethCommon.Hash](t) + estimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, evmcfg.KeySpecificMaxGasPriceWei(fromAddress)).Return(gas.EvmFee{Legacy: assets.GWei(32)}, uint32(500), nil).Run(func(_ mock.Arguments) { close(chStartEstimate) <-chBlock }) @@ -1140,7 +1141,7 @@ func TestEthBroadcaster_ProcessUnstartedEthTxs_Errors(t *testing.T) { t.Cleanup(func() { assert.NoError(t, eventBroadcaster.Close()) }) lggr := logger.TestLogger(t) eb = txmgr.NewEthBroadcaster(borm, ethClient, evmcfg, ethKeyStore, eventBroadcaster, - []ethkey.State{keyState}, gas.NewFixedPriceEstimator(evmcfg, lggr), fn, lggr, + []ethkey.State{keyState}, gas.NewWrappedEvmEstimator(gas.NewFixedPriceEstimator(evmcfg, lggr), evmcfg), fn, lggr, &testCheckerFactory{}, false) { @@ -1889,7 +1890,7 @@ func TestEthBroadcaster_SyncNonce(t *testing.T) { sub.On("Events").Return(make(<-chan pg.Event)) sub.On("Close") eventBroadcaster.On("Subscribe", "insert_on_eth_txes", "").Return(sub, nil) - estimator := gas.NewFixedPriceEstimator(evmcfg, lggr) + estimator := gas.NewWrappedEvmEstimator(gas.NewFixedPriceEstimator(evmcfg, lggr), evmcfg) checkerFactory := &testCheckerFactory{} t.Run("does nothing if nonce sync is disabled", func(t *testing.T) { diff --git a/core/chains/evm/txmgr/eth_confirmer.go b/core/chains/evm/txmgr/eth_confirmer.go index ea642db7571..93b66f7b8df 100644 --- a/core/chains/evm/txmgr/eth_confirmer.go +++ b/core/chains/evm/txmgr/eth_confirmer.go @@ -18,6 +18,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promauto" "go.uber.org/multierr" + txmgrtypes "github.com/smartcontractkit/chainlink/common/txmgr/types" "github.com/smartcontractkit/chainlink/core/assets" evmclient "github.com/smartcontractkit/chainlink/core/chains/evm/client" "github.com/smartcontractkit/chainlink/core/chains/evm/gas" @@ -117,7 +118,7 @@ type EthConfirmer struct { lggr logger.Logger ethClient evmclient.Client ChainKeyStore - estimator gas.Estimator + estimator txmgrtypes.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, gethCommon.Hash] resumeCallback ResumeCallback keyStates []ethkey.State @@ -132,7 +133,7 @@ type EthConfirmer struct { // NewEthConfirmer instantiates a new eth confirmer func NewEthConfirmer(orm ORM, ethClient evmclient.Client, config Config, keystore KeyStore, - keyStates []ethkey.State, estimator gas.Estimator, resumeCallback ResumeCallback, lggr logger.Logger) *EthConfirmer { + keyStates []ethkey.State, estimator txmgrtypes.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, gethCommon.Hash], resumeCallback ResumeCallback, lggr logger.Logger) *EthConfirmer { ctx, cancel := context.WithCancel(context.Background()) lggr = lggr.Named("EthConfirmer") @@ -758,7 +759,8 @@ func (ec *EthConfirmer) logFieldsPreviousAttempt(attempt EthTxAttempt) []interfa } func (ec *EthConfirmer) bumpGas(ctx context.Context, etx EthTx, previousAttempts []EthTxAttempt) (bumpedAttempt EthTxAttempt, err error) { - priorAttempts := make([]gas.PriorAttempt, len(previousAttempts)) + // TODO: once generics are introduced at the top level struct (EthConfirmer) remove the chain-specific typings + priorAttempts := make([]txmgrtypes.PriorAttempt[gas.EvmFee, gethCommon.Hash], len(previousAttempts)) // This feels a bit useless but until we get iterators there is no other // way to cast an array of structs to an array of interfaces for i, attempt := range previousAttempts { @@ -767,29 +769,28 @@ func (ec *EthConfirmer) bumpGas(ctx context.Context, etx EthTx, previousAttempts previousAttempt := previousAttempts[0] logFields := ec.logFieldsPreviousAttempt(previousAttempt) keySpecificMaxGasPriceWei := ec.config.KeySpecificMaxGasPriceWei(etx.FromAddress) - switch previousAttempt.TxType { - case 0x0: // Legacy - var bumpedGasPrice *assets.Wei - var bumpedGasLimit uint32 - bumpedGasPrice, bumpedGasLimit, err = ec.estimator.BumpLegacyGas(ctx, previousAttempt.GasPrice, etx.GasLimit, keySpecificMaxGasPriceWei, priorAttempts) - if err == nil { - promNumGasBumps.WithLabelValues(ec.chainID.String()).Inc() - ec.lggr.Debugw("Rebroadcast bumping gas for Legacy tx", append(logFields, "bumpedGasPrice", bumpedGasPrice.String())...) - return ec.NewLegacyAttempt(etx, bumpedGasPrice, bumpedGasLimit) - } - case 0x2: // EIP1559 - var bumpedFee gas.DynamicFee - var bumpedGasLimit uint32 - original := previousAttempt.DynamicFee() - bumpedFee, bumpedGasLimit, err = ec.estimator.BumpDynamicFee(ctx, original, etx.GasLimit, keySpecificMaxGasPriceWei, priorAttempts) - if err == nil { - promNumGasBumps.WithLabelValues(ec.chainID.String()).Inc() - ec.lggr.Debugw("Rebroadcast bumping gas for DynamicFee tx", append(logFields, "bumpedTipCap", bumpedFee.TipCap.String(), "bumpedFeeCap", bumpedFee.FeeCap.String())...) - return ec.NewDynamicFeeAttempt(etx, bumpedFee, bumpedGasLimit) + + var bumpedFee gas.EvmFee + var bumpedFeeLimit uint32 + bumpedFee, bumpedFeeLimit, err = ec.estimator.BumpFee(ctx, previousAttempt.Fee(), etx.GasLimit, keySpecificMaxGasPriceWei, priorAttempts) + + if err == nil { + promNumGasBumps.WithLabelValues(ec.chainID.String()).Inc() + switch previousAttempt.TxType { + case 0x0: // Legacy + ec.lggr.Debugw("Rebroadcast bumping gas for Legacy tx", append(logFields, "bumpedGasPrice", bumpedFee.Legacy.String())...) + return ec.NewLegacyAttempt(etx, bumpedFee.Legacy, bumpedFeeLimit) + case 0x2: // EIP1559 + ec.lggr.Debugw("Rebroadcast bumping gas for DynamicFee tx", append(logFields, "bumpedTipCap", bumpedFee.Dynamic.TipCap.String(), "bumpedFeeCap", bumpedFee.Dynamic.FeeCap.String())...) + if bumpedFee.Dynamic == nil { + err = errors.Errorf("Attempt %v is a type 2 transaction but estimator did not return dynamic fee bump", previousAttempt.ID) + } else { + return ec.NewDynamicFeeAttempt(etx, *bumpedFee.Dynamic, bumpedFeeLimit) + } + default: + err = errors.Errorf("invariant violation: Attempt %v had unrecognised transaction type %v"+ + "This is a bug! Please report to https://github.com/smartcontractkit/chainlink/issues", previousAttempt.ID, previousAttempt.TxType) } - default: - err = errors.Errorf("invariant violation: Attempt %v had unrecognised transaction type %v"+ - "This is a bug! Please report to https://github.com/smartcontractkit/chainlink/issues", previousAttempt.ID, previousAttempt.TxType) } if errors.Is(errors.Cause(err), gas.ErrBumpGasExceedsLimit) { diff --git a/core/chains/evm/txmgr/mocks/tx_manager.go b/core/chains/evm/txmgr/mocks/tx_manager.go index 304f13f31d7..435b6df63c5 100644 --- a/core/chains/evm/txmgr/mocks/tx_manager.go +++ b/core/chains/evm/txmgr/mocks/tx_manager.go @@ -11,6 +11,8 @@ import ( context "context" + evmtypes "github.com/smartcontractkit/chainlink/core/chains/evm/types" + gas "github.com/smartcontractkit/chainlink/core/chains/evm/gas" mock "github.com/stretchr/testify/mock" @@ -19,7 +21,7 @@ import ( txmgr "github.com/smartcontractkit/chainlink/core/chains/evm/txmgr" - types "github.com/smartcontractkit/chainlink/core/chains/evm/types" + types "github.com/smartcontractkit/chainlink/common/txmgr/types" ) // TxManager is an autogenerated mock type for the TxManager type @@ -99,15 +101,15 @@ func (_m *TxManager) GetForwarderForEOA(eoa common.Address) (common.Address, err } // GetGasEstimator provides a mock function with given fields: -func (_m *TxManager) GetGasEstimator() gas.Estimator { +func (_m *TxManager) GetGasEstimator() types.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, common.Hash] { ret := _m.Called() - var r0 gas.Estimator - if rf, ok := ret.Get(0).(func() gas.Estimator); ok { + var r0 types.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, common.Hash] + if rf, ok := ret.Get(0).(func() types.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, common.Hash]); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(gas.Estimator) + r0 = ret.Get(0).(types.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, common.Hash]) } } @@ -159,7 +161,7 @@ func (_m *TxManager) Name() string { } // OnNewLongestChain provides a mock function with given fields: ctx, head -func (_m *TxManager) OnNewLongestChain(ctx context.Context, head *types.Head) { +func (_m *TxManager) OnNewLongestChain(ctx context.Context, head *evmtypes.Head) { _m.Called(ctx, head) } diff --git a/core/chains/evm/txmgr/models.go b/core/chains/evm/txmgr/models.go index 2b4617c1f05..36ee8aba155 100644 --- a/core/chains/evm/txmgr/models.go +++ b/core/chains/evm/txmgr/models.go @@ -16,6 +16,7 @@ import ( uuid "github.com/satori/go.uuid" "gopkg.in/guregu/null.v4" + txmgrtypes "github.com/smartcontractkit/chainlink/common/txmgr/types" "github.com/smartcontractkit/chainlink/core/assets" "github.com/smartcontractkit/chainlink/core/chains/evm/gas" evmtypes "github.com/smartcontractkit/chainlink/core/chains/evm/types" @@ -279,7 +280,7 @@ func (e EthTx) GetChecker() (TransmitCheckerSpec, error) { return t, errors.Wrap(json.Unmarshal(*e.TransmitChecker, &t), "unmarshalling transmit checker") } -var _ gas.PriorAttempt = EthTxAttempt{} +var _ txmgrtypes.PriorAttempt[gas.EvmFee, common.Hash] = EthTxAttempt{} type EthTxAttempt struct { ID int64 @@ -311,7 +312,18 @@ func (a EthTxAttempt) GetSignedTx() (*types.Transaction, error) { return signedTx, nil } -func (a EthTxAttempt) DynamicFee() gas.DynamicFee { +func (a EthTxAttempt) Fee() (fee gas.EvmFee) { + fee.Legacy = a.getGasPrice() + + dynamic := a.dynamicFee() + // add dynamic struct only if values are not nil + if dynamic.FeeCap != nil && dynamic.TipCap != nil { + fee.Dynamic = &dynamic + } + return fee +} + +func (a EthTxAttempt) dynamicFee() gas.DynamicFee { return gas.DynamicFee{ FeeCap: a.GasFeeCap, TipCap: a.GasTipCap, @@ -326,7 +338,7 @@ func (a EthTxAttempt) GetChainSpecificGasLimit() uint32 { return a.ChainSpecificGasLimit } -func (a EthTxAttempt) GetGasPrice() *assets.Wei { +func (a EthTxAttempt) getGasPrice() *assets.Wei { return a.GasPrice } diff --git a/core/chains/evm/txmgr/txmgr.go b/core/chains/evm/txmgr/txmgr.go index eb2d6f09889..fae978992ee 100644 --- a/core/chains/evm/txmgr/txmgr.go +++ b/core/chains/evm/txmgr/txmgr.go @@ -14,6 +14,7 @@ import ( uuid "github.com/satori/go.uuid" "github.com/smartcontractkit/sqlx" + txmgrtypes "github.com/smartcontractkit/chainlink/common/txmgr/types" "github.com/smartcontractkit/chainlink/core/assets" evmclient "github.com/smartcontractkit/chainlink/core/chains/evm/client" "github.com/smartcontractkit/chainlink/core/chains/evm/forwarders" @@ -77,7 +78,7 @@ type TxManager interface { Trigger(addr common.Address) CreateEthTransaction(newTx NewTx, qopts ...pg.QOpt) (etx EthTx, err error) GetForwarderForEOA(eoa common.Address) (forwarder common.Address, err error) - GetGasEstimator() gas.Estimator + GetGasEstimator() txmgrtypes.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, common.Hash] RegisterResumeCallback(fn ResumeCallback) SendEther(chainID *big.Int, from, to common.Address, value assets.Eth, gasLimit uint32) (etx EthTx, err error) Reset(f func(), addr common.Address, abandon bool) error @@ -102,7 +103,7 @@ type Txm struct { config Config keyStore KeyStore eventBroadcaster pg.EventBroadcaster - gasEstimator gas.Estimator + gasEstimator txmgrtypes.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, common.Hash] chainID big.Int checkerFactory TransmitCheckerFactory @@ -520,7 +521,7 @@ func (b *Txm) checkEnabled(addr common.Address) error { } // GetGasEstimator returns the gas estimator, mostly useful for tests -func (b *Txm) GetGasEstimator() gas.Estimator { +func (b *Txm) GetGasEstimator() txmgrtypes.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, common.Hash] { return b.gasEstimator } @@ -647,9 +648,11 @@ func (n *NullTxManager) Reset(f func(), addr common.Address, abandon bool) error func (n *NullTxManager) SendEther(chainID *big.Int, from, to common.Address, value assets.Eth, gasLimit uint32) (etx EthTx, err error) { return etx, errors.New(n.ErrMsg) } -func (n *NullTxManager) Healthy() error { return nil } -func (n *NullTxManager) Ready() error { return nil } -func (n *NullTxManager) Name() string { return "" } -func (n *NullTxManager) HealthReport() map[string]error { return nil } -func (n *NullTxManager) GetGasEstimator() gas.Estimator { return nil } +func (n *NullTxManager) Healthy() error { return nil } +func (n *NullTxManager) Ready() error { return nil } +func (n *NullTxManager) Name() string { return "" } +func (n *NullTxManager) HealthReport() map[string]error { return nil } +func (n *NullTxManager) GetGasEstimator() txmgrtypes.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, common.Hash] { + return nil +} func (n *NullTxManager) RegisterResumeCallback(fn ResumeCallback) {} diff --git a/core/chains/evm/txmgr/txmgr_test.go b/core/chains/evm/txmgr/txmgr_test.go index 4e4fdcdc35c..cb7e65d7f7f 100644 --- a/core/chains/evm/txmgr/txmgr_test.go +++ b/core/chains/evm/txmgr/txmgr_test.go @@ -322,7 +322,7 @@ func newMockConfig(t *testing.T) *txmmocks.Config { cfg.On("BlockHistoryEstimatorBlockHistorySize").Return(uint16(42)).Maybe().Once() cfg.On("BlockHistoryEstimatorEIP1559FeeCapBufferBlocks").Return(uint16(42)).Maybe().Once() cfg.On("BlockHistoryEstimatorTransactionPercentile").Return(uint16(42)).Maybe().Once() - cfg.On("EvmEIP1559DynamicFees").Return(false).Maybe().Once() + cfg.On("EvmEIP1559DynamicFees").Return(false).Maybe().Twice() cfg.On("EvmGasBumpPercent").Return(uint16(42)).Maybe().Once() cfg.On("EvmGasBumpThreshold").Return(uint64(42)).Maybe() cfg.On("EvmGasBumpWei").Return(assets.NewWeiI(42)).Maybe().Once() diff --git a/core/internal/cltest/cltest.go b/core/internal/cltest/cltest.go index c1ded0e36ea..510886c08ef 100644 --- a/core/internal/cltest/cltest.go +++ b/core/internal/cltest/cltest.go @@ -208,7 +208,7 @@ func NewEthBroadcaster(t testing.TB, orm txmgr.ORM, ethClient evmclient.Client, t.Cleanup(func() { assert.NoError(t, eventBroadcaster.Close()) }) lggr := logger.TestLogger(t) return txmgr.NewEthBroadcaster(orm, ethClient, config, keyStore, eventBroadcaster, - keyStates, gas.NewFixedPriceEstimator(config, lggr), nil, lggr, + keyStates, gas.NewWrappedEvmEstimator(gas.NewFixedPriceEstimator(config, lggr), config), nil, lggr, checkerFactory, nonceAutoSync) } @@ -221,7 +221,7 @@ func NewEthConfirmer(t testing.TB, orm txmgr.ORM, ethClient evmclient.Client, co t.Helper() lggr := logger.TestLogger(t) ec := txmgr.NewEthConfirmer(orm, ethClient, config, ks, keyStates, - gas.NewFixedPriceEstimator(config, lggr), fn, lggr) + gas.NewWrappedEvmEstimator(gas.NewFixedPriceEstimator(config, lggr), config), fn, lggr) return ec } diff --git a/core/internal/features/features_test.go b/core/internal/features/features_test.go index dec113f11b6..50250b642d0 100644 --- a/core/internal/features/features_test.go +++ b/core/internal/features/features_test.go @@ -1379,10 +1379,10 @@ func TestIntegration_BlockHistoryEstimator(t *testing.T) { chain := evmtest.MustGetDefaultChain(t, cc) estimator := chain.TxManager().GetGasEstimator() - gasPrice, gasLimit, err := estimator.GetLegacyGas(testutils.Context(t), nil, 500_000, maxGasPrice) + gasPrice, gasLimit, err := estimator.GetFee(testutils.Context(t), nil, 500_000, maxGasPrice) require.NoError(t, err) assert.Equal(t, uint32(500000), gasLimit) - assert.Equal(t, "41.5 gwei", gasPrice.String()) + assert.Equal(t, "41.5 gwei", gasPrice.Legacy.String()) assert.Equal(t, initialDefaultGasPrice, chain.Config().EvmGasPriceDefault().Int64()) // unchanged // BlockHistoryEstimator new blocks @@ -1401,9 +1401,9 @@ func TestIntegration_BlockHistoryEstimator(t *testing.T) { newHeads.TrySend(cltest.Head(43)) gomega.NewWithT(t).Eventually(func() string { - gasPrice, _, err := estimator.GetLegacyGas(testutils.Context(t), nil, 500000, maxGasPrice) + gasPrice, _, err := estimator.GetFee(testutils.Context(t), nil, 500000, maxGasPrice) require.NoError(t, err) - return gasPrice.String() + return gasPrice.Legacy.String() }, testutils.WaitTimeout(t), cltest.DBPollingInterval).Should(gomega.Equal("45 gwei")) } diff --git a/core/services/keeper/upkeep_executer.go b/core/services/keeper/upkeep_executer.go index 92f4e3d58ed..2dad25f5dbf 100644 --- a/core/services/keeper/upkeep_executer.go +++ b/core/services/keeper/upkeep_executer.go @@ -12,6 +12,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + txmgrtypes "github.com/smartcontractkit/chainlink/common/txmgr/types" "github.com/smartcontractkit/chainlink/core/assets" evmclient "github.com/smartcontractkit/chainlink/core/chains/evm/client" "github.com/smartcontractkit/chainlink/core/chains/evm/gas" @@ -51,7 +52,7 @@ type UpkeepExecuter struct { config Config executionQueue chan struct{} headBroadcaster httypes.HeadBroadcasterRegistry - gasEstimator gas.Estimator + gasEstimator txmgrtypes.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, common.Hash] job job.Job mailbox *utils.Mailbox[*evmtypes.Head] orm ORM @@ -69,7 +70,7 @@ func NewUpkeepExecuter( pr pipeline.Runner, ethClient evmclient.Client, headBroadcaster httypes.HeadBroadcaster, - gasEstimator gas.Estimator, + gasEstimator txmgrtypes.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, common.Hash], logger logger.Logger, config Config, effectiveKeeperAddress common.Address, diff --git a/core/services/keeper/upkeep_executer_test.go b/core/services/keeper/upkeep_executer_test.go index 198c70228e7..ec6b529280a 100644 --- a/core/services/keeper/upkeep_executer_test.go +++ b/core/services/keeper/upkeep_executer_test.go @@ -14,10 +14,10 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + txmgrmocks "github.com/smartcontractkit/chainlink/common/txmgr/types/mocks" "github.com/smartcontractkit/chainlink/core/assets" "github.com/smartcontractkit/chainlink/core/chains/evm" "github.com/smartcontractkit/chainlink/core/chains/evm/gas" - gasmocks "github.com/smartcontractkit/chainlink/core/chains/evm/gas/mocks" evmmocks "github.com/smartcontractkit/chainlink/core/chains/evm/mocks" "github.com/smartcontractkit/chainlink/core/chains/evm/txmgr" txmmocks "github.com/smartcontractkit/chainlink/core/chains/evm/txmgr/mocks" @@ -40,17 +40,18 @@ func newHead() evmtypes.Head { return evmtypes.NewHead(big.NewInt(20), utils.NewHash(), utils.NewHash(), 1000, utils.NewBigI(0)) } -func mockEstimator(t *testing.T) (estimator *gasmocks.Estimator) { - estimator = gasmocks.NewEstimator(t) - estimator.On("GetLegacyGas", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(assets.GWei(60), uint32(0), nil) - estimator.On("GetDynamicFee", mock.Anything, mock.Anything, mock.Anything).Maybe().Return(gas.DynamicFee{ - FeeCap: assets.GWei(60), - TipCap: assets.GWei(60), +func mockEstimator(t *testing.T) (estimator *txmgrmocks.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, common.Hash]) { + // note: estimator will only return 1 of legacy or dynamic fees (not both) + // assumed to call legacy estimator only + estimator = txmgrmocks.NewFeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, common.Hash](t) + estimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(gas.EvmFee{ + Legacy: assets.GWei(60), + Dynamic: nil, }, uint32(60), nil) return } -func setup(t *testing.T, estimator *gasmocks.Estimator, overrideFn func(c *chainlink.Config, s *chainlink.Secrets)) ( +func setup(t *testing.T, estimator *txmgrmocks.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, common.Hash], overrideFn func(c *chainlink.Config, s *chainlink.Secrets)) ( *sqlx.DB, config.GeneralConfig, *evmmocks.Client, diff --git a/core/services/ocr2/plugins/ocr2vrf/reasonablegasprice/reasonable_gas_price_provider.go b/core/services/ocr2/plugins/ocr2vrf/reasonablegasprice/reasonable_gas_price_provider.go index cbe0fd98c6f..430a57dd790 100644 --- a/core/services/ocr2/plugins/ocr2vrf/reasonablegasprice/reasonable_gas_price_provider.go +++ b/core/services/ocr2/plugins/ocr2vrf/reasonablegasprice/reasonable_gas_price_provider.go @@ -4,15 +4,18 @@ import ( "math/big" "time" + "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/ocr2vrf/types" + txmgrtypes "github.com/smartcontractkit/chainlink/common/txmgr/types" "github.com/smartcontractkit/chainlink/core/assets" "github.com/smartcontractkit/chainlink/core/chains/evm/gas" + evmtypes "github.com/smartcontractkit/chainlink/core/chains/evm/types" ) // reasonableGasPriceProvider provides an estimate for the average gas price type reasonableGasPriceProvider struct { - estimator gas.Estimator + estimator txmgrtypes.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, common.Hash] timeout time.Duration maxGasPrice *assets.Wei supportsDynamicFee bool @@ -21,7 +24,7 @@ type reasonableGasPriceProvider struct { var _ types.ReasonableGasPrice = (*reasonableGasPriceProvider)(nil) func NewReasonableGasPriceProvider( - estimator gas.Estimator, + estimator txmgrtypes.FeeEstimator[*evmtypes.Head, gas.EvmFee, *assets.Wei, common.Hash], timeout time.Duration, maxGasPrice *assets.Wei, supportsDynamicFee bool, diff --git a/core/web/evm_transfer_controller.go b/core/web/evm_transfer_controller.go index bc07bcd8c4f..87e4e0147af 100644 --- a/core/web/evm_transfer_controller.go +++ b/core/web/evm_transfer_controller.go @@ -9,6 +9,7 @@ import ( "github.com/smartcontractkit/chainlink/core/assets" "github.com/smartcontractkit/chainlink/core/chains/evm" + "github.com/smartcontractkit/chainlink/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/core/logger/audit" "github.com/smartcontractkit/chainlink/core/services/chainlink" "github.com/smartcontractkit/chainlink/core/store/models" @@ -93,16 +94,22 @@ func ValidateEthBalanceForTransfer(c *gin.Context, chain evm.Chain, fromAddr com return errors.Errorf("balance is too low for this transaction to be executed: %v", balance) } - var gasPrice *assets.Wei + var fees gas.EvmFee gasLimit := chain.Config().EvmGasLimitTransfer() estimator := chain.TxManager().GetGasEstimator() - gasPrice, gasLimit, err = estimator.GetLegacyGas(c, nil, gasLimit, chain.Config().KeySpecificMaxGasPriceWei(fromAddr)) + fees, gasLimit, err = estimator.GetFee(c, nil, gasLimit, chain.Config().KeySpecificMaxGasPriceWei(fromAddr)) if err != nil { return errors.Wrap(err, "failed to estimate gas") } + // TODO: support EIP-1559 transactions + if fees.Legacy == nil { + return errors.New("estimator did not return legacy tx fee estimates") + } + gasPrice := fees.Legacy + // Creating a `Big` struct to avoid having a mutation on `tr.Amount` and hence affecting the value stored in the DB amountAsBig := utils.NewBig(amount.ToInt()) fee := new(big.Int).Mul(gasPrice.ToInt(), big.NewInt(int64(gasLimit)))