From ea5d3879a9478f86432d722e4a139a7f3e27a1da Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Fri, 13 Mar 2026 09:38:31 +0000 Subject: [PATCH 1/4] feat: add SimulationOptions to ContractWriter.SubmitTransaction Replace the trailing expectedSimulationFailureRules parameter with *SimulationOptions, a struct wrapping both SimulateTransaction (bool) and ExpectedSimulationFailureErrors. The pointer type makes it nil-able so existing callers can pass nil to use the implementation's default simulation behavior. Changes: - Add SimulationOptions and ExpectedSimulationFailureError types - Update ContractWriter interface and UnimplementedContractWriter - Update proto schemas (contract_writer.proto, aptos.proto) with SimulationOptions message - Regenerate proto Go files - Update LOOP bridge client/server and converters - Add SimulationOptions to Aptos SubmitTransactionRequest Go type and wire up proto converters - Update all callers and test helpers --- pkg/chains/aptos/aptos.pb.go | 210 +++++++++++++---- pkg/chains/aptos/aptos.proto | 10 + pkg/chains/aptos/proto_helpers.go | 24 ++ pkg/loop/internal/pb/contract_writer.pb.go | 222 +++++++++++++----- pkg/loop/internal/pb/contract_writer.proto | 10 + .../contractreader/contract_reader_test.go | 2 +- .../contractwriter/contract_writer.go | 19 +- .../contractwriter/converters.go | 48 ++++ .../contractwriter/converters_test.go | 69 ++++++ pkg/types/chains/aptos/aptos.go | 23 +- pkg/types/contract_writer.go | 48 +++- pkg/types/interfacetests/utils.go | 4 +- 12 files changed, 571 insertions(+), 118 deletions(-) diff --git a/pkg/chains/aptos/aptos.pb.go b/pkg/chains/aptos/aptos.pb.go index 1578de42ca..52e713de2f 100644 --- a/pkg/chains/aptos/aptos.pb.go +++ b/pkg/chains/aptos/aptos.pb.go @@ -1037,12 +1037,13 @@ func (x *AccountTransactionsReply) GetTransactions() []*Transaction { } type SubmitTransactionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ReceiverModuleId *ModuleID `protobuf:"bytes,1,opt,name=receiver_module_id,json=receiverModuleId,proto3" json:"receiver_module_id,omitempty"` - EncodedPayload []byte `protobuf:"bytes,2,opt,name=encoded_payload,json=encodedPayload,proto3" json:"encoded_payload,omitempty"` - GasConfig *GasConfig `protobuf:"bytes,3,opt,name=gas_config,json=gasConfig,proto3,oneof" json:"gas_config,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + ReceiverModuleId *ModuleID `protobuf:"bytes,1,opt,name=receiver_module_id,json=receiverModuleId,proto3" json:"receiver_module_id,omitempty"` + EncodedPayload []byte `protobuf:"bytes,2,opt,name=encoded_payload,json=encodedPayload,proto3" json:"encoded_payload,omitempty"` + GasConfig *GasConfig `protobuf:"bytes,3,opt,name=gas_config,json=gasConfig,proto3,oneof" json:"gas_config,omitempty"` + SimulationOptions *SimulationOptions `protobuf:"bytes,4,opt,name=simulation_options,json=simulationOptions,proto3" json:"simulation_options,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SubmitTransactionRequest) Reset() { @@ -1096,6 +1097,13 @@ func (x *SubmitTransactionRequest) GetGasConfig() *GasConfig { return nil } +func (x *SubmitTransactionRequest) GetSimulationOptions() *SimulationOptions { + if x != nil { + return x.SimulationOptions + } + return nil +} + type SubmitTransactionReply struct { state protoimpl.MessageState `protogen:"open.v1"` TxStatus TxStatus `protobuf:"varint,1,opt,name=tx_status,json=txStatus,proto3,enum=loop.aptos.TxStatus" json:"tx_status,omitempty"` @@ -1208,6 +1216,102 @@ func (x *GasConfig) GetGasUnitPrice() uint64 { return 0 } +type SimulationOptions struct { + state protoimpl.MessageState `protogen:"open.v1"` + SimulateTransaction bool `protobuf:"varint,1,opt,name=simulate_transaction,json=simulateTransaction,proto3" json:"simulate_transaction,omitempty"` + ExpectedSimulationFailureErrors []*ExpectedSimulationFailureError `protobuf:"bytes,2,rep,name=expected_simulation_failure_errors,json=expectedSimulationFailureErrors,proto3" json:"expected_simulation_failure_errors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SimulationOptions) Reset() { + *x = SimulationOptions{} + mi := &file_aptos_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SimulationOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SimulationOptions) ProtoMessage() {} + +func (x *SimulationOptions) ProtoReflect() protoreflect.Message { + mi := &file_aptos_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SimulationOptions.ProtoReflect.Descriptor instead. +func (*SimulationOptions) Descriptor() ([]byte, []int) { + return file_aptos_proto_rawDescGZIP(), []int{18} +} + +func (x *SimulationOptions) GetSimulateTransaction() bool { + if x != nil { + return x.SimulateTransaction + } + return false +} + +func (x *SimulationOptions) GetExpectedSimulationFailureErrors() []*ExpectedSimulationFailureError { + if x != nil { + return x.ExpectedSimulationFailureErrors + } + return nil +} + +type ExpectedSimulationFailureError struct { + state protoimpl.MessageState `protogen:"open.v1"` + ErrorString string `protobuf:"bytes,1,opt,name=error_string,json=errorString,proto3" json:"error_string,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExpectedSimulationFailureError) Reset() { + *x = ExpectedSimulationFailureError{} + mi := &file_aptos_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExpectedSimulationFailureError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExpectedSimulationFailureError) ProtoMessage() {} + +func (x *ExpectedSimulationFailureError) ProtoReflect() protoreflect.Message { + mi := &file_aptos_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExpectedSimulationFailureError.ProtoReflect.Descriptor instead. +func (*ExpectedSimulationFailureError) Descriptor() ([]byte, []int) { + return file_aptos_proto_rawDescGZIP(), []int{19} +} + +func (x *ExpectedSimulationFailureError) GetErrorString() string { + if x != nil { + return x.ErrorString + } + return "" +} + var File_aptos_proto protoreflect.FileDescriptor const file_aptos_proto_rawDesc = "" + @@ -1269,12 +1373,13 @@ const file_aptos_proto_rawDesc = "" + "\x06_startB\b\n" + "\x06_limit\"W\n" + "\x18AccountTransactionsReply\x12;\n" + - "\ftransactions\x18\x01 \x03(\v2\x17.loop.aptos.TransactionR\ftransactions\"\xd1\x01\n" + + "\ftransactions\x18\x01 \x03(\v2\x17.loop.aptos.TransactionR\ftransactions\"\x9f\x02\n" + "\x18SubmitTransactionRequest\x12B\n" + "\x12receiver_module_id\x18\x01 \x01(\v2\x14.loop.aptos.ModuleIDR\x10receiverModuleId\x12'\n" + "\x0fencoded_payload\x18\x02 \x01(\fR\x0eencodedPayload\x129\n" + "\n" + - "gas_config\x18\x03 \x01(\v2\x15.loop.aptos.GasConfigH\x00R\tgasConfig\x88\x01\x01B\r\n" + + "gas_config\x18\x03 \x01(\v2\x15.loop.aptos.GasConfigH\x00R\tgasConfig\x88\x01\x01\x12L\n" + + "\x12simulation_options\x18\x04 \x01(\v2\x1d.loop.aptos.SimulationOptionsR\x11simulationOptionsB\r\n" + "\v_gas_config\"\x92\x01\n" + "\x16SubmitTransactionReply\x121\n" + "\ttx_status\x18\x01 \x01(\x0e2\x14.loop.aptos.TxStatusR\btxStatus\x12\x17\n" + @@ -1282,7 +1387,12 @@ const file_aptos_proto_rawDesc = "" + "\x12tx_idempotency_key\x18\x03 \x01(\tR\x10txIdempotencyKey\"W\n" + "\tGasConfig\x12$\n" + "\x0emax_gas_amount\x18\x01 \x01(\x04R\fmaxGasAmount\x12$\n" + - "\x0egas_unit_price\x18\x02 \x01(\x04R\fgasUnitPrice*\xb4\x02\n" + + "\x0egas_unit_price\x18\x02 \x01(\x04R\fgasUnitPrice\"\xbf\x01\n" + + "\x11SimulationOptions\x121\n" + + "\x14simulate_transaction\x18\x01 \x01(\bR\x13simulateTransaction\x12w\n" + + "\"expected_simulation_failure_errors\x18\x02 \x03(\v2*.loop.aptos.ExpectedSimulationFailureErrorR\x1fexpectedSimulationFailureErrors\"C\n" + + "\x1eExpectedSimulationFailureError\x12!\n" + + "\ferror_string\x18\x01 \x01(\tR\verrorString*\xb4\x02\n" + "\vTypeTagKind\x12\x16\n" + "\x12TYPE_TAG_KIND_BOOL\x10\x00\x12\x14\n" + "\x10TYPE_TAG_KIND_U8\x10\x01\x12\x15\n" + @@ -1330,29 +1440,31 @@ func file_aptos_proto_rawDescGZIP() []byte { } var file_aptos_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_aptos_proto_msgTypes = make([]protoimpl.MessageInfo, 18) +var file_aptos_proto_msgTypes = make([]protoimpl.MessageInfo, 20) var file_aptos_proto_goTypes = []any{ - (TypeTagKind)(0), // 0: loop.aptos.TypeTagKind - (TransactionVariant)(0), // 1: loop.aptos.TransactionVariant - (TxStatus)(0), // 2: loop.aptos.TxStatus - (*AccountAPTBalanceRequest)(nil), // 3: loop.aptos.AccountAPTBalanceRequest - (*AccountAPTBalanceReply)(nil), // 4: loop.aptos.AccountAPTBalanceReply - (*ViewRequest)(nil), // 5: loop.aptos.ViewRequest - (*ViewReply)(nil), // 6: loop.aptos.ViewReply - (*ViewPayload)(nil), // 7: loop.aptos.ViewPayload - (*ModuleID)(nil), // 8: loop.aptos.ModuleID - (*TypeTag)(nil), // 9: loop.aptos.TypeTag - (*VectorTag)(nil), // 10: loop.aptos.VectorTag - (*StructTag)(nil), // 11: loop.aptos.StructTag - (*GenericTag)(nil), // 12: loop.aptos.GenericTag - (*TransactionByHashRequest)(nil), // 13: loop.aptos.TransactionByHashRequest - (*TransactionByHashReply)(nil), // 14: loop.aptos.TransactionByHashReply - (*Transaction)(nil), // 15: loop.aptos.Transaction - (*AccountTransactionsRequest)(nil), // 16: loop.aptos.AccountTransactionsRequest - (*AccountTransactionsReply)(nil), // 17: loop.aptos.AccountTransactionsReply - (*SubmitTransactionRequest)(nil), // 18: loop.aptos.SubmitTransactionRequest - (*SubmitTransactionReply)(nil), // 19: loop.aptos.SubmitTransactionReply - (*GasConfig)(nil), // 20: loop.aptos.GasConfig + (TypeTagKind)(0), // 0: loop.aptos.TypeTagKind + (TransactionVariant)(0), // 1: loop.aptos.TransactionVariant + (TxStatus)(0), // 2: loop.aptos.TxStatus + (*AccountAPTBalanceRequest)(nil), // 3: loop.aptos.AccountAPTBalanceRequest + (*AccountAPTBalanceReply)(nil), // 4: loop.aptos.AccountAPTBalanceReply + (*ViewRequest)(nil), // 5: loop.aptos.ViewRequest + (*ViewReply)(nil), // 6: loop.aptos.ViewReply + (*ViewPayload)(nil), // 7: loop.aptos.ViewPayload + (*ModuleID)(nil), // 8: loop.aptos.ModuleID + (*TypeTag)(nil), // 9: loop.aptos.TypeTag + (*VectorTag)(nil), // 10: loop.aptos.VectorTag + (*StructTag)(nil), // 11: loop.aptos.StructTag + (*GenericTag)(nil), // 12: loop.aptos.GenericTag + (*TransactionByHashRequest)(nil), // 13: loop.aptos.TransactionByHashRequest + (*TransactionByHashReply)(nil), // 14: loop.aptos.TransactionByHashReply + (*Transaction)(nil), // 15: loop.aptos.Transaction + (*AccountTransactionsRequest)(nil), // 16: loop.aptos.AccountTransactionsRequest + (*AccountTransactionsReply)(nil), // 17: loop.aptos.AccountTransactionsReply + (*SubmitTransactionRequest)(nil), // 18: loop.aptos.SubmitTransactionRequest + (*SubmitTransactionReply)(nil), // 19: loop.aptos.SubmitTransactionReply + (*GasConfig)(nil), // 20: loop.aptos.GasConfig + (*SimulationOptions)(nil), // 21: loop.aptos.SimulationOptions + (*ExpectedSimulationFailureError)(nil), // 22: loop.aptos.ExpectedSimulationFailureError } var file_aptos_proto_depIdxs = []int32{ 7, // 0: loop.aptos.ViewRequest.payload:type_name -> loop.aptos.ViewPayload @@ -1369,22 +1481,24 @@ var file_aptos_proto_depIdxs = []int32{ 15, // 11: loop.aptos.AccountTransactionsReply.transactions:type_name -> loop.aptos.Transaction 8, // 12: loop.aptos.SubmitTransactionRequest.receiver_module_id:type_name -> loop.aptos.ModuleID 20, // 13: loop.aptos.SubmitTransactionRequest.gas_config:type_name -> loop.aptos.GasConfig - 2, // 14: loop.aptos.SubmitTransactionReply.tx_status:type_name -> loop.aptos.TxStatus - 3, // 15: loop.aptos.Aptos.AccountAPTBalance:input_type -> loop.aptos.AccountAPTBalanceRequest - 5, // 16: loop.aptos.Aptos.View:input_type -> loop.aptos.ViewRequest - 13, // 17: loop.aptos.Aptos.TransactionByHash:input_type -> loop.aptos.TransactionByHashRequest - 16, // 18: loop.aptos.Aptos.AccountTransactions:input_type -> loop.aptos.AccountTransactionsRequest - 18, // 19: loop.aptos.Aptos.SubmitTransaction:input_type -> loop.aptos.SubmitTransactionRequest - 4, // 20: loop.aptos.Aptos.AccountAPTBalance:output_type -> loop.aptos.AccountAPTBalanceReply - 6, // 21: loop.aptos.Aptos.View:output_type -> loop.aptos.ViewReply - 14, // 22: loop.aptos.Aptos.TransactionByHash:output_type -> loop.aptos.TransactionByHashReply - 17, // 23: loop.aptos.Aptos.AccountTransactions:output_type -> loop.aptos.AccountTransactionsReply - 19, // 24: loop.aptos.Aptos.SubmitTransaction:output_type -> loop.aptos.SubmitTransactionReply - 20, // [20:25] is the sub-list for method output_type - 15, // [15:20] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 21, // 14: loop.aptos.SubmitTransactionRequest.simulation_options:type_name -> loop.aptos.SimulationOptions + 2, // 15: loop.aptos.SubmitTransactionReply.tx_status:type_name -> loop.aptos.TxStatus + 22, // 16: loop.aptos.SimulationOptions.expected_simulation_failure_errors:type_name -> loop.aptos.ExpectedSimulationFailureError + 3, // 17: loop.aptos.Aptos.AccountAPTBalance:input_type -> loop.aptos.AccountAPTBalanceRequest + 5, // 18: loop.aptos.Aptos.View:input_type -> loop.aptos.ViewRequest + 13, // 19: loop.aptos.Aptos.TransactionByHash:input_type -> loop.aptos.TransactionByHashRequest + 16, // 20: loop.aptos.Aptos.AccountTransactions:input_type -> loop.aptos.AccountTransactionsRequest + 18, // 21: loop.aptos.Aptos.SubmitTransaction:input_type -> loop.aptos.SubmitTransactionRequest + 4, // 22: loop.aptos.Aptos.AccountAPTBalance:output_type -> loop.aptos.AccountAPTBalanceReply + 6, // 23: loop.aptos.Aptos.View:output_type -> loop.aptos.ViewReply + 14, // 24: loop.aptos.Aptos.TransactionByHash:output_type -> loop.aptos.TransactionByHashReply + 17, // 25: loop.aptos.Aptos.AccountTransactions:output_type -> loop.aptos.AccountTransactionsReply + 19, // 26: loop.aptos.Aptos.SubmitTransaction:output_type -> loop.aptos.SubmitTransactionReply + 22, // [22:27] is the sub-list for method output_type + 17, // [17:22] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_aptos_proto_init() } @@ -1407,7 +1521,7 @@ func file_aptos_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_aptos_proto_rawDesc), len(file_aptos_proto_rawDesc)), NumEnums: 3, - NumMessages: 18, + NumMessages: 20, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/chains/aptos/aptos.proto b/pkg/chains/aptos/aptos.proto index da9be7b348..67410b0a3e 100644 --- a/pkg/chains/aptos/aptos.proto +++ b/pkg/chains/aptos/aptos.proto @@ -130,6 +130,7 @@ message SubmitTransactionRequest { ModuleID receiver_module_id = 1; bytes encoded_payload = 2; optional GasConfig gas_config = 3; + SimulationOptions simulation_options = 4; } enum TxStatus { @@ -149,3 +150,12 @@ message GasConfig { uint64 gas_unit_price = 2; // Price per gas unit in octas } +message SimulationOptions { + bool simulate_transaction = 1; + repeated ExpectedSimulationFailureError expected_simulation_failure_errors = 2; +} + +message ExpectedSimulationFailureError { + string error_string = 1; +} + diff --git a/pkg/chains/aptos/proto_helpers.go b/pkg/chains/aptos/proto_helpers.go index ce1573a0fc..0bc1e3ad11 100644 --- a/pkg/chains/aptos/proto_helpers.go +++ b/pkg/chains/aptos/proto_helpers.go @@ -434,6 +434,18 @@ func ConvertSubmitTransactionRequestToProto(req typeaptos.SubmitTransactionReque } } + if req.SimulationOptions != nil { + protoOpts := &SimulationOptions{ + SimulateTransaction: req.SimulationOptions.SimulateTransaction, + } + for _, rule := range req.SimulationOptions.ExpectedSimulationFailureErrors { + protoOpts.ExpectedSimulationFailureErrors = append(protoOpts.ExpectedSimulationFailureErrors, &ExpectedSimulationFailureError{ + ErrorString: rule.ErrorString, + }) + } + protoReq.SimulationOptions = protoOpts + } + return protoReq, nil } @@ -468,6 +480,18 @@ func ConvertSubmitTransactionRequestFromProto(proto *SubmitTransactionRequest) ( } } + if proto.SimulationOptions != nil { + opts := &typeaptos.SimulationOptions{ + SimulateTransaction: proto.SimulationOptions.SimulateTransaction, + } + for _, rule := range proto.SimulationOptions.ExpectedSimulationFailureErrors { + opts.ExpectedSimulationFailureErrors = append(opts.ExpectedSimulationFailureErrors, typeaptos.ExpectedSimulationFailureError{ + ErrorString: rule.GetErrorString(), + }) + } + req.SimulationOptions = opts + } + return req, nil } diff --git a/pkg/loop/internal/pb/contract_writer.pb.go b/pkg/loop/internal/pb/contract_writer.pb.go index 01e0e872ab..303c3a27a9 100644 --- a/pkg/loop/internal/pb/contract_writer.pb.go +++ b/pkg/loop/internal/pb/contract_writer.pb.go @@ -25,16 +25,17 @@ const ( ) type SubmitTransactionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ContractName string `protobuf:"bytes,1,opt,name=contract_name,json=contractName,proto3" json:"contract_name,omitempty"` - Method string `protobuf:"bytes,2,opt,name=method,proto3" json:"method,omitempty"` - Params *codec.VersionedBytes `protobuf:"bytes,3,opt,name=params,proto3" json:"params,omitempty"` - TransactionId string `protobuf:"bytes,4,opt,name=transaction_id,json=transactionId,proto3" json:"transaction_id,omitempty"` - ToAddress string `protobuf:"bytes,5,opt,name=to_address,json=toAddress,proto3" json:"to_address,omitempty"` - Meta *TransactionMeta `protobuf:"bytes,6,opt,name=meta,proto3" json:"meta,omitempty"` - Value *BigInt `protobuf:"bytes,7,opt,name=value,proto3" json:"value,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + ContractName string `protobuf:"bytes,1,opt,name=contract_name,json=contractName,proto3" json:"contract_name,omitempty"` + Method string `protobuf:"bytes,2,opt,name=method,proto3" json:"method,omitempty"` + Params *codec.VersionedBytes `protobuf:"bytes,3,opt,name=params,proto3" json:"params,omitempty"` + TransactionId string `protobuf:"bytes,4,opt,name=transaction_id,json=transactionId,proto3" json:"transaction_id,omitempty"` + ToAddress string `protobuf:"bytes,5,opt,name=to_address,json=toAddress,proto3" json:"to_address,omitempty"` + Meta *TransactionMeta `protobuf:"bytes,6,opt,name=meta,proto3" json:"meta,omitempty"` + Value *BigInt `protobuf:"bytes,7,opt,name=value,proto3" json:"value,omitempty"` + SimulationOptions *SimulationOptions `protobuf:"bytes,8,opt,name=simulation_options,json=simulationOptions,proto3" json:"simulation_options,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SubmitTransactionRequest) Reset() { @@ -116,6 +117,13 @@ func (x *SubmitTransactionRequest) GetValue() *BigInt { return nil } +func (x *SubmitTransactionRequest) GetSimulationOptions() *SimulationOptions { + if x != nil { + return x.SimulationOptions + } + return nil +} + type TransactionMeta struct { state protoimpl.MessageState `protogen:"open.v1"` WorkflowExecutionId string `protobuf:"bytes,1,opt,name=workflow_execution_id,json=workflowExecutionId,proto3" json:"workflow_execution_id,omitempty"` @@ -168,6 +176,102 @@ func (x *TransactionMeta) GetGasLimit() *BigInt { return nil } +type SimulationOptions struct { + state protoimpl.MessageState `protogen:"open.v1"` + SimulateTransaction bool `protobuf:"varint,1,opt,name=simulate_transaction,json=simulateTransaction,proto3" json:"simulate_transaction,omitempty"` + ExpectedSimulationFailureErrors []*ExpectedSimulationFailureError `protobuf:"bytes,2,rep,name=expected_simulation_failure_errors,json=expectedSimulationFailureErrors,proto3" json:"expected_simulation_failure_errors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SimulationOptions) Reset() { + *x = SimulationOptions{} + mi := &file_contract_writer_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SimulationOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SimulationOptions) ProtoMessage() {} + +func (x *SimulationOptions) ProtoReflect() protoreflect.Message { + mi := &file_contract_writer_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SimulationOptions.ProtoReflect.Descriptor instead. +func (*SimulationOptions) Descriptor() ([]byte, []int) { + return file_contract_writer_proto_rawDescGZIP(), []int{2} +} + +func (x *SimulationOptions) GetSimulateTransaction() bool { + if x != nil { + return x.SimulateTransaction + } + return false +} + +func (x *SimulationOptions) GetExpectedSimulationFailureErrors() []*ExpectedSimulationFailureError { + if x != nil { + return x.ExpectedSimulationFailureErrors + } + return nil +} + +type ExpectedSimulationFailureError struct { + state protoimpl.MessageState `protogen:"open.v1"` + ErrorString string `protobuf:"bytes,1,opt,name=error_string,json=errorString,proto3" json:"error_string,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExpectedSimulationFailureError) Reset() { + *x = ExpectedSimulationFailureError{} + mi := &file_contract_writer_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExpectedSimulationFailureError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExpectedSimulationFailureError) ProtoMessage() {} + +func (x *ExpectedSimulationFailureError) ProtoReflect() protoreflect.Message { + mi := &file_contract_writer_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExpectedSimulationFailureError.ProtoReflect.Descriptor instead. +func (*ExpectedSimulationFailureError) Descriptor() ([]byte, []int) { + return file_contract_writer_proto_rawDescGZIP(), []int{3} +} + +func (x *ExpectedSimulationFailureError) GetErrorString() string { + if x != nil { + return x.ErrorString + } + return "" +} + // GetEstimateFeeReply has arguments for [github.com/smartcontractkit/chainlink-common/pkg/types.ContractWriter.GetEstimateFee]. type GetEstimateFeeRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -183,7 +287,7 @@ type GetEstimateFeeRequest struct { func (x *GetEstimateFeeRequest) Reset() { *x = GetEstimateFeeRequest{} - mi := &file_contract_writer_proto_msgTypes[2] + mi := &file_contract_writer_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -195,7 +299,7 @@ func (x *GetEstimateFeeRequest) String() string { func (*GetEstimateFeeRequest) ProtoMessage() {} func (x *GetEstimateFeeRequest) ProtoReflect() protoreflect.Message { - mi := &file_contract_writer_proto_msgTypes[2] + mi := &file_contract_writer_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -208,7 +312,7 @@ func (x *GetEstimateFeeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetEstimateFeeRequest.ProtoReflect.Descriptor instead. func (*GetEstimateFeeRequest) Descriptor() ([]byte, []int) { - return file_contract_writer_proto_rawDescGZIP(), []int{2} + return file_contract_writer_proto_rawDescGZIP(), []int{4} } func (x *GetEstimateFeeRequest) GetContractName() string { @@ -264,7 +368,7 @@ type GetFeeComponentsReply struct { func (x *GetFeeComponentsReply) Reset() { *x = GetFeeComponentsReply{} - mi := &file_contract_writer_proto_msgTypes[3] + mi := &file_contract_writer_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -276,7 +380,7 @@ func (x *GetFeeComponentsReply) String() string { func (*GetFeeComponentsReply) ProtoMessage() {} func (x *GetFeeComponentsReply) ProtoReflect() protoreflect.Message { - mi := &file_contract_writer_proto_msgTypes[3] + mi := &file_contract_writer_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -289,7 +393,7 @@ func (x *GetFeeComponentsReply) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFeeComponentsReply.ProtoReflect.Descriptor instead. func (*GetFeeComponentsReply) Descriptor() ([]byte, []int) { - return file_contract_writer_proto_rawDescGZIP(), []int{3} + return file_contract_writer_proto_rawDescGZIP(), []int{5} } func (x *GetFeeComponentsReply) GetExecutionFee() *BigInt { @@ -317,7 +421,7 @@ type GetEstimateFeeReply struct { func (x *GetEstimateFeeReply) Reset() { *x = GetEstimateFeeReply{} - mi := &file_contract_writer_proto_msgTypes[4] + mi := &file_contract_writer_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -329,7 +433,7 @@ func (x *GetEstimateFeeReply) String() string { func (*GetEstimateFeeReply) ProtoMessage() {} func (x *GetEstimateFeeReply) ProtoReflect() protoreflect.Message { - mi := &file_contract_writer_proto_msgTypes[4] + mi := &file_contract_writer_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -342,7 +446,7 @@ func (x *GetEstimateFeeReply) ProtoReflect() protoreflect.Message { // Deprecated: Use GetEstimateFeeReply.ProtoReflect.Descriptor instead. func (*GetEstimateFeeReply) Descriptor() ([]byte, []int) { - return file_contract_writer_proto_rawDescGZIP(), []int{4} + return file_contract_writer_proto_rawDescGZIP(), []int{6} } func (x *GetEstimateFeeReply) GetFee() *BigInt { @@ -363,7 +467,7 @@ var File_contract_writer_proto protoreflect.FileDescriptor const file_contract_writer_proto_rawDesc = "" + "\n" + - "\x15contract_writer.proto\x12\x04loop\x1a\x1ainternal/codec/codec.proto\x1a\x1eloop/internal/pb/relayer.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x14chains/evm/evm.proto\"\x9b\x02\n" + + "\x15contract_writer.proto\x12\x04loop\x1a\x1ainternal/codec/codec.proto\x1a\x1eloop/internal/pb/relayer.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x14chains/evm/evm.proto\"\xe3\x02\n" + "\x18SubmitTransactionRequest\x12#\n" + "\rcontract_name\x18\x01 \x01(\tR\fcontractName\x12\x16\n" + "\x06method\x18\x02 \x01(\tR\x06method\x12-\n" + @@ -372,10 +476,16 @@ const file_contract_writer_proto_rawDesc = "" + "\n" + "to_address\x18\x05 \x01(\tR\ttoAddress\x12)\n" + "\x04meta\x18\x06 \x01(\v2\x15.loop.TransactionMetaR\x04meta\x12\"\n" + - "\x05value\x18\a \x01(\v2\f.loop.BigIntR\x05value\"p\n" + + "\x05value\x18\a \x01(\v2\f.loop.BigIntR\x05value\x12F\n" + + "\x12simulation_options\x18\b \x01(\v2\x17.loop.SimulationOptionsR\x11simulationOptions\"p\n" + "\x0fTransactionMeta\x122\n" + "\x15workflow_execution_id\x18\x01 \x01(\tR\x13workflowExecutionId\x12)\n" + - "\tgas_limit\x18\x02 \x01(\v2\f.loop.BigIntR\bgasLimit\"\xf1\x01\n" + + "\tgas_limit\x18\x02 \x01(\v2\f.loop.BigIntR\bgasLimit\"\xb9\x01\n" + + "\x11SimulationOptions\x121\n" + + "\x14simulate_transaction\x18\x01 \x01(\bR\x13simulateTransaction\x12q\n" + + "\"expected_simulation_failure_errors\x18\x02 \x03(\v2$.loop.ExpectedSimulationFailureErrorR\x1fexpectedSimulationFailureErrors\"C\n" + + "\x1eExpectedSimulationFailureError\x12!\n" + + "\ferror_string\x18\x01 \x01(\tR\verrorString\"\xf1\x01\n" + "\x15GetEstimateFeeRequest\x12#\n" + "\rcontract_name\x18\x01 \x01(\tR\fcontractName\x12\x16\n" + "\x06method\x18\x02 \x01(\tR\x06method\x12-\n" + @@ -408,43 +518,47 @@ func file_contract_writer_proto_rawDescGZIP() []byte { return file_contract_writer_proto_rawDescData } -var file_contract_writer_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_contract_writer_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_contract_writer_proto_goTypes = []any{ (*SubmitTransactionRequest)(nil), // 0: loop.SubmitTransactionRequest (*TransactionMeta)(nil), // 1: loop.TransactionMeta - (*GetEstimateFeeRequest)(nil), // 2: loop.GetEstimateFeeRequest - (*GetFeeComponentsReply)(nil), // 3: loop.GetFeeComponentsReply - (*GetEstimateFeeReply)(nil), // 4: loop.GetEstimateFeeReply - (*codec.VersionedBytes)(nil), // 5: codec.VersionedBytes - (*BigInt)(nil), // 6: loop.BigInt - (*evm.GetTransactionStatusRequest)(nil), // 7: loop.evm.GetTransactionStatusRequest - (*emptypb.Empty)(nil), // 8: google.protobuf.Empty - (*evm.GetTransactionStatusReply)(nil), // 9: loop.evm.GetTransactionStatusReply + (*SimulationOptions)(nil), // 2: loop.SimulationOptions + (*ExpectedSimulationFailureError)(nil), // 3: loop.ExpectedSimulationFailureError + (*GetEstimateFeeRequest)(nil), // 4: loop.GetEstimateFeeRequest + (*GetFeeComponentsReply)(nil), // 5: loop.GetFeeComponentsReply + (*GetEstimateFeeReply)(nil), // 6: loop.GetEstimateFeeReply + (*codec.VersionedBytes)(nil), // 7: codec.VersionedBytes + (*BigInt)(nil), // 8: loop.BigInt + (*evm.GetTransactionStatusRequest)(nil), // 9: loop.evm.GetTransactionStatusRequest + (*emptypb.Empty)(nil), // 10: google.protobuf.Empty + (*evm.GetTransactionStatusReply)(nil), // 11: loop.evm.GetTransactionStatusReply } var file_contract_writer_proto_depIdxs = []int32{ - 5, // 0: loop.SubmitTransactionRequest.params:type_name -> codec.VersionedBytes + 7, // 0: loop.SubmitTransactionRequest.params:type_name -> codec.VersionedBytes 1, // 1: loop.SubmitTransactionRequest.meta:type_name -> loop.TransactionMeta - 6, // 2: loop.SubmitTransactionRequest.value:type_name -> loop.BigInt - 6, // 3: loop.TransactionMeta.gas_limit:type_name -> loop.BigInt - 5, // 4: loop.GetEstimateFeeRequest.params:type_name -> codec.VersionedBytes - 1, // 5: loop.GetEstimateFeeRequest.meta:type_name -> loop.TransactionMeta - 6, // 6: loop.GetEstimateFeeRequest.value:type_name -> loop.BigInt - 6, // 7: loop.GetFeeComponentsReply.execution_fee:type_name -> loop.BigInt - 6, // 8: loop.GetFeeComponentsReply.data_availability_fee:type_name -> loop.BigInt - 6, // 9: loop.GetEstimateFeeReply.fee:type_name -> loop.BigInt - 0, // 10: loop.ContractWriter.SubmitTransaction:input_type -> loop.SubmitTransactionRequest - 7, // 11: loop.ContractWriter.GetTransactionStatus:input_type -> loop.evm.GetTransactionStatusRequest - 8, // 12: loop.ContractWriter.GetFeeComponents:input_type -> google.protobuf.Empty - 2, // 13: loop.ContractWriter.GetEstimateFee:input_type -> loop.GetEstimateFeeRequest - 8, // 14: loop.ContractWriter.SubmitTransaction:output_type -> google.protobuf.Empty - 9, // 15: loop.ContractWriter.GetTransactionStatus:output_type -> loop.evm.GetTransactionStatusReply - 3, // 16: loop.ContractWriter.GetFeeComponents:output_type -> loop.GetFeeComponentsReply - 4, // 17: loop.ContractWriter.GetEstimateFee:output_type -> loop.GetEstimateFeeReply - 14, // [14:18] is the sub-list for method output_type - 10, // [10:14] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 8, // 2: loop.SubmitTransactionRequest.value:type_name -> loop.BigInt + 2, // 3: loop.SubmitTransactionRequest.simulation_options:type_name -> loop.SimulationOptions + 8, // 4: loop.TransactionMeta.gas_limit:type_name -> loop.BigInt + 3, // 5: loop.SimulationOptions.expected_simulation_failure_errors:type_name -> loop.ExpectedSimulationFailureError + 7, // 6: loop.GetEstimateFeeRequest.params:type_name -> codec.VersionedBytes + 1, // 7: loop.GetEstimateFeeRequest.meta:type_name -> loop.TransactionMeta + 8, // 8: loop.GetEstimateFeeRequest.value:type_name -> loop.BigInt + 8, // 9: loop.GetFeeComponentsReply.execution_fee:type_name -> loop.BigInt + 8, // 10: loop.GetFeeComponentsReply.data_availability_fee:type_name -> loop.BigInt + 8, // 11: loop.GetEstimateFeeReply.fee:type_name -> loop.BigInt + 0, // 12: loop.ContractWriter.SubmitTransaction:input_type -> loop.SubmitTransactionRequest + 9, // 13: loop.ContractWriter.GetTransactionStatus:input_type -> loop.evm.GetTransactionStatusRequest + 10, // 14: loop.ContractWriter.GetFeeComponents:input_type -> google.protobuf.Empty + 4, // 15: loop.ContractWriter.GetEstimateFee:input_type -> loop.GetEstimateFeeRequest + 10, // 16: loop.ContractWriter.SubmitTransaction:output_type -> google.protobuf.Empty + 11, // 17: loop.ContractWriter.GetTransactionStatus:output_type -> loop.evm.GetTransactionStatusReply + 5, // 18: loop.ContractWriter.GetFeeComponents:output_type -> loop.GetFeeComponentsReply + 6, // 19: loop.ContractWriter.GetEstimateFee:output_type -> loop.GetEstimateFeeReply + 16, // [16:20] is the sub-list for method output_type + 12, // [12:16] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name } func init() { file_contract_writer_proto_init() } @@ -459,7 +573,7 @@ func file_contract_writer_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_contract_writer_proto_rawDesc), len(file_contract_writer_proto_rawDesc)), NumEnums: 0, - NumMessages: 5, + NumMessages: 7, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/loop/internal/pb/contract_writer.proto b/pkg/loop/internal/pb/contract_writer.proto index 8ac2f6fa77..9ecf5cc346 100644 --- a/pkg/loop/internal/pb/contract_writer.proto +++ b/pkg/loop/internal/pb/contract_writer.proto @@ -24,6 +24,7 @@ message SubmitTransactionRequest { string to_address = 5; TransactionMeta meta = 6; BigInt value = 7; + SimulationOptions simulation_options = 8; } message TransactionMeta { @@ -31,6 +32,15 @@ message TransactionMeta { BigInt gas_limit = 2; } +message SimulationOptions { + bool simulate_transaction = 1; + repeated ExpectedSimulationFailureError expected_simulation_failure_errors = 2; +} + +message ExpectedSimulationFailureError { + string error_string = 1; +} + // GetEstimateFeeReply has arguments for [github.com/smartcontractkit/chainlink-common/pkg/types.ContractWriter.GetEstimateFee]. message GetEstimateFeeRequest { string contract_name = 1; diff --git a/pkg/loop/internal/relayer/pluginprovider/contractreader/contract_reader_test.go b/pkg/loop/internal/relayer/pluginprovider/contractreader/contract_reader_test.go index 697d74a44d..0c0601a513 100644 --- a/pkg/loop/internal/relayer/pluginprovider/contractreader/contract_reader_test.go +++ b/pkg/loop/internal/relayer/pluginprovider/contractreader/contract_reader_test.go @@ -450,7 +450,7 @@ type fakeContractWriter struct { cr *fakeContractReader } -func (f *fakeContractWriter) SubmitTransaction(_ context.Context, contractName, method string, args any, transactionID string, toAddress string, meta *types.TxMeta, value *big.Int) error { +func (f *fakeContractWriter) SubmitTransaction(_ context.Context, contractName, method string, args any, transactionID string, toAddress string, meta *types.TxMeta, value *big.Int, _ *types.SimulationOptions) error { contractID := toAddress + "-" + contractName switch method { case MethodSettingStruct: diff --git a/pkg/loop/internal/relayer/pluginprovider/contractwriter/contract_writer.go b/pkg/loop/internal/relayer/pluginprovider/contractwriter/contract_writer.go index eca3a69555..23ac873590 100644 --- a/pkg/loop/internal/relayer/pluginprovider/contractwriter/contract_writer.go +++ b/pkg/loop/internal/relayer/pluginprovider/contractwriter/contract_writer.go @@ -45,20 +45,21 @@ func WithClientEncoding(version codecpb.EncodingVersion) ClientOpt { } } -func (c *Client) SubmitTransaction(ctx context.Context, contractName, method string, params any, transactionID, toAddress string, meta *types.TxMeta, value *big.Int) error { +func (c *Client) SubmitTransaction(ctx context.Context, contractName, method string, params any, transactionID, toAddress string, meta *types.TxMeta, value *big.Int, simulationOpts *types.SimulationOptions) error { versionedParams, err := codecpb.EncodeVersionedBytes(params, c.encodeWith) if err != nil { return err } req := pb.SubmitTransactionRequest{ - ContractName: contractName, - Method: method, - Params: versionedParams, - TransactionId: transactionID, - ToAddress: toAddress, - Meta: TxMetaToProto(meta), - Value: pb.NewBigIntFromInt(value), + ContractName: contractName, + Method: method, + Params: versionedParams, + TransactionId: transactionID, + ToAddress: toAddress, + Meta: TxMetaToProto(meta), + Value: pb.NewBigIntFromInt(value), + SimulationOptions: SimulationOptionsToProto(simulationOpts), } _, err = c.grpc.SubmitTransaction(ctx, &req) @@ -153,7 +154,7 @@ func (s *Server) SubmitTransaction(ctx context.Context, req *pb.SubmitTransactio return nil, err } - err := s.impl.SubmitTransaction(ctx, req.ContractName, req.Method, params, req.TransactionId, req.ToAddress, TxMetaFromProto(req.Meta), req.Value.Int()) + err := s.impl.SubmitTransaction(ctx, req.ContractName, req.Method, params, req.TransactionId, req.ToAddress, TxMetaFromProto(req.Meta), req.Value.Int(), SimulationOptionsFromProto(req.SimulationOptions)) if err != nil { return nil, err } diff --git a/pkg/loop/internal/relayer/pluginprovider/contractwriter/converters.go b/pkg/loop/internal/relayer/pluginprovider/contractwriter/converters.go index 1a21d6bb38..2cf48805b5 100644 --- a/pkg/loop/internal/relayer/pluginprovider/contractwriter/converters.go +++ b/pkg/loop/internal/relayer/pluginprovider/contractwriter/converters.go @@ -24,6 +24,54 @@ func TxMetaToProto(meta *types.TxMeta) *pb.TransactionMeta { return proto } +// SimulationOptionsToProto converts Go SimulationOptions to proto. +func SimulationOptionsToProto(opts *types.SimulationOptions) *pb.SimulationOptions { + if opts == nil { + return nil + } + return &pb.SimulationOptions{ + SimulateTransaction: opts.SimulateTransaction, + ExpectedSimulationFailureErrors: failureRulesToProto(opts.ExpectedSimulationFailureErrors), + } +} + +// SimulationOptionsFromProto converts proto SimulationOptions to Go types. +func SimulationOptionsFromProto(proto *pb.SimulationOptions) *types.SimulationOptions { + if proto == nil { + return nil + } + return &types.SimulationOptions{ + SimulateTransaction: proto.GetSimulateTransaction(), + ExpectedSimulationFailureErrors: failureRulesFromProto(proto.GetExpectedSimulationFailureErrors()), + } +} + +func failureRulesToProto(rules []types.ExpectedSimulationFailureError) []*pb.ExpectedSimulationFailureError { + if len(rules) == 0 { + return nil + } + pbRules := make([]*pb.ExpectedSimulationFailureError, len(rules)) + for i, rule := range rules { + pbRules[i] = &pb.ExpectedSimulationFailureError{ + ErrorString: rule.ErrorString, + } + } + return pbRules +} + +func failureRulesFromProto(pbRules []*pb.ExpectedSimulationFailureError) []types.ExpectedSimulationFailureError { + if len(pbRules) == 0 { + return nil + } + rules := make([]types.ExpectedSimulationFailureError, len(pbRules)) + for i, pbRule := range pbRules { + rules[i] = types.ExpectedSimulationFailureError{ + ErrorString: pbRule.GetErrorString(), + } + } + return rules +} + // TxMetaFromProto converts a TxMeta from it's generated protobuf Go type to our internal Go type. func TxMetaFromProto(proto *pb.TransactionMeta) *types.TxMeta { if proto == nil { diff --git a/pkg/loop/internal/relayer/pluginprovider/contractwriter/converters_test.go b/pkg/loop/internal/relayer/pluginprovider/contractwriter/converters_test.go index 43539a2f91..53564a0e68 100644 --- a/pkg/loop/internal/relayer/pluginprovider/contractwriter/converters_test.go +++ b/pkg/loop/internal/relayer/pluginprovider/contractwriter/converters_test.go @@ -73,3 +73,72 @@ func TestTxMetaToProto(t *testing.T) { require.Equal(t, big.NewInt(10), proto.GasLimit.Int()) }) } + +func TestSimulationOptionsToProto(t *testing.T) { + t.Run("nil input", func(t *testing.T) { + result := contractwriter.SimulationOptionsToProto(nil) + require.Nil(t, result) + }) + + t.Run("simulate with no rules", func(t *testing.T) { + opts := &types.SimulationOptions{SimulateTransaction: true} + result := contractwriter.SimulationOptionsToProto(opts) + require.NotNil(t, result) + require.True(t, result.SimulateTransaction) + require.Empty(t, result.ExpectedSimulationFailureErrors) + }) + + t.Run("simulate with rules", func(t *testing.T) { + opts := &types.SimulationOptions{ + SimulateTransaction: true, + ExpectedSimulationFailureErrors: []types.ExpectedSimulationFailureError{ + {ErrorString: "execution reverted"}, + {ErrorString: "insufficient funds"}, + }, + } + result := contractwriter.SimulationOptionsToProto(opts) + require.NotNil(t, result) + require.True(t, result.SimulateTransaction) + require.Len(t, result.ExpectedSimulationFailureErrors, 2) + require.Equal(t, "execution reverted", result.ExpectedSimulationFailureErrors[0].ErrorString) + require.Equal(t, "insufficient funds", result.ExpectedSimulationFailureErrors[1].ErrorString) + }) + + t.Run("no simulate", func(t *testing.T) { + opts := &types.SimulationOptions{SimulateTransaction: false} + result := contractwriter.SimulationOptionsToProto(opts) + require.NotNil(t, result) + require.False(t, result.SimulateTransaction) + }) +} + +func TestSimulationOptionsFromProto(t *testing.T) { + t.Run("nil input", func(t *testing.T) { + result := contractwriter.SimulationOptionsFromProto(nil) + require.Nil(t, result) + }) + + t.Run("simulate with no rules", func(t *testing.T) { + proto := &pb.SimulationOptions{SimulateTransaction: true} + result := contractwriter.SimulationOptionsFromProto(proto) + require.NotNil(t, result) + require.True(t, result.SimulateTransaction) + require.Empty(t, result.ExpectedSimulationFailureErrors) + }) + + t.Run("simulate with rules", func(t *testing.T) { + proto := &pb.SimulationOptions{ + SimulateTransaction: true, + ExpectedSimulationFailureErrors: []*pb.ExpectedSimulationFailureError{ + {ErrorString: "execution reverted"}, + {ErrorString: "insufficient funds"}, + }, + } + result := contractwriter.SimulationOptionsFromProto(proto) + require.NotNil(t, result) + require.True(t, result.SimulateTransaction) + require.Len(t, result.ExpectedSimulationFailureErrors, 2) + require.Equal(t, "execution reverted", result.ExpectedSimulationFailureErrors[0].ErrorString) + require.Equal(t, "insufficient funds", result.ExpectedSimulationFailureErrors[1].ErrorString) + }) +} diff --git a/pkg/types/chains/aptos/aptos.go b/pkg/types/chains/aptos/aptos.go index 81b94af7b9..77db6f5158 100644 --- a/pkg/types/chains/aptos/aptos.go +++ b/pkg/types/chains/aptos/aptos.go @@ -203,10 +203,27 @@ type AccountTransactionsReply struct { // ========== SubmitTransaction ========== +// SimulationOptions controls transaction simulation behavior for Aptos transactions. +// When nil, the implementation uses its default simulation behavior. +type SimulationOptions struct { + // SimulateTransaction controls whether the transaction should be simulated before submission. + SimulateTransaction bool + // ExpectedSimulationFailureErrors defines rules for expected simulation failures. + // Only meaningful when SimulateTransaction is true. + ExpectedSimulationFailureErrors []ExpectedSimulationFailureError +} + +// ExpectedSimulationFailureError defines a rule for expected simulation failures. +type ExpectedSimulationFailureError struct { + // ErrorString is the error message string to match against simulation failures. + ErrorString string +} + type SubmitTransactionRequest struct { - ReceiverModuleID ModuleID // This can potentially be removed if the EncodedPayload is of type EntryFunction which has all the details - EncodedPayload []byte - GasConfig *GasConfig + ReceiverModuleID ModuleID // This can potentially be removed if the EncodedPayload is of type EntryFunction which has all the details + EncodedPayload []byte + GasConfig *GasConfig + SimulationOptions *SimulationOptions } type TransactionStatus int diff --git a/pkg/types/contract_writer.go b/pkg/types/contract_writer.go index d8e4ea291b..0fcfe76f4e 100644 --- a/pkg/types/contract_writer.go +++ b/pkg/types/contract_writer.go @@ -14,6 +14,25 @@ const ( // IdempotencyKey is generated by SubmitTransaction caller to prevent double sending or to track transaction status type IdempotencyKey = string +// ExpectedSimulationFailureError defines a rule for expected simulation failures. +// When simulation of a transaction fails, these rules determine whether the +// failure is expected and the transaction should still be submitted. +type ExpectedSimulationFailureError struct { + // ErrorString is the error message string to match against simulation failures. + ErrorString string +} + +// SimulationOptions controls transaction simulation behavior. +// When nil is passed for this parameter, the implementation uses its default simulation behavior. +// When SimulateTransaction is false, ExpectedSimulationFailureErrors is ignored. +type SimulationOptions struct { + // SimulateTransaction controls whether the transaction should be simulated before submission. + SimulateTransaction bool + // ExpectedSimulationFailureErrors defines rules for expected simulation failures. + // Only meaningful when SimulateTransaction is true. + ExpectedSimulationFailureErrors []ExpectedSimulationFailureError +} + type ContractWriter interface { services.Service @@ -21,7 +40,8 @@ type ContractWriter interface { // // - `args` should be any object which maps a set of method param into the contract and method specific method params. // - `transactionID` will be used by the underlying TXM as an idempotency key, and unique reference to track transaction attempts. - SubmitTransaction(ctx context.Context, contractName, method string, args any, transactionID IdempotencyKey, toAddress string, meta *TxMeta, value *big.Int) error + // - `simulationOpts` controls simulation behavior. When nil, the implementation uses its default behavior. + SubmitTransaction(ctx context.Context, contractName, method string, args any, transactionID IdempotencyKey, toAddress string, meta *TxMeta, value *big.Int, simulationOpts *SimulationOptions) error // GetTransactionStatus returns the current status of a transaction in the underlying chain's TXM. GetTransactionStatus(ctx context.Context, transactionID IdempotencyKey) (TransactionStatus, error) @@ -70,3 +90,29 @@ type EstimateFee struct { Fee *big.Int Decimals uint32 } + +type UnimplementedContractWriter struct{} + +var _ ContractWriter = UnimplementedContractWriter{} + +func (UnimplementedContractWriter) SubmitTransaction(ctx context.Context, contractName, method string, args any, transactionID IdempotencyKey, toAddress string, meta *TxMeta, value *big.Int, simulationOpts *SimulationOptions) error { + return UnimplementedError("ContractWriter.SubmitTransaction unimplemented") +} + +func (UnimplementedContractWriter) GetTransactionStatus(ctx context.Context, transactionID IdempotencyKey) (TransactionStatus, error) { + return Unknown, UnimplementedError("ContractWriter.GetTransactionStatus unimplemented") +} + +func (UnimplementedContractWriter) GetFeeComponents(ctx context.Context) (*ChainFeeComponents, error) { + return nil, UnimplementedError("ContractWriter.GetFeeComponents unimplemented") +} + +func (UnimplementedContractWriter) GetEstimateFee(ctx context.Context, contract, method string, args any, toAddress string, meta *TxMeta, val *big.Int) (EstimateFee, error) { + return EstimateFee{}, UnimplementedError("ContractWriter.GetEstimateFee unimplemented") +} + +func (UnimplementedContractWriter) Start(context.Context) error { return nil } +func (UnimplementedContractWriter) Close() error { return nil } +func (UnimplementedContractWriter) HealthReport() map[string]error { return nil } +func (UnimplementedContractWriter) Name() string { return "UnimplementedContractWriter" } +func (UnimplementedContractWriter) Ready() error { return nil } diff --git a/pkg/types/interfacetests/utils.go b/pkg/types/interfacetests/utils.go index 4c8610b462..5b6608f678 100644 --- a/pkg/types/interfacetests/utils.go +++ b/pkg/types/interfacetests/utils.go @@ -89,7 +89,7 @@ func RunTestsInParallel[T TestingT[T]](t T, tester BasicTester[T], tests []Testc func batchContractWrite[T TestingT[T]](t T, tester ChainComponentsInterfaceTester[T], cw types.ContractWriter, boundContracts []types.BoundContract, batchCallEntry BatchCallEntry, mockRun bool) { // This is necessary because the mock helper function requires the entire batchCallEntry rather than an individual testStruct if mockRun { - err := cw.SubmitTransaction(t.Context(), AnyContractName, "batchContractWrite", batchCallEntry, "", "", nil, big.NewInt(0)) + err := cw.SubmitTransaction(t.Context(), AnyContractName, "batchContractWrite", batchCallEntry, "", "", nil, big.NewInt(0), nil) require.NoError(t, err) return } @@ -115,7 +115,7 @@ func batchContractWrite[T TestingT[T]](t T, tester ChainComponentsInterfaceTeste func SubmitTransactionToCW[T TestingT[T]](t T, tester ChainComponentsInterfaceTester[T], cw types.ContractWriter, method string, args any, contract types.BoundContract, status types.TransactionStatus) string { tester.DirtyContracts() txID := uuid.New().String() - err := cw.SubmitTransaction(t.Context(), contract.Name, method, args, txID, contract.Address, nil, big.NewInt(0)) + err := cw.SubmitTransaction(t.Context(), contract.Name, method, args, txID, contract.Address, nil, big.NewInt(0), nil) require.NoError(t, err) err = WaitForTransactionStatus(t, tester, cw, txID, status, false) From d0363fa36407ab53af0eefac778a5e819f1188a8 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Fri, 13 Mar 2026 12:10:56 +0000 Subject: [PATCH 2/4] chore: simplify comments on SimulationOptions types --- pkg/types/chains/aptos/aptos.go | 11 +++-------- pkg/types/contract_writer.go | 16 ++++------------ 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/pkg/types/chains/aptos/aptos.go b/pkg/types/chains/aptos/aptos.go index 77db6f5158..0f5c2be15e 100644 --- a/pkg/types/chains/aptos/aptos.go +++ b/pkg/types/chains/aptos/aptos.go @@ -203,19 +203,14 @@ type AccountTransactionsReply struct { // ========== SubmitTransaction ========== -// SimulationOptions controls transaction simulation behavior for Aptos transactions. -// When nil, the implementation uses its default simulation behavior. +// SimulationOptions controls transaction simulation behavior. Nil means use implementation defaults. type SimulationOptions struct { - // SimulateTransaction controls whether the transaction should be simulated before submission. - SimulateTransaction bool - // ExpectedSimulationFailureErrors defines rules for expected simulation failures. - // Only meaningful when SimulateTransaction is true. + SimulateTransaction bool ExpectedSimulationFailureErrors []ExpectedSimulationFailureError } -// ExpectedSimulationFailureError defines a rule for expected simulation failures. +// ExpectedSimulationFailureError defines an expected simulation failure to match against. type ExpectedSimulationFailureError struct { - // ErrorString is the error message string to match against simulation failures. ErrorString string } diff --git a/pkg/types/contract_writer.go b/pkg/types/contract_writer.go index 0fcfe76f4e..88946e2a22 100644 --- a/pkg/types/contract_writer.go +++ b/pkg/types/contract_writer.go @@ -14,22 +14,14 @@ const ( // IdempotencyKey is generated by SubmitTransaction caller to prevent double sending or to track transaction status type IdempotencyKey = string -// ExpectedSimulationFailureError defines a rule for expected simulation failures. -// When simulation of a transaction fails, these rules determine whether the -// failure is expected and the transaction should still be submitted. +// ExpectedSimulationFailureError defines an expected simulation failure to match against. type ExpectedSimulationFailureError struct { - // ErrorString is the error message string to match against simulation failures. ErrorString string } -// SimulationOptions controls transaction simulation behavior. -// When nil is passed for this parameter, the implementation uses its default simulation behavior. -// When SimulateTransaction is false, ExpectedSimulationFailureErrors is ignored. +// SimulationOptions controls transaction simulation behavior. Nil means use implementation defaults. type SimulationOptions struct { - // SimulateTransaction controls whether the transaction should be simulated before submission. - SimulateTransaction bool - // ExpectedSimulationFailureErrors defines rules for expected simulation failures. - // Only meaningful when SimulateTransaction is true. + SimulateTransaction bool ExpectedSimulationFailureErrors []ExpectedSimulationFailureError } @@ -40,7 +32,7 @@ type ContractWriter interface { // // - `args` should be any object which maps a set of method param into the contract and method specific method params. // - `transactionID` will be used by the underlying TXM as an idempotency key, and unique reference to track transaction attempts. - // - `simulationOpts` controls simulation behavior. When nil, the implementation uses its default behavior. + // - `simulationOpts` controls simulation behavior. Nil means use implementation defaults. SubmitTransaction(ctx context.Context, contractName, method string, args any, transactionID IdempotencyKey, toAddress string, meta *TxMeta, value *big.Int, simulationOpts *SimulationOptions) error // GetTransactionStatus returns the current status of a transaction in the underlying chain's TXM. From 2ac30e6e9ecae6311bf726b6789cb945ee83cbd6 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Fri, 13 Mar 2026 12:23:01 +0000 Subject: [PATCH 3/4] chore: remove unused UnimplementedContractWriter --- pkg/types/contract_writer.go | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/pkg/types/contract_writer.go b/pkg/types/contract_writer.go index 88946e2a22..4e35891d3f 100644 --- a/pkg/types/contract_writer.go +++ b/pkg/types/contract_writer.go @@ -83,28 +83,3 @@ type EstimateFee struct { Decimals uint32 } -type UnimplementedContractWriter struct{} - -var _ ContractWriter = UnimplementedContractWriter{} - -func (UnimplementedContractWriter) SubmitTransaction(ctx context.Context, contractName, method string, args any, transactionID IdempotencyKey, toAddress string, meta *TxMeta, value *big.Int, simulationOpts *SimulationOptions) error { - return UnimplementedError("ContractWriter.SubmitTransaction unimplemented") -} - -func (UnimplementedContractWriter) GetTransactionStatus(ctx context.Context, transactionID IdempotencyKey) (TransactionStatus, error) { - return Unknown, UnimplementedError("ContractWriter.GetTransactionStatus unimplemented") -} - -func (UnimplementedContractWriter) GetFeeComponents(ctx context.Context) (*ChainFeeComponents, error) { - return nil, UnimplementedError("ContractWriter.GetFeeComponents unimplemented") -} - -func (UnimplementedContractWriter) GetEstimateFee(ctx context.Context, contract, method string, args any, toAddress string, meta *TxMeta, val *big.Int) (EstimateFee, error) { - return EstimateFee{}, UnimplementedError("ContractWriter.GetEstimateFee unimplemented") -} - -func (UnimplementedContractWriter) Start(context.Context) error { return nil } -func (UnimplementedContractWriter) Close() error { return nil } -func (UnimplementedContractWriter) HealthReport() map[string]error { return nil } -func (UnimplementedContractWriter) Name() string { return "UnimplementedContractWriter" } -func (UnimplementedContractWriter) Ready() error { return nil } From 91d39b4ec928fca325bc987c9cb7d74b41aac208 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Fri, 13 Mar 2026 12:26:07 +0000 Subject: [PATCH 4/4] chore: inline single-use failureRules converter helpers --- .../contractwriter/converters.go | 46 ++++++------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/pkg/loop/internal/relayer/pluginprovider/contractwriter/converters.go b/pkg/loop/internal/relayer/pluginprovider/contractwriter/converters.go index 2cf48805b5..2c4b9cb8b4 100644 --- a/pkg/loop/internal/relayer/pluginprovider/contractwriter/converters.go +++ b/pkg/loop/internal/relayer/pluginprovider/contractwriter/converters.go @@ -29,10 +29,15 @@ func SimulationOptionsToProto(opts *types.SimulationOptions) *pb.SimulationOptio if opts == nil { return nil } - return &pb.SimulationOptions{ - SimulateTransaction: opts.SimulateTransaction, - ExpectedSimulationFailureErrors: failureRulesToProto(opts.ExpectedSimulationFailureErrors), + protoOpts := &pb.SimulationOptions{ + SimulateTransaction: opts.SimulateTransaction, } + for _, rule := range opts.ExpectedSimulationFailureErrors { + protoOpts.ExpectedSimulationFailureErrors = append(protoOpts.ExpectedSimulationFailureErrors, &pb.ExpectedSimulationFailureError{ + ErrorString: rule.ErrorString, + }) + } + return protoOpts } // SimulationOptionsFromProto converts proto SimulationOptions to Go types. @@ -40,36 +45,15 @@ func SimulationOptionsFromProto(proto *pb.SimulationOptions) *types.SimulationOp if proto == nil { return nil } - return &types.SimulationOptions{ - SimulateTransaction: proto.GetSimulateTransaction(), - ExpectedSimulationFailureErrors: failureRulesFromProto(proto.GetExpectedSimulationFailureErrors()), - } -} - -func failureRulesToProto(rules []types.ExpectedSimulationFailureError) []*pb.ExpectedSimulationFailureError { - if len(rules) == 0 { - return nil - } - pbRules := make([]*pb.ExpectedSimulationFailureError, len(rules)) - for i, rule := range rules { - pbRules[i] = &pb.ExpectedSimulationFailureError{ - ErrorString: rule.ErrorString, - } - } - return pbRules -} - -func failureRulesFromProto(pbRules []*pb.ExpectedSimulationFailureError) []types.ExpectedSimulationFailureError { - if len(pbRules) == 0 { - return nil + opts := &types.SimulationOptions{ + SimulateTransaction: proto.GetSimulateTransaction(), } - rules := make([]types.ExpectedSimulationFailureError, len(pbRules)) - for i, pbRule := range pbRules { - rules[i] = types.ExpectedSimulationFailureError{ - ErrorString: pbRule.GetErrorString(), - } + for _, rule := range proto.GetExpectedSimulationFailureErrors() { + opts.ExpectedSimulationFailureErrors = append(opts.ExpectedSimulationFailureErrors, types.ExpectedSimulationFailureError{ + ErrorString: rule.GetErrorString(), + }) } - return rules + return opts } // TxMetaFromProto converts a TxMeta from it's generated protobuf Go type to our internal Go type.