diff --git a/pkg/capabilities/v2/chain-capabilities/aptos/proto_helpers.go b/pkg/capabilities/v2/chain-capabilities/aptos/proto_helpers.go new file mode 100644 index 000000000..6887edfb3 --- /dev/null +++ b/pkg/capabilities/v2/chain-capabilities/aptos/proto_helpers.go @@ -0,0 +1,161 @@ +package aptos + +import ( + "fmt" + "math" + + typesaptos "github.com/smartcontractkit/chainlink-common/pkg/types/chains/aptos" +) + +// ConvertViewPayloadFromProto converts a capability ViewPayload into Aptos domain types. +// Capability requests currently accept shortened Aptos addresses, so this helper left-pads +// addresses up to 32 bytes instead of requiring exact-length address bytes. +func ConvertViewPayloadFromProto(payload *ViewPayload) (*typesaptos.ViewPayload, error) { + if payload == nil { + return nil, fmt.Errorf("payload is required") + } + if payload.Module == nil { + return nil, fmt.Errorf("payload.module is required") + } + if err := requireNonEmptyBytes(payload.Module.Address, "payload.module.address"); err != nil { + return nil, err + } + if err := requireNonEmptyString(payload.Module.Name, "payload.module.name"); err != nil { + return nil, err + } + if err := requireNonEmptyString(payload.Function, "payload.function"); err != nil { + return nil, err + } + + moduleAddress, err := convertAccountAddressFromProto(payload.Module.Address, "module") + if err != nil { + return nil, err + } + + argTypes := make([]typesaptos.TypeTag, 0, len(payload.ArgTypes)) + for i, tag := range payload.ArgTypes { + converted, err := ConvertTypeTagFromProto(tag) + if err != nil { + return nil, fmt.Errorf("invalid arg type at index %d: %w", i, err) + } + argTypes = append(argTypes, *converted) + } + + return &typesaptos.ViewPayload{ + Module: typesaptos.ModuleID{ + Address: moduleAddress, + Name: payload.Module.Name, + }, + Function: payload.Function, + ArgTypes: argTypes, + Args: payload.Args, + }, nil +} + +// ConvertTypeTagFromProto converts a capability TypeTag into Aptos domain types. +func ConvertTypeTagFromProto(tag *TypeTag) (*typesaptos.TypeTag, error) { + if tag == nil { + return nil, fmt.Errorf("type tag is nil") + } + + switch tag.Kind { + case TypeTagKind_TYPE_TAG_KIND_BOOL: + return &typesaptos.TypeTag{Value: typesaptos.BoolTag{}}, nil + case TypeTagKind_TYPE_TAG_KIND_U8: + return &typesaptos.TypeTag{Value: typesaptos.U8Tag{}}, nil + case TypeTagKind_TYPE_TAG_KIND_U16: + return &typesaptos.TypeTag{Value: typesaptos.U16Tag{}}, nil + case TypeTagKind_TYPE_TAG_KIND_U32: + return &typesaptos.TypeTag{Value: typesaptos.U32Tag{}}, nil + case TypeTagKind_TYPE_TAG_KIND_U64: + return &typesaptos.TypeTag{Value: typesaptos.U64Tag{}}, nil + case TypeTagKind_TYPE_TAG_KIND_U128: + return &typesaptos.TypeTag{Value: typesaptos.U128Tag{}}, nil + case TypeTagKind_TYPE_TAG_KIND_U256: + return &typesaptos.TypeTag{Value: typesaptos.U256Tag{}}, nil + case TypeTagKind_TYPE_TAG_KIND_ADDRESS: + return &typesaptos.TypeTag{Value: typesaptos.AddressTag{}}, nil + case TypeTagKind_TYPE_TAG_KIND_SIGNER: + return &typesaptos.TypeTag{Value: typesaptos.SignerTag{}}, nil + case TypeTagKind_TYPE_TAG_KIND_VECTOR: + vector := tag.GetVector() + if vector == nil { + return nil, fmt.Errorf("vector tag missing vector value") + } + elementType, err := ConvertTypeTagFromProto(vector.ElementType) + if err != nil { + return nil, fmt.Errorf("invalid vector element type: %w", err) + } + return &typesaptos.TypeTag{Value: typesaptos.VectorTag{ElementType: *elementType}}, nil + case TypeTagKind_TYPE_TAG_KIND_STRUCT: + structTag := tag.GetStruct() + if structTag == nil { + return nil, fmt.Errorf("struct tag missing struct value") + } + if err := requireNonEmptyBytes(structTag.Address, "struct address"); err != nil { + return nil, err + } + if err := requireNonEmptyString(structTag.Module, "struct module"); err != nil { + return nil, err + } + if err := requireNonEmptyString(structTag.Name, "struct name"); err != nil { + return nil, err + } + + structAddress, err := convertAccountAddressFromProto(structTag.Address, "struct") + if err != nil { + return nil, err + } + + typeParams := make([]typesaptos.TypeTag, 0, len(structTag.TypeParams)) + for i, tp := range structTag.TypeParams { + converted, err := ConvertTypeTagFromProto(tp) + if err != nil { + return nil, fmt.Errorf("invalid struct type param at index %d: %w", i, err) + } + typeParams = append(typeParams, *converted) + } + + return &typesaptos.TypeTag{Value: typesaptos.StructTag{ + Address: structAddress, + Module: structTag.Module, + Name: structTag.Name, + TypeParams: typeParams, + }}, nil + case TypeTagKind_TYPE_TAG_KIND_GENERIC: + generic := tag.GetGeneric() + if generic == nil { + return nil, fmt.Errorf("generic tag missing generic value") + } + if generic.Index > math.MaxUint16 { + return nil, fmt.Errorf("generic type index out of range: %d", generic.Index) + } + return &typesaptos.TypeTag{Value: typesaptos.GenericTag{Index: uint16(generic.Index)}}, nil + default: + return nil, fmt.Errorf("unsupported type tag kind: %v", tag.Kind) + } +} + +func requireNonEmptyBytes(value []byte, field string) error { + if len(value) == 0 { + return fmt.Errorf("%s is required", field) + } + return nil +} + +func requireNonEmptyString(value string, field string) error { + if value == "" { + return fmt.Errorf("%s is required", field) + } + return nil +} + +func convertAccountAddressFromProto(address []byte, field string) (typesaptos.AccountAddress, error) { + if len(address) > typesaptos.AccountAddressLength { + return typesaptos.AccountAddress{}, fmt.Errorf("%s address too long: %d", field, len(address)) + } + + var converted typesaptos.AccountAddress + copy(converted[typesaptos.AccountAddressLength-len(address):], address) + return converted, nil +} diff --git a/pkg/capabilities/v2/chain-capabilities/aptos/proto_helpers_test.go b/pkg/capabilities/v2/chain-capabilities/aptos/proto_helpers_test.go new file mode 100644 index 000000000..770b96752 --- /dev/null +++ b/pkg/capabilities/v2/chain-capabilities/aptos/proto_helpers_test.go @@ -0,0 +1,218 @@ +package aptos_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + aptoscap "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/aptos" + typesaptos "github.com/smartcontractkit/chainlink-common/pkg/types/chains/aptos" +) + +func TestConvertViewPayloadFromProto_ConvertsNestedVectorStructAndGenericTags(t *testing.T) { + t.Parallel() + + payload, err := aptoscap.ConvertViewPayloadFromProto(&aptoscap.ViewPayload{ + Module: &aptoscap.ModuleID{Address: []byte{0x01}, Name: "coin"}, + Function: "name", + ArgTypes: []*aptoscap.TypeTag{ + { + Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_VECTOR, + Value: &aptoscap.TypeTag_Vector{Vector: &aptoscap.VectorTag{ + ElementType: &aptoscap.TypeTag{ + Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_STRUCT, + Value: &aptoscap.TypeTag_Struct{Struct: &aptoscap.StructTag{ + Address: []byte{0x02}, + Module: "aptos_coin", + Name: "Coin", + TypeParams: []*aptoscap.TypeTag{ + { + Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_GENERIC, + Value: &aptoscap.TypeTag_Generic{Generic: &aptoscap.GenericTag{Index: 7}}, + }, + }, + }}, + }, + }}, + }, + }, + }) + require.NoError(t, err) + require.NotNil(t, payload) + require.Equal(t, "name", payload.Function) + require.Len(t, payload.ArgTypes, 1) + + vectorTag, ok := payload.ArgTypes[0].Value.(typesaptos.VectorTag) + require.True(t, ok) + structTag, ok := vectorTag.ElementType.Value.(typesaptos.StructTag) + require.True(t, ok) + require.Equal(t, "aptos_coin", structTag.Module) + require.Equal(t, "Coin", structTag.Name) + require.Len(t, structTag.TypeParams, 1) + genericTag, ok := structTag.TypeParams[0].Value.(typesaptos.GenericTag) + require.True(t, ok) + require.EqualValues(t, 7, genericTag.Index) +} + +func TestConvertViewPayloadFromProto_RejectsInvalidPayloadInputs(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + payload *aptoscap.ViewPayload + wantErr string + }{ + { + name: "missing payload", + payload: nil, + wantErr: "payload is required", + }, + { + name: "missing module", + payload: &aptoscap.ViewPayload{Function: "name"}, + wantErr: "payload.module is required", + }, + { + name: "missing module address", + payload: &aptoscap.ViewPayload{ + Module: &aptoscap.ModuleID{Name: "coin"}, + Function: "name", + }, + wantErr: "payload.module.address is required", + }, + { + name: "missing function", + payload: &aptoscap.ViewPayload{ + Module: &aptoscap.ModuleID{Address: []byte{0x01}, Name: "coin"}, + }, + wantErr: "payload.function is required", + }, + { + name: "missing module name", + payload: &aptoscap.ViewPayload{ + Module: &aptoscap.ModuleID{Address: []byte{0x01}}, + Function: "name", + }, + wantErr: "payload.module.name is required", + }, + { + name: "oversized module address", + payload: &aptoscap.ViewPayload{ + Module: &aptoscap.ModuleID{Address: make([]byte, typesaptos.AccountAddressLength+1), Name: "coin"}, + Function: "name", + }, + wantErr: "module address too long", + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + _, err := aptoscap.ConvertViewPayloadFromProto(tc.payload) + require.ErrorContains(t, err, tc.wantErr) + }) + } +} + +func TestConvertTypeTagFromProto_RejectsInvalidInput(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + tag *aptoscap.TypeTag + wantErr string + }{ + { + name: "nil type tag", + tag: nil, + wantErr: "type tag is nil", + }, + { + name: "unsupported kind", + tag: &aptoscap.TypeTag{Kind: aptoscap.TypeTagKind(255)}, + wantErr: "unsupported type tag kind", + }, + { + name: "missing struct address", + tag: &aptoscap.TypeTag{ + Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_STRUCT, + Value: &aptoscap.TypeTag_Struct{Struct: &aptoscap.StructTag{ + Module: "coin", + Name: "Coin", + }}, + }, + wantErr: "struct address is required", + }, + { + name: "missing struct module", + tag: &aptoscap.TypeTag{ + Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_STRUCT, + Value: &aptoscap.TypeTag_Struct{Struct: &aptoscap.StructTag{ + Address: []byte{0x01}, + Name: "Coin", + }}, + }, + wantErr: "struct module is required", + }, + { + name: "missing struct name", + tag: &aptoscap.TypeTag{ + Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_STRUCT, + Value: &aptoscap.TypeTag_Struct{Struct: &aptoscap.StructTag{ + Address: []byte{0x01}, + Module: "coin", + }}, + }, + wantErr: "struct name is required", + }, + { + name: "oversized struct address", + tag: &aptoscap.TypeTag{ + Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_STRUCT, + Value: &aptoscap.TypeTag_Struct{Struct: &aptoscap.StructTag{ + Address: make([]byte, typesaptos.AccountAddressLength+1), + Module: "coin", + Name: "Coin", + }}, + }, + wantErr: "struct address too long", + }, + { + name: "invalid vector element type", + tag: &aptoscap.TypeTag{ + Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_VECTOR, + Value: &aptoscap.TypeTag_Vector{Vector: &aptoscap.VectorTag{ + ElementType: &aptoscap.TypeTag{ + Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_STRUCT, + Value: &aptoscap.TypeTag_Struct{Struct: &aptoscap.StructTag{ + Address: make([]byte, typesaptos.AccountAddressLength+1), + Module: "coin", + Name: "Coin", + }}, + }, + }}, + }, + wantErr: "invalid vector element type: struct address too long", + }, + { + name: "generic index out of range", + tag: &aptoscap.TypeTag{ + Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_GENERIC, + Value: &aptoscap.TypeTag_Generic{Generic: &aptoscap.GenericTag{Index: 1 << 16}}, + }, + wantErr: "generic type index out of range", + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + _, err := aptoscap.ConvertTypeTagFromProto(tc.tag) + require.ErrorContains(t, err, tc.wantErr) + }) + } +}