77 "encoding/hex"
88 "errors"
99 "fmt"
10+ "math/big"
1011 "time"
1112
1213 "go.opentelemetry.io/otel/attribute"
@@ -57,6 +58,8 @@ type TargetStrategy interface {
5758 TransmitReport (ctx context.Context , report []byte , reportContext []byte , signatures [][]byte , request capabilities.CapabilityRequest ) (string , error )
5859 // Wrapper around the ChainWriter to get the transaction status
5960 GetTransactionStatus (ctx context.Context , transactionID string ) (commontypes.TransactionStatus , error )
61+ // Wrapper around the ChainWriter to get the fee esimate
62+ GetEstimateFee (ctx context.Context , contract string , method string , args any , toAddress string , meta * commontypes.TxMeta , val * big.Int ) (commontypes.EstimateFee , error )
6063}
6164
6265var (
@@ -74,6 +77,7 @@ const (
7477
7578type chainService interface {
7679 LatestHead (ctx context.Context ) (commontypes.Head , error )
80+ GetTransactionFee (ctx context.Context , transactionID string ) (commontypes.ChainFeeComponents , error )
7781}
7882
7983type writeTarget struct {
@@ -175,6 +179,53 @@ func success() capabilities.CapabilityResponse {
175179 return capabilities.CapabilityResponse {}
176180}
177181
182+ // getGasSpendLimit returns the gas spend limit for the given chain ID from the request metadata
183+ func (c * writeTarget ) getGasSpendLimit (request capabilities.CapabilityRequest ) (string , error ) {
184+ spendType := "GAS." + c .chainInfo .ChainID
185+
186+ for _ , limit := range request .Metadata .SpendLimits {
187+ if spendType == string (limit .SpendType ) {
188+ return limit .Limit , nil
189+ }
190+ }
191+ return "" , fmt .Errorf ("no gas spend limit found for chain %s" , c .chainInfo .ChainID )
192+ }
193+
194+ // checkGasEstimate verifies if the estimated gas fee is within the spend limit and returns the fee
195+ func (c * writeTarget ) checkGasEstimate (ctx context.Context , request capabilities.CapabilityRequest ) (* big.Int , uint32 , error ) {
196+ spendLimit , err := c .getGasSpendLimit (request )
197+ if err != nil {
198+ return nil , 0 , fmt .Errorf ("failed to get gas spend limit, not performing gas estimation: %w" , err )
199+ }
200+
201+ // Get gas estimate from ContractWriter
202+ fee , err := c .targetStrategy .GetEstimateFee (ctx , "" , "" , nil , "" , nil , nil )
203+ if err != nil {
204+ return nil , 0 , fmt .Errorf ("failed to get gas estimate: %w" , err )
205+ }
206+
207+ // Convert spend limit from ETH to wei
208+ limitFloat , ok := new (big.Float ).SetString (spendLimit )
209+ if ! ok {
210+ return nil , 0 , fmt .Errorf ("invalid gas spend limit format: %s" , spendLimit )
211+ }
212+
213+ // Multiply by 10^decimals to convert from ETH to wei
214+ multiplier := new (big.Float ).SetInt (new (big.Int ).Exp (big .NewInt (10 ), big .NewInt (int64 (fee .Decimals )), nil ))
215+ limitFloat .Mul (limitFloat , multiplier )
216+
217+ // Convert to big.Int for comparison
218+ limit := new (big.Int )
219+ limitFloat .Int (limit )
220+
221+ // Compare estimate with limit
222+ if fee .Fee .Cmp (limit ) > 0 {
223+ return nil , 0 , fmt .Errorf ("estimated gas fee %s exceeds spend limit %s" , fee .Fee .String (), limit .String ())
224+ }
225+
226+ return fee .Fee , fee .Decimals , nil
227+ }
228+
178229func (c * writeTarget ) Execute (ctx context.Context , request capabilities.CapabilityRequest ) (capabilities.CapabilityResponse , error ) {
179230 // Take the local timestamp
180231 tsStart := time .Now ().UnixMilli ()
@@ -189,6 +240,24 @@ func (c *writeTarget) Execute(ctx context.Context, request capabilities.Capabili
189240
190241 c .lggr .Debugw ("Execute" , "request" , request , "capInfo" , capInfo )
191242
243+ // Check gas estimate before proceeding
244+ // TODO: discuss if we should release this in a separate PR
245+ fee , _ , err := c .checkGasEstimate (ctx , request )
246+ if err != nil {
247+ // Build error message
248+ info := & requestInfo {
249+ tsStart : tsStart ,
250+ node : c .nodeAddress ,
251+ request : request ,
252+ }
253+ errMsg := c .asEmittedError (ctx , & wt.WriteError {
254+ Code : uint32 (TransmissionStateFatal ),
255+ Summary : "InsufficientFunds" ,
256+ Cause : err .Error (),
257+ }, "info" , info )
258+ return capabilities.CapabilityResponse {}, errMsg
259+ }
260+
192261 // Helper to keep track of the request info
193262 info := requestInfo {
194263 tsStart : tsStart ,
@@ -342,7 +411,25 @@ func (c *writeTarget) Execute(ctx context.Context, request capabilities.Capabili
342411 if err != nil {
343412 return capabilities.CapabilityResponse {}, err
344413 }
345- return success (), nil
414+
415+ // Get the transaction fee
416+ feeComponents , err := c .cs .GetTransactionFee (ctx , txID )
417+ // TODO: implement in EVM + Aptos chain service
418+ if err != nil {
419+ return capabilities.CapabilityResponse {}, fmt .Errorf ("failed to get transaction fee: %w" , err )
420+ }
421+
422+ return capabilities.CapabilityResponse {
423+ Metadata : capabilities.ResponseMetadata {
424+ Metering : []capabilities.MeteringNodeDetail {
425+ {
426+ Peer2PeerID : "ignored_by_engine" ,
427+ SpendUnit : "GAS." + c .chainInfo .ChainID ,
428+ SpendValue : feeComponents .ExecutionFee .String (),
429+ },
430+ },
431+ },
432+ }, nil
346433}
347434
348435func (c * writeTarget ) RegisterToWorkflow (ctx context.Context , request capabilities.RegisterToWorkflowRequest ) error {
0 commit comments