From 986a9a2b32aee7138c8cdc27537111a2689d2de5 Mon Sep 17 00:00:00 2001 From: patrickhuie19 Date: Mon, 23 Jun 2025 20:53:37 -0400 Subject: [PATCH 01/10] WIP --- capabilities/writetarget/write_target.go | 89 +++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/capabilities/writetarget/write_target.go b/capabilities/writetarget/write_target.go index 386b6dc..403b795 100644 --- a/capabilities/writetarget/write_target.go +++ b/capabilities/writetarget/write_target.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "errors" "fmt" + "math/big" "time" "go.opentelemetry.io/otel/attribute" @@ -57,6 +58,8 @@ type TargetStrategy interface { TransmitReport(ctx context.Context, report []byte, reportContext []byte, signatures [][]byte, request capabilities.CapabilityRequest) (string, error) // Wrapper around the ChainWriter to get the transaction status GetTransactionStatus(ctx context.Context, transactionID string) (commontypes.TransactionStatus, error) + // Wrapper around the ChainWriter to get the fee esimate + GetEstimateFee(ctx context.Context, contract string, method string, args any, toAddress string, meta *commontypes.TxMeta, val *big.Int) (commontypes.EstimateFee, error) } var ( @@ -74,6 +77,7 @@ const ( type chainService interface { LatestHead(ctx context.Context) (commontypes.Head, error) + GetTransactionFee(ctx context.Context, transactionID string) (commontypes.ChainFeeComponents, error) } type writeTarget struct { @@ -175,6 +179,53 @@ func success() capabilities.CapabilityResponse { return capabilities.CapabilityResponse{} } +// getGasSpendLimit returns the gas spend limit for the given chain ID from the request metadata +func (c *writeTarget) getGasSpendLimit(request capabilities.CapabilityRequest) (string, error) { + spendType := "GAS." + c.chainInfo.ChainID + + for _, limit := range request.Metadata.SpendLimits { + if spendType == string(limit.SpendType) { + return limit.Limit, nil + } + } + return "", fmt.Errorf("no gas spend limit found for chain %s", c.chainInfo.ChainID) +} + +// checkGasEstimate verifies if the estimated gas fee is within the spend limit and returns the fee +func (c *writeTarget) checkGasEstimate(ctx context.Context, request capabilities.CapabilityRequest) (*big.Int, uint32, error) { + spendLimit, err := c.getGasSpendLimit(request) + if err != nil { + return nil, 0, fmt.Errorf("failed to get gas spend limit, not performing gas estimation: %w", err) + } + + // Get gas estimate from ContractWriter + fee, err := c.targetStrategy.GetEstimateFee(ctx, "", "", nil, "", nil, nil) + if err != nil { + return nil, 0, fmt.Errorf("failed to get gas estimate: %w", err) + } + + // Convert spend limit from ETH to wei + limitFloat, ok := new(big.Float).SetString(spendLimit) + if !ok { + return nil, 0, fmt.Errorf("invalid gas spend limit format: %s", spendLimit) + } + + // Multiply by 10^decimals to convert from ETH to wei + multiplier := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(fee.Decimals)), nil)) + limitFloat.Mul(limitFloat, multiplier) + + // Convert to big.Int for comparison + limit := new(big.Int) + limitFloat.Int(limit) + + // Compare estimate with limit + if fee.Fee.Cmp(limit) > 0 { + return nil, 0, fmt.Errorf("estimated gas fee %s exceeds spend limit %s", fee.Fee.String(), limit.String()) + } + + return fee.Fee, fee.Decimals, nil +} + func (c *writeTarget) Execute(ctx context.Context, request capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { // Take the local timestamp tsStart := time.Now().UnixMilli() @@ -189,6 +240,24 @@ func (c *writeTarget) Execute(ctx context.Context, request capabilities.Capabili c.lggr.Debugw("Execute", "request", request, "capInfo", capInfo) + // Check gas estimate before proceeding + // TODO: discuss if we should release this in a separate PR + fee, _, err := c.checkGasEstimate(ctx, request) + if err != nil { + // Build error message + info := &requestInfo{ + tsStart: tsStart, + node: c.nodeAddress, + request: request, + } + errMsg := c.asEmittedError(ctx, &wt.WriteError{ + Code: uint32(TransmissionStateFatal), + Summary: "InsufficientFunds", + Cause: err.Error(), + }, "info", info) + return capabilities.CapabilityResponse{}, errMsg + } + // Helper to keep track of the request info info := requestInfo{ tsStart: tsStart, @@ -342,7 +411,25 @@ func (c *writeTarget) Execute(ctx context.Context, request capabilities.Capabili if err != nil { return capabilities.CapabilityResponse{}, err } - return success(), nil + + // Get the transaction fee + feeComponents, err := c.cs.GetTransactionFee(ctx, txID) + // TODO: implement in EVM + Aptos chain service + if err != nil { + return capabilities.CapabilityResponse{}, fmt.Errorf("failed to get transaction fee: %w", err) + } + + return capabilities.CapabilityResponse{ + Metadata: capabilities.ResponseMetadata{ + Metering: []capabilities.MeteringNodeDetail{ + { + Peer2PeerID: "ignored_by_engine", + SpendUnit: "GAS." + c.chainInfo.ChainID, + SpendValue: feeComponents.ExecutionFee.String(), + }, + }, + }, + }, nil } func (c *writeTarget) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error { From 4ef7f71b3aa6cf027e5f2e6084e3ed779b78c549 Mon Sep 17 00:00:00 2001 From: patrickhuie19 Date: Thu, 26 Jun 2025 11:10:57 -0400 Subject: [PATCH 02/10] GetTransactionFee wired through TargetStrategy iface; backwards incompatible estimate check fixed --- capabilities/writetarget/write_target.go | 52 +++++++++++++----------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/capabilities/writetarget/write_target.go b/capabilities/writetarget/write_target.go index 403b795..627e2f3 100644 --- a/capabilities/writetarget/write_target.go +++ b/capabilities/writetarget/write_target.go @@ -13,6 +13,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" + "github.com/shopspring/decimal" "github.com/smartcontractkit/chainlink-common/pkg/beholder" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types" @@ -60,6 +61,9 @@ type TargetStrategy interface { GetTransactionStatus(ctx context.Context, transactionID string) (commontypes.TransactionStatus, error) // Wrapper around the ChainWriter to get the fee esimate GetEstimateFee(ctx context.Context, contract string, method string, args any, toAddress string, meta *commontypes.TxMeta, val *big.Int) (commontypes.EstimateFee, error) + // GetTransactionFee retrieves the actual transaction fee in native currency from the transaction receipt. + // This method should be implemented by chain-specific services and handle the conversion of gas units to native currency. + GetTransactionFee(ctx context.Context, transactionID string) (decimal.Decimal, error) } var ( @@ -77,7 +81,6 @@ const ( type chainService interface { LatestHead(ctx context.Context) (commontypes.Head, error) - GetTransactionFee(ctx context.Context, transactionID string) (commontypes.ChainFeeComponents, error) } type writeTarget struct { @@ -192,12 +195,7 @@ func (c *writeTarget) getGasSpendLimit(request capabilities.CapabilityRequest) ( } // checkGasEstimate verifies if the estimated gas fee is within the spend limit and returns the fee -func (c *writeTarget) checkGasEstimate(ctx context.Context, request capabilities.CapabilityRequest) (*big.Int, uint32, error) { - spendLimit, err := c.getGasSpendLimit(request) - if err != nil { - return nil, 0, fmt.Errorf("failed to get gas spend limit, not performing gas estimation: %w", err) - } - +func (c *writeTarget) checkGasEstimate(ctx context.Context, spendLimit string) (*big.Int, uint32, error) { // Get gas estimate from ContractWriter fee, err := c.targetStrategy.GetEstimateFee(ctx, "", "", nil, "", nil, nil) if err != nil { @@ -240,22 +238,29 @@ func (c *writeTarget) Execute(ctx context.Context, request capabilities.Capabili c.lggr.Debugw("Execute", "request", request, "capInfo", capInfo) - // Check gas estimate before proceeding - // TODO: discuss if we should release this in a separate PR - fee, _, err := c.checkGasEstimate(ctx, request) + // Get gas spend limit first + spendLimit, err := c.getGasSpendLimit(request) if err != nil { - // Build error message - info := &requestInfo{ - tsStart: tsStart, - node: c.nodeAddress, - request: request, + // No spend limit provided, skip gas estimation and continue with execution + c.lggr.Debugw("No gas spend limit found, skipping gas estimation", "err", err) + } else { + // Check gas estimate before proceeding + // TODO: discuss if we should release this in a separate PR + _, _, err := c.checkGasEstimate(ctx, spendLimit) + if err != nil { + // Build error message + info := &requestInfo{ + tsStart: tsStart, + node: c.nodeAddress, + request: request, + } + errMsg := c.asEmittedError(ctx, &wt.WriteError{ + Code: uint32(TransmissionStateFatal), + Summary: "InsufficientFunds", + Cause: err.Error(), + }, "info", info) + return capabilities.CapabilityResponse{}, errMsg } - errMsg := c.asEmittedError(ctx, &wt.WriteError{ - Code: uint32(TransmissionStateFatal), - Summary: "InsufficientFunds", - Cause: err.Error(), - }, "info", info) - return capabilities.CapabilityResponse{}, errMsg } // Helper to keep track of the request info @@ -413,8 +418,7 @@ func (c *writeTarget) Execute(ctx context.Context, request capabilities.Capabili } // Get the transaction fee - feeComponents, err := c.cs.GetTransactionFee(ctx, txID) - // TODO: implement in EVM + Aptos chain service + fee, err := c.targetStrategy.GetTransactionFee(ctx, txID) if err != nil { return capabilities.CapabilityResponse{}, fmt.Errorf("failed to get transaction fee: %w", err) } @@ -425,7 +429,7 @@ func (c *writeTarget) Execute(ctx context.Context, request capabilities.Capabili { Peer2PeerID: "ignored_by_engine", SpendUnit: "GAS." + c.chainInfo.ChainID, - SpendValue: feeComponents.ExecutionFee.String(), + SpendValue: fee.String(), }, }, }, From c96de389557f60c5f72803aa0d046d8d065700bc Mon Sep 17 00:00:00 2001 From: patrickhuie19 Date: Thu, 26 Jun 2025 18:01:35 -0400 Subject: [PATCH 03/10] new signature for estimate fee; unit tests; mocks --- .../writetarget/mocks/target_strategy.go | 119 ++++++++++++++++ capabilities/writetarget/write_target.go | 56 ++++---- capabilities/writetarget/write_target_test.go | 129 +++++++++++++++++- 3 files changed, 270 insertions(+), 34 deletions(-) diff --git a/capabilities/writetarget/mocks/target_strategy.go b/capabilities/writetarget/mocks/target_strategy.go index 6f2740c..a52c7fe 100644 --- a/capabilities/writetarget/mocks/target_strategy.go +++ b/capabilities/writetarget/mocks/target_strategy.go @@ -7,6 +7,8 @@ import ( capabilities "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + decimal "github.com/shopspring/decimal" + mock "github.com/stretchr/testify/mock" types "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -27,6 +29,123 @@ func (_m *TargetStrategy) EXPECT() *TargetStrategy_Expecter { return &TargetStrategy_Expecter{mock: &_m.Mock} } +// GetEstimateFee provides a mock function with given fields: ctx, report, reportContext, signatures, request +func (_m *TargetStrategy) GetEstimateFee(ctx context.Context, report []byte, reportContext []byte, signatures [][]byte, request capabilities.CapabilityRequest) (types.EstimateFee, error) { + ret := _m.Called(ctx, report, reportContext, signatures, request) + + if len(ret) == 0 { + panic("no return value specified for GetEstimateFee") + } + + var r0 types.EstimateFee + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []byte, []byte, [][]byte, capabilities.CapabilityRequest) (types.EstimateFee, error)); ok { + return rf(ctx, report, reportContext, signatures, request) + } + if rf, ok := ret.Get(0).(func(context.Context, []byte, []byte, [][]byte, capabilities.CapabilityRequest) types.EstimateFee); ok { + r0 = rf(ctx, report, reportContext, signatures, request) + } else { + r0 = ret.Get(0).(types.EstimateFee) + } + + if rf, ok := ret.Get(1).(func(context.Context, []byte, []byte, [][]byte, capabilities.CapabilityRequest) error); ok { + r1 = rf(ctx, report, reportContext, signatures, request) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TargetStrategy_GetEstimateFee_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetEstimateFee' +type TargetStrategy_GetEstimateFee_Call struct { + *mock.Call +} + +// GetEstimateFee is a helper method to define mock.On call +// - ctx context.Context +// - report []byte +// - reportContext []byte +// - signatures [][]byte +// - request capabilities.CapabilityRequest +func (_e *TargetStrategy_Expecter) GetEstimateFee(ctx interface{}, report interface{}, reportContext interface{}, signatures interface{}, request interface{}) *TargetStrategy_GetEstimateFee_Call { + return &TargetStrategy_GetEstimateFee_Call{Call: _e.mock.On("GetEstimateFee", ctx, report, reportContext, signatures, request)} +} + +func (_c *TargetStrategy_GetEstimateFee_Call) Run(run func(ctx context.Context, report []byte, reportContext []byte, signatures [][]byte, request capabilities.CapabilityRequest)) *TargetStrategy_GetEstimateFee_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]byte), args[2].([]byte), args[3].([][]byte), args[4].(capabilities.CapabilityRequest)) + }) + return _c +} + +func (_c *TargetStrategy_GetEstimateFee_Call) Return(_a0 types.EstimateFee, _a1 error) *TargetStrategy_GetEstimateFee_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *TargetStrategy_GetEstimateFee_Call) RunAndReturn(run func(context.Context, []byte, []byte, [][]byte, capabilities.CapabilityRequest) (types.EstimateFee, error)) *TargetStrategy_GetEstimateFee_Call { + _c.Call.Return(run) + return _c +} + +// GetTransactionFee provides a mock function with given fields: ctx, transactionID +func (_m *TargetStrategy) GetTransactionFee(ctx context.Context, transactionID string) (decimal.Decimal, error) { + ret := _m.Called(ctx, transactionID) + + if len(ret) == 0 { + panic("no return value specified for GetTransactionFee") + } + + var r0 decimal.Decimal + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (decimal.Decimal, error)); ok { + return rf(ctx, transactionID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) decimal.Decimal); ok { + r0 = rf(ctx, transactionID) + } else { + r0 = ret.Get(0).(decimal.Decimal) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, transactionID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TargetStrategy_GetTransactionFee_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTransactionFee' +type TargetStrategy_GetTransactionFee_Call struct { + *mock.Call +} + +// GetTransactionFee is a helper method to define mock.On call +// - ctx context.Context +// - transactionID string +func (_e *TargetStrategy_Expecter) GetTransactionFee(ctx interface{}, transactionID interface{}) *TargetStrategy_GetTransactionFee_Call { + return &TargetStrategy_GetTransactionFee_Call{Call: _e.mock.On("GetTransactionFee", ctx, transactionID)} +} + +func (_c *TargetStrategy_GetTransactionFee_Call) Run(run func(ctx context.Context, transactionID string)) *TargetStrategy_GetTransactionFee_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *TargetStrategy_GetTransactionFee_Call) Return(_a0 decimal.Decimal, _a1 error) *TargetStrategy_GetTransactionFee_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *TargetStrategy_GetTransactionFee_Call) RunAndReturn(run func(context.Context, string) (decimal.Decimal, error)) *TargetStrategy_GetTransactionFee_Call { + _c.Call.Return(run) + return _c +} + // GetTransactionStatus provides a mock function with given fields: ctx, transactionID func (_m *TargetStrategy) GetTransactionStatus(ctx context.Context, transactionID string) (types.TransactionStatus, error) { ret := _m.Called(ctx, transactionID) diff --git a/capabilities/writetarget/write_target.go b/capabilities/writetarget/write_target.go index 627e2f3..fc1e468 100644 --- a/capabilities/writetarget/write_target.go +++ b/capabilities/writetarget/write_target.go @@ -60,7 +60,7 @@ type TargetStrategy interface { // Wrapper around the ChainWriter to get the transaction status GetTransactionStatus(ctx context.Context, transactionID string) (commontypes.TransactionStatus, error) // Wrapper around the ChainWriter to get the fee esimate - GetEstimateFee(ctx context.Context, contract string, method string, args any, toAddress string, meta *commontypes.TxMeta, val *big.Int) (commontypes.EstimateFee, error) + GetEstimateFee(ctx context.Context, report []byte, reportContext []byte, signatures [][]byte, request capabilities.CapabilityRequest) (commontypes.EstimateFee, error) // GetTransactionFee retrieves the actual transaction fee in native currency from the transaction receipt. // This method should be implemented by chain-specific services and handle the conversion of gas units to native currency. GetTransactionFee(ctx context.Context, transactionID string) (decimal.Decimal, error) @@ -195,9 +195,9 @@ func (c *writeTarget) getGasSpendLimit(request capabilities.CapabilityRequest) ( } // checkGasEstimate verifies if the estimated gas fee is within the spend limit and returns the fee -func (c *writeTarget) checkGasEstimate(ctx context.Context, spendLimit string) (*big.Int, uint32, error) { +func (c *writeTarget) checkGasEstimate(ctx context.Context, spendLimit string, report []byte, reportContext []byte, signatures [][]byte, request capabilities.CapabilityRequest) (*big.Int, uint32, error) { // Get gas estimate from ContractWriter - fee, err := c.targetStrategy.GetEstimateFee(ctx, "", "", nil, "", nil, nil) + fee, err := c.targetStrategy.GetEstimateFee(ctx, report, reportContext, signatures, request) if err != nil { return nil, 0, fmt.Errorf("failed to get gas estimate: %w", err) } @@ -238,31 +238,6 @@ func (c *writeTarget) Execute(ctx context.Context, request capabilities.Capabili c.lggr.Debugw("Execute", "request", request, "capInfo", capInfo) - // Get gas spend limit first - spendLimit, err := c.getGasSpendLimit(request) - if err != nil { - // No spend limit provided, skip gas estimation and continue with execution - c.lggr.Debugw("No gas spend limit found, skipping gas estimation", "err", err) - } else { - // Check gas estimate before proceeding - // TODO: discuss if we should release this in a separate PR - _, _, err := c.checkGasEstimate(ctx, spendLimit) - if err != nil { - // Build error message - info := &requestInfo{ - tsStart: tsStart, - node: c.nodeAddress, - request: request, - } - errMsg := c.asEmittedError(ctx, &wt.WriteError{ - Code: uint32(TransmissionStateFatal), - Summary: "InsufficientFunds", - Cause: err.Error(), - }, "info", info) - return capabilities.CapabilityResponse{}, errMsg - } - } - // Helper to keep track of the request info info := requestInfo{ tsStart: tsStart, @@ -309,6 +284,31 @@ func (c *writeTarget) Execute(ctx context.Context, request capabilities.Capabili // Source the report ID from the input info.reportInfo.reportID = binary.BigEndian.Uint16(inputs.ID) + // Get gas spend limit first + spendLimit, err := c.getGasSpendLimit(request) + if err != nil { + // No spend limit provided, skip gas estimation and continue with execution + c.lggr.Debugw("No gas spend limit found, skipping gas estimation", "err", err) + } else { + // Check gas estimate before proceeding + // TODO: discuss if we should release this in a separate PR + _, _, err := c.checkGasEstimate(ctx, spendLimit, inputs.Report, inputs.Context, inputs.Signatures, request) + if err != nil { + // Build error message + info := &requestInfo{ + tsStart: tsStart, + node: c.nodeAddress, + request: request, + } + errMsg := c.asEmittedError(ctx, &wt.WriteError{ + Code: uint32(TransmissionStateFatal), + Summary: "InsufficientFunds", + Cause: err.Error(), + }, "info", info) + return capabilities.CapabilityResponse{}, errMsg + } + } + err = c.beholder.ProtoEmitter.EmitWithLog(ctx, builder.buildWriteInitiated(info)) if err != nil { c.lggr.Errorw("failed to emit write initiated", "err", err) diff --git a/capabilities/writetarget/write_target_test.go b/capabilities/writetarget/write_target_test.go index 00b23d1..e483d4d 100644 --- a/capabilities/writetarget/write_target_test.go +++ b/capabilities/writetarget/write_target_test.go @@ -3,9 +3,11 @@ package writetarget_test import ( "context" "errors" + "math/big" "testing" "time" + "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -36,6 +38,7 @@ func setupWriteTarget( chainSvc *wtmocks.ChainService, productSpecificProcessor bool, emitter beholder.ProtoEmitter, + spendLimits []capabilities.SpendLimit, ) (capabilities.ExecutableCapability, capabilities.CapabilityRequest) { platformProcessors, err := processor.NewPlatformProcessors(emitter) require.NoError(t, err) @@ -43,6 +46,8 @@ func setupWriteTarget( if productSpecificProcessor { platformProcessors["test"] = newMockProductSpecificProcessor(t) } + // Always add a mock for the default processor (writetarget) to prevent nil pointer dereference + platformProcessors["writetarget"] = newMockProductSpecificProcessor(t) monClient, err := writetarget.NewMonitor(writetarget.MonitorOpts{lggr, platformProcessors, processor.PlatformDefaultProcessors, emitter}) require.NoError(t, err) @@ -56,7 +61,7 @@ func setupWriteTarget( PollPeriod: pollPeriod, AcceptanceTimeout: timeout, }, - ChainInfo: monitor.ChainInfo{}, + ChainInfo: monitor.ChainInfo{ChainID: "1"}, Logger: lggr, Beholder: monClient, ChainService: chainSvc, @@ -94,6 +99,7 @@ func setupWriteTarget( WorkflowOwner: repDecoded.WorkflowOwner, WorkflowName: repDecoded.WorkflowName, WorkflowExecutionID: repDecoded.ExecutionID, + SpendLimits: spendLimits, } cfg, err := values.NewMap(map[string]any{"address": "0x1", "processor": "test"}) @@ -105,7 +111,9 @@ func setupWriteTarget( func newMockProductSpecificProcessor(t *testing.T) beholder.ProtoProcessor { processor := monmocks.NewProtoProcessor(t) - processor.EXPECT().Process(mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + // Handle both 3-arg and 4-arg Process calls + processor.EXPECT().Process(mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() + processor.EXPECT().Process(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() return processor } @@ -118,6 +126,13 @@ type testCase struct { errorContains string productSpecificProcessor bool requiredLogMessage string + // Gas estimation and transaction fee fields + spendLimits []capabilities.SpendLimit + gasEstimateError error + gasEstimateFee *commontypes.EstimateFee + transactionFeeError error + transactionFee decimal.Decimal + expectTransactionFee bool } func TestWriteTarget_Execute(t *testing.T) { @@ -128,6 +143,8 @@ func TestWriteTarget_Execute(t *testing.T) { txState: commontypes.Finalized, expectError: false, requiredLogMessage: "no matching processor for MetaCapabilityProcessor=test", + transactionFee: decimal.NewFromFloat(0.0001), + expectTransactionFee: true, }, { name: "succeeds transmission state is already succeeded", @@ -142,6 +159,8 @@ func TestWriteTarget_Execute(t *testing.T) { expectError: false, productSpecificProcessor: true, requiredLogMessage: "confirmed - transmission state visible", + transactionFee: decimal.NewFromFloat(0.0001), + expectTransactionFee: true, }, { name: "already succeeded with product specific processor", @@ -204,6 +223,8 @@ func TestWriteTarget_Execute(t *testing.T) { simulateTxError: false, expectError: false, requiredLogMessage: "confirmed - transmission state visible but submitted by another node. This node's tx failed", + transactionFee: decimal.NewFromFloat(0.0001), + expectTransactionFee: true, }, { name: "Returns success if report is on-chain but tx is failed", @@ -212,6 +233,49 @@ func TestWriteTarget_Execute(t *testing.T) { simulateTxError: false, expectError: false, requiredLogMessage: "confirmed - transmission state visible but submitted by another node. This node's tx failed", + transactionFee: decimal.NewFromFloat(0.0001), + expectTransactionFee: true, + }, + // Gas estimation and transaction fee test cases + { + name: "succeeds when no spend limit is specified", + initialTransmissionState: writetarget.TransmissionState{Status: writetarget.TransmissionStateNotAttempted}, + txState: commontypes.Finalized, + expectError: false, + spendLimits: []capabilities.SpendLimit{}, + transactionFee: decimal.NewFromFloat(0.0005), + expectTransactionFee: true, + requiredLogMessage: "No gas spend limit found, skipping gas estimation", + }, + { + name: "fails when gas estimate exceeds spend limit", + initialTransmissionState: writetarget.TransmissionState{Status: writetarget.TransmissionStateNotAttempted}, + txState: commontypes.Unknown, + expectError: true, + errorContains: "InsufficientFunds", + spendLimits: []capabilities.SpendLimit{ + {SpendType: "GAS.1", Limit: "0.001"}, + }, + gasEstimateFee: &commontypes.EstimateFee{ + Fee: big.NewInt(2000000000000000), // 0.002 ETH in wei (exceeds limit) + Decimals: 18, + }, + }, + { + name: "succeeds when gas estimate is within spend limit and includes transaction fee", + initialTransmissionState: writetarget.TransmissionState{Status: writetarget.TransmissionStateNotAttempted}, + txState: commontypes.Finalized, + expectError: false, + spendLimits: []capabilities.SpendLimit{ + {SpendType: "GAS.1", Limit: "0.001"}, + }, + gasEstimateFee: &commontypes.EstimateFee{ + Fee: big.NewInt(500000000000000), // 0.0005 ETH in wei (within limit) + Decimals: 18, + }, + transactionFee: decimal.NewFromFloat(0.0005), + expectTransactionFee: true, + requiredLogMessage: "confirmed - transmission state visible", }, } @@ -225,12 +289,17 @@ func TestWriteTarget_Execute(t *testing.T) { mockTransmissionState(tc, strategy) mockBeholderMessages(tc, emitter) mockTransmit(tc, strategy, emitter) + mockGasEstimation(tc, strategy) + mockTransactionFee(tc, strategy) chainSvc := wtmocks.NewChainService(t) - chainSvc.EXPECT().LatestHead(mock.Anything). - Return(commontypes.Head{Height: "100"}, nil) + // Only set up LatestHead mock if gas estimation doesn't fail + if !(tc.expectError && len(tc.spendLimits) > 0 && tc.gasEstimateFee != nil && tc.gasEstimateFee.Fee.Cmp(big.NewInt(1000000000000000)) > 0) { + chainSvc.EXPECT().LatestHead(mock.Anything). + Return(commontypes.Head{Height: "100"}, nil) + } - target, req := setupWriteTarget(t, lggr, strategy, chainSvc, tc.productSpecificProcessor, emitter) + target, req := setupWriteTarget(t, lggr, strategy, chainSvc, tc.productSpecificProcessor, emitter, tc.spendLimits) resp, err := target.Execute(t.Context(), req) if tc.expectError { @@ -239,6 +308,14 @@ func TestWriteTarget_Execute(t *testing.T) { } else { require.NoError(t, err) assert.NotNil(t, resp) + + // Verify transaction fee in response metadata for successful cases + if tc.expectTransactionFee && tc.transactionFeeError == nil { + require.NotEmpty(t, resp.Metadata.Metering) + require.Equal(t, "ignored_by_engine", resp.Metadata.Metering[0].Peer2PeerID) + require.Equal(t, "GAS.1", resp.Metadata.Metering[0].SpendUnit) + require.Equal(t, tc.transactionFee.String(), resp.Metadata.Metering[0].SpendValue) + } } if tc.requiredLogMessage != "" { @@ -255,7 +332,7 @@ func TestWriteTarget_Execute(t *testing.T) { emitter := monmocks.NewProtoEmitter(t) emitter.EXPECT().EmitWithLog(mock.Anything, mock.Anything, mock.Anything).Return(nil) - target, _ := setupWriteTarget(t, logger.Test(t), strategy, chainSvc, false, emitter) + target, _ := setupWriteTarget(t, logger.Test(t), strategy, chainSvc, false, emitter, nil) inputs, _ := values.NewMap(map[string]any{}) config, _ := values.NewMap(map[string]any{"address": "x", "processor": "y"}) @@ -300,6 +377,11 @@ func TestWriteTarget_Execute(t *testing.T) { } func mockTransmissionState(tc testCase, strategy *wtmocks.TargetStrategy) { + // Skip transmission state mocks if gas estimation fails + if tc.expectError && len(tc.spendLimits) > 0 && tc.gasEstimateFee != nil && tc.gasEstimateFee.Fee.Cmp(big.NewInt(1000000000000000)) > 0 { + return + } + // initial query for transmission state strategy.EXPECT().QueryTransmissionState(mock.Anything, mock.Anything, mock.Anything). Return(&tc.initialTransmissionState, nil).Once() @@ -315,6 +397,12 @@ func mockTransmissionState(tc testCase, strategy *wtmocks.TargetStrategy) { } func mockBeholderMessages(tc testCase, emitter *monmocks.ProtoEmitter) { + // For gas estimation errors, only expect WriteError (no WriteInitiated) + if tc.expectError && len(tc.spendLimits) > 0 && tc.gasEstimateFee != nil && tc.gasEstimateFee.Fee.Cmp(big.NewInt(1000000000000000)) > 0 { + emitter.EXPECT().EmitWithLog(mock.Anything, mock.AnythingOfType("*writetarget.WriteError"), mock.Anything, mock.Anything).Return(nil).Once() + return + } + // Ensure the correct beholder messages are emitted for each case emitter.EXPECT().EmitWithLog(mock.Anything, mock.AnythingOfType("*writetarget.WriteInitiated"), mock.Anything).Return(nil).Once() if tc.expectError { @@ -326,6 +414,11 @@ func mockBeholderMessages(tc testCase, emitter *monmocks.ProtoEmitter) { } func mockTransmit(tc testCase, strategy *wtmocks.TargetStrategy, emitter *monmocks.ProtoEmitter) { + // Skip transmission mocks if gas estimation fails + if tc.expectError && len(tc.spendLimits) > 0 && tc.gasEstimateFee != nil && tc.gasEstimateFee.Fee.Cmp(big.NewInt(1000000000000000)) > 0 { + return + } + if tc.txState != commontypes.Unknown { ex := strategy.EXPECT().TransmitReport(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) if tc.simulateTxError { @@ -338,6 +431,30 @@ func mockTransmit(tc testCase, strategy *wtmocks.TargetStrategy, emitter *monmoc } } +func mockGasEstimation(tc testCase, strategy *wtmocks.TargetStrategy) { + // Only set up gas estimation mock if we have spend limits + if len(tc.spendLimits) > 0 { + ex := strategy.EXPECT().GetEstimateFee(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) + if tc.gasEstimateError != nil { + ex.Return(commontypes.EstimateFee{}, tc.gasEstimateError) + } else if tc.gasEstimateFee != nil { + ex.Return(*tc.gasEstimateFee, nil) + } + } +} + +func mockTransactionFee(tc testCase, strategy *wtmocks.TargetStrategy) { + // Only set up transaction fee mock if we expect the execution to reach that point + if !tc.expectError && tc.expectTransactionFee { + ex := strategy.EXPECT().GetTransactionFee(mock.Anything, mock.Anything) + if tc.transactionFeeError != nil { + ex.Return(decimal.Decimal{}, tc.transactionFeeError) + } else { + ex.Return(tc.transactionFee, nil) + } + } +} + func TestNewWriteTargetID(t *testing.T) { tests := []struct { name string From 58bfe4bc8ccd08e7e714cbdee73e9ee90619909f Mon Sep 17 00:00:00 2001 From: patrickhuie19 Date: Thu, 26 Jun 2025 18:11:17 -0400 Subject: [PATCH 04/10] lint --- capabilities/writetarget/write_target.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/capabilities/writetarget/write_target.go b/capabilities/writetarget/write_target.go index fc1e468..43190e8 100644 --- a/capabilities/writetarget/write_target.go +++ b/capabilities/writetarget/write_target.go @@ -10,10 +10,10 @@ import ( "math/big" "time" + "github.com/shopspring/decimal" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" - "github.com/shopspring/decimal" "github.com/smartcontractkit/chainlink-common/pkg/beholder" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types" @@ -292,8 +292,8 @@ func (c *writeTarget) Execute(ctx context.Context, request capabilities.Capabili } else { // Check gas estimate before proceeding // TODO: discuss if we should release this in a separate PR - _, _, err := c.checkGasEstimate(ctx, spendLimit, inputs.Report, inputs.Context, inputs.Signatures, request) - if err != nil { + _, _, gasEstimateErr := c.checkGasEstimate(ctx, spendLimit, inputs.Report, inputs.Context, inputs.Signatures, request) + if gasEstimateErr != nil { // Build error message info := &requestInfo{ tsStart: tsStart, @@ -303,7 +303,7 @@ func (c *writeTarget) Execute(ctx context.Context, request capabilities.Capabili errMsg := c.asEmittedError(ctx, &wt.WriteError{ Code: uint32(TransmissionStateFatal), Summary: "InsufficientFunds", - Cause: err.Error(), + Cause: gasEstimateErr.Error(), }, "info", info) return capabilities.CapabilityResponse{}, errMsg } From d893172855e68eaa16a0904aa48245114632c388 Mon Sep 17 00:00:00 2001 From: patrickhuie19 Date: Mon, 30 Jun 2025 09:49:24 -0400 Subject: [PATCH 05/10] GetEstimateFee continues to return fee + decimals; conversion handled in framework --- capabilities/writetarget/write_target.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/capabilities/writetarget/write_target.go b/capabilities/writetarget/write_target.go index 43190e8..fb2176c 100644 --- a/capabilities/writetarget/write_target.go +++ b/capabilities/writetarget/write_target.go @@ -202,13 +202,13 @@ func (c *writeTarget) checkGasEstimate(ctx context.Context, spendLimit string, r return nil, 0, fmt.Errorf("failed to get gas estimate: %w", err) } - // Convert spend limit from ETH to wei + // Convert spend limit from chain currency to gas units limitFloat, ok := new(big.Float).SetString(spendLimit) if !ok { return nil, 0, fmt.Errorf("invalid gas spend limit format: %s", spendLimit) } - // Multiply by 10^decimals to convert from ETH to wei + // Multiply by 10^decimals to convert from chain currency to gas units multiplier := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(fee.Decimals)), nil)) limitFloat.Mul(limitFloat, multiplier) From 35e1e197c909b55b55f8cd9070b23f9dd9ef6bee Mon Sep 17 00:00:00 2001 From: patrickhuie19 Date: Tue, 1 Jul 2025 19:07:12 -0400 Subject: [PATCH 06/10] removing gas estimation checks --- capabilities/go.mod | 2 +- capabilities/writetarget/write_target.go | 60 ++---------------------- 2 files changed, 4 insertions(+), 58 deletions(-) diff --git a/capabilities/go.mod b/capabilities/go.mod index 3f645ae..07f5718 100644 --- a/capabilities/go.mod +++ b/capabilities/go.mod @@ -7,6 +7,7 @@ toolchain go1.24.3 require ( github.com/google/uuid v1.6.0 github.com/jpillora/backoff v1.0.0 + github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chainlink-common v0.7.1-0.20250618162808-a5a42ee8701b github.com/stretchr/testify v1.10.0 go.opentelemetry.io/otel v1.35.0 @@ -49,7 +50,6 @@ require ( github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/procfs v0.16.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.2.0 // indirect - github.com/shopspring/decimal v1.4.0 // indirect github.com/smartcontractkit/libocr v0.0.0-20250328171017-609ec10a5510 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect diff --git a/capabilities/writetarget/write_target.go b/capabilities/writetarget/write_target.go index fb2176c..592f478 100644 --- a/capabilities/writetarget/write_target.go +++ b/capabilities/writetarget/write_target.go @@ -194,36 +194,6 @@ func (c *writeTarget) getGasSpendLimit(request capabilities.CapabilityRequest) ( return "", fmt.Errorf("no gas spend limit found for chain %s", c.chainInfo.ChainID) } -// checkGasEstimate verifies if the estimated gas fee is within the spend limit and returns the fee -func (c *writeTarget) checkGasEstimate(ctx context.Context, spendLimit string, report []byte, reportContext []byte, signatures [][]byte, request capabilities.CapabilityRequest) (*big.Int, uint32, error) { - // Get gas estimate from ContractWriter - fee, err := c.targetStrategy.GetEstimateFee(ctx, report, reportContext, signatures, request) - if err != nil { - return nil, 0, fmt.Errorf("failed to get gas estimate: %w", err) - } - - // Convert spend limit from chain currency to gas units - limitFloat, ok := new(big.Float).SetString(spendLimit) - if !ok { - return nil, 0, fmt.Errorf("invalid gas spend limit format: %s", spendLimit) - } - - // Multiply by 10^decimals to convert from chain currency to gas units - multiplier := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(fee.Decimals)), nil)) - limitFloat.Mul(limitFloat, multiplier) - - // Convert to big.Int for comparison - limit := new(big.Int) - limitFloat.Int(limit) - - // Compare estimate with limit - if fee.Fee.Cmp(limit) > 0 { - return nil, 0, fmt.Errorf("estimated gas fee %s exceeds spend limit %s", fee.Fee.String(), limit.String()) - } - - return fee.Fee, fee.Decimals, nil -} - func (c *writeTarget) Execute(ctx context.Context, request capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { // Take the local timestamp tsStart := time.Now().UnixMilli() @@ -284,31 +254,6 @@ func (c *writeTarget) Execute(ctx context.Context, request capabilities.Capabili // Source the report ID from the input info.reportInfo.reportID = binary.BigEndian.Uint16(inputs.ID) - // Get gas spend limit first - spendLimit, err := c.getGasSpendLimit(request) - if err != nil { - // No spend limit provided, skip gas estimation and continue with execution - c.lggr.Debugw("No gas spend limit found, skipping gas estimation", "err", err) - } else { - // Check gas estimate before proceeding - // TODO: discuss if we should release this in a separate PR - _, _, gasEstimateErr := c.checkGasEstimate(ctx, spendLimit, inputs.Report, inputs.Context, inputs.Signatures, request) - if gasEstimateErr != nil { - // Build error message - info := &requestInfo{ - tsStart: tsStart, - node: c.nodeAddress, - request: request, - } - errMsg := c.asEmittedError(ctx, &wt.WriteError{ - Code: uint32(TransmissionStateFatal), - Summary: "InsufficientFunds", - Cause: gasEstimateErr.Error(), - }, "info", info) - return capabilities.CapabilityResponse{}, errMsg - } - } - err = c.beholder.ProtoEmitter.EmitWithLog(ctx, builder.buildWriteInitiated(info)) if err != nil { c.lggr.Errorw("failed to emit write initiated", "err", err) @@ -420,14 +365,15 @@ func (c *writeTarget) Execute(ctx context.Context, request capabilities.Capabili // Get the transaction fee fee, err := c.targetStrategy.GetTransactionFee(ctx, txID) if err != nil { - return capabilities.CapabilityResponse{}, fmt.Errorf("failed to get transaction fee: %w", err) + c.lggr.Errorw("failed to get transaction fee: %w", err) + return capabilities.CapabilityResponse{}, nil } return capabilities.CapabilityResponse{ Metadata: capabilities.ResponseMetadata{ Metering: []capabilities.MeteringNodeDetail{ { - Peer2PeerID: "ignored_by_engine", + // Peer2PeerID from remote peers is ignored by engine SpendUnit: "GAS." + c.chainInfo.ChainID, SpendValue: fee.String(), }, From 81fa437d60205c9bd704c5c17781f053c1cf958a Mon Sep 17 00:00:00 2001 From: patrickhuie19 Date: Tue, 1 Jul 2025 19:26:06 -0400 Subject: [PATCH 07/10] lint --- capabilities/writetarget/write_target.go | 1 - 1 file changed, 1 deletion(-) diff --git a/capabilities/writetarget/write_target.go b/capabilities/writetarget/write_target.go index 592f478..7f1cf7c 100644 --- a/capabilities/writetarget/write_target.go +++ b/capabilities/writetarget/write_target.go @@ -7,7 +7,6 @@ import ( "encoding/hex" "errors" "fmt" - "math/big" "time" "github.com/shopspring/decimal" From 8b5a8d4eafb63c7ab692871981c0ce898ae5567b Mon Sep 17 00:00:00 2001 From: patrickhuie19 Date: Tue, 1 Jul 2025 19:31:09 -0400 Subject: [PATCH 08/10] lint --- capabilities/writetarget/write_target.go | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/capabilities/writetarget/write_target.go b/capabilities/writetarget/write_target.go index 7f1cf7c..baec333 100644 --- a/capabilities/writetarget/write_target.go +++ b/capabilities/writetarget/write_target.go @@ -181,18 +181,6 @@ func success() capabilities.CapabilityResponse { return capabilities.CapabilityResponse{} } -// getGasSpendLimit returns the gas spend limit for the given chain ID from the request metadata -func (c *writeTarget) getGasSpendLimit(request capabilities.CapabilityRequest) (string, error) { - spendType := "GAS." + c.chainInfo.ChainID - - for _, limit := range request.Metadata.SpendLimits { - if spendType == string(limit.SpendType) { - return limit.Limit, nil - } - } - return "", fmt.Errorf("no gas spend limit found for chain %s", c.chainInfo.ChainID) -} - func (c *writeTarget) Execute(ctx context.Context, request capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { // Take the local timestamp tsStart := time.Now().UnixMilli() @@ -373,8 +361,8 @@ func (c *writeTarget) Execute(ctx context.Context, request capabilities.Capabili Metering: []capabilities.MeteringNodeDetail{ { // Peer2PeerID from remote peers is ignored by engine - SpendUnit: "GAS." + c.chainInfo.ChainID, - SpendValue: fee.String(), + SpendUnit: "GAS." + c.chainInfo.ChainID, + SpendValue: fee.String(), }, }, }, From 18be4062a34a213725f793b8cb8d2d9789558cb3 Mon Sep 17 00:00:00 2001 From: patrickhuie19 Date: Tue, 1 Jul 2025 19:39:00 -0400 Subject: [PATCH 09/10] removing gas estimation from unit tests --- capabilities/writetarget/write_target_test.go | 47 +------------------ 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/capabilities/writetarget/write_target_test.go b/capabilities/writetarget/write_target_test.go index e483d4d..65ebafe 100644 --- a/capabilities/writetarget/write_target_test.go +++ b/capabilities/writetarget/write_target_test.go @@ -245,37 +245,7 @@ func TestWriteTarget_Execute(t *testing.T) { spendLimits: []capabilities.SpendLimit{}, transactionFee: decimal.NewFromFloat(0.0005), expectTransactionFee: true, - requiredLogMessage: "No gas spend limit found, skipping gas estimation", - }, - { - name: "fails when gas estimate exceeds spend limit", - initialTransmissionState: writetarget.TransmissionState{Status: writetarget.TransmissionStateNotAttempted}, - txState: commontypes.Unknown, - expectError: true, - errorContains: "InsufficientFunds", - spendLimits: []capabilities.SpendLimit{ - {SpendType: "GAS.1", Limit: "0.001"}, - }, - gasEstimateFee: &commontypes.EstimateFee{ - Fee: big.NewInt(2000000000000000), // 0.002 ETH in wei (exceeds limit) - Decimals: 18, - }, - }, - { - name: "succeeds when gas estimate is within spend limit and includes transaction fee", - initialTransmissionState: writetarget.TransmissionState{Status: writetarget.TransmissionStateNotAttempted}, - txState: commontypes.Finalized, - expectError: false, - spendLimits: []capabilities.SpendLimit{ - {SpendType: "GAS.1", Limit: "0.001"}, - }, - gasEstimateFee: &commontypes.EstimateFee{ - Fee: big.NewInt(500000000000000), // 0.0005 ETH in wei (within limit) - Decimals: 18, - }, - transactionFee: decimal.NewFromFloat(0.0005), - expectTransactionFee: true, - requiredLogMessage: "confirmed - transmission state visible", + requiredLogMessage: "no matching processor for MetaCapabilityProcessor=test", }, } @@ -289,7 +259,6 @@ func TestWriteTarget_Execute(t *testing.T) { mockTransmissionState(tc, strategy) mockBeholderMessages(tc, emitter) mockTransmit(tc, strategy, emitter) - mockGasEstimation(tc, strategy) mockTransactionFee(tc, strategy) chainSvc := wtmocks.NewChainService(t) @@ -312,7 +281,7 @@ func TestWriteTarget_Execute(t *testing.T) { // Verify transaction fee in response metadata for successful cases if tc.expectTransactionFee && tc.transactionFeeError == nil { require.NotEmpty(t, resp.Metadata.Metering) - require.Equal(t, "ignored_by_engine", resp.Metadata.Metering[0].Peer2PeerID) + require.Empty(t, resp.Metadata.Metering[0].Peer2PeerID) require.Equal(t, "GAS.1", resp.Metadata.Metering[0].SpendUnit) require.Equal(t, tc.transactionFee.String(), resp.Metadata.Metering[0].SpendValue) } @@ -431,18 +400,6 @@ func mockTransmit(tc testCase, strategy *wtmocks.TargetStrategy, emitter *monmoc } } -func mockGasEstimation(tc testCase, strategy *wtmocks.TargetStrategy) { - // Only set up gas estimation mock if we have spend limits - if len(tc.spendLimits) > 0 { - ex := strategy.EXPECT().GetEstimateFee(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) - if tc.gasEstimateError != nil { - ex.Return(commontypes.EstimateFee{}, tc.gasEstimateError) - } else if tc.gasEstimateFee != nil { - ex.Return(*tc.gasEstimateFee, nil) - } - } -} - func mockTransactionFee(tc testCase, strategy *wtmocks.TargetStrategy) { // Only set up transaction fee mock if we expect the execution to reach that point if !tc.expectError && tc.expectTransactionFee { From 4c237fd88c26f18ee9d6654728ba133cdcfd7631 Mon Sep 17 00:00:00 2001 From: patrickhuie19 Date: Tue, 1 Jul 2025 19:51:36 -0400 Subject: [PATCH 10/10] lint --- capabilities/writetarget/write_target_test.go | 33 ++----------------- 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/capabilities/writetarget/write_target_test.go b/capabilities/writetarget/write_target_test.go index 65ebafe..4f5ab81 100644 --- a/capabilities/writetarget/write_target_test.go +++ b/capabilities/writetarget/write_target_test.go @@ -3,7 +3,6 @@ package writetarget_test import ( "context" "errors" - "math/big" "testing" "time" @@ -38,7 +37,6 @@ func setupWriteTarget( chainSvc *wtmocks.ChainService, productSpecificProcessor bool, emitter beholder.ProtoEmitter, - spendLimits []capabilities.SpendLimit, ) (capabilities.ExecutableCapability, capabilities.CapabilityRequest) { platformProcessors, err := processor.NewPlatformProcessors(emitter) require.NoError(t, err) @@ -99,7 +97,6 @@ func setupWriteTarget( WorkflowOwner: repDecoded.WorkflowOwner, WorkflowName: repDecoded.WorkflowName, WorkflowExecutionID: repDecoded.ExecutionID, - SpendLimits: spendLimits, } cfg, err := values.NewMap(map[string]any{"address": "0x1", "processor": "test"}) @@ -127,9 +124,6 @@ type testCase struct { productSpecificProcessor bool requiredLogMessage string // Gas estimation and transaction fee fields - spendLimits []capabilities.SpendLimit - gasEstimateError error - gasEstimateFee *commontypes.EstimateFee transactionFeeError error transactionFee decimal.Decimal expectTransactionFee bool @@ -242,7 +236,6 @@ func TestWriteTarget_Execute(t *testing.T) { initialTransmissionState: writetarget.TransmissionState{Status: writetarget.TransmissionStateNotAttempted}, txState: commontypes.Finalized, expectError: false, - spendLimits: []capabilities.SpendLimit{}, transactionFee: decimal.NewFromFloat(0.0005), expectTransactionFee: true, requiredLogMessage: "no matching processor for MetaCapabilityProcessor=test", @@ -262,13 +255,9 @@ func TestWriteTarget_Execute(t *testing.T) { mockTransactionFee(tc, strategy) chainSvc := wtmocks.NewChainService(t) - // Only set up LatestHead mock if gas estimation doesn't fail - if !(tc.expectError && len(tc.spendLimits) > 0 && tc.gasEstimateFee != nil && tc.gasEstimateFee.Fee.Cmp(big.NewInt(1000000000000000)) > 0) { - chainSvc.EXPECT().LatestHead(mock.Anything). - Return(commontypes.Head{Height: "100"}, nil) - } + chainSvc.EXPECT().LatestHead(mock.Anything).Return(commontypes.Head{Height: "100"}, nil) - target, req := setupWriteTarget(t, lggr, strategy, chainSvc, tc.productSpecificProcessor, emitter, tc.spendLimits) + target, req := setupWriteTarget(t, lggr, strategy, chainSvc, tc.productSpecificProcessor, emitter) resp, err := target.Execute(t.Context(), req) if tc.expectError { @@ -301,7 +290,7 @@ func TestWriteTarget_Execute(t *testing.T) { emitter := monmocks.NewProtoEmitter(t) emitter.EXPECT().EmitWithLog(mock.Anything, mock.Anything, mock.Anything).Return(nil) - target, _ := setupWriteTarget(t, logger.Test(t), strategy, chainSvc, false, emitter, nil) + target, _ := setupWriteTarget(t, logger.Test(t), strategy, chainSvc, false, emitter) inputs, _ := values.NewMap(map[string]any{}) config, _ := values.NewMap(map[string]any{"address": "x", "processor": "y"}) @@ -346,11 +335,6 @@ func TestWriteTarget_Execute(t *testing.T) { } func mockTransmissionState(tc testCase, strategy *wtmocks.TargetStrategy) { - // Skip transmission state mocks if gas estimation fails - if tc.expectError && len(tc.spendLimits) > 0 && tc.gasEstimateFee != nil && tc.gasEstimateFee.Fee.Cmp(big.NewInt(1000000000000000)) > 0 { - return - } - // initial query for transmission state strategy.EXPECT().QueryTransmissionState(mock.Anything, mock.Anything, mock.Anything). Return(&tc.initialTransmissionState, nil).Once() @@ -366,12 +350,6 @@ func mockTransmissionState(tc testCase, strategy *wtmocks.TargetStrategy) { } func mockBeholderMessages(tc testCase, emitter *monmocks.ProtoEmitter) { - // For gas estimation errors, only expect WriteError (no WriteInitiated) - if tc.expectError && len(tc.spendLimits) > 0 && tc.gasEstimateFee != nil && tc.gasEstimateFee.Fee.Cmp(big.NewInt(1000000000000000)) > 0 { - emitter.EXPECT().EmitWithLog(mock.Anything, mock.AnythingOfType("*writetarget.WriteError"), mock.Anything, mock.Anything).Return(nil).Once() - return - } - // Ensure the correct beholder messages are emitted for each case emitter.EXPECT().EmitWithLog(mock.Anything, mock.AnythingOfType("*writetarget.WriteInitiated"), mock.Anything).Return(nil).Once() if tc.expectError { @@ -383,11 +361,6 @@ func mockBeholderMessages(tc testCase, emitter *monmocks.ProtoEmitter) { } func mockTransmit(tc testCase, strategy *wtmocks.TargetStrategy, emitter *monmocks.ProtoEmitter) { - // Skip transmission mocks if gas estimation fails - if tc.expectError && len(tc.spendLimits) > 0 && tc.gasEstimateFee != nil && tc.gasEstimateFee.Fee.Cmp(big.NewInt(1000000000000000)) > 0 { - return - } - if tc.txState != commontypes.Unknown { ex := strategy.EXPECT().TransmitReport(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) if tc.simulateTxError {