-
Notifications
You must be signed in to change notification settings - Fork 28
feat: add Aptos capability view proto helpers #1992
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1d36041
571c98d
82182b1
b4da11d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
Comment on lines
+90
to
+124
|
||
| 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 | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| }) | ||
| } | ||
| } | ||
|
Comment on lines
+57
to
+117
|
||
|
|
||
| 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) | ||
| }) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
payload.Module.Addresscan be empty/nil;convertAccountAddressFromProtowill then return the all-zero address without error. Since the module address is required to identify the view function, this should be rejected explicitly (e.g., requirelen(payload.Module.Address) > 0and return a "...Module.Address is required" error) to avoid silently calling address 0x0.