Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions pkg/capabilities/v2/chain-capabilities/aptos/proto_helpers.go
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
}
Comment on lines +17 to +33
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

payload.Module.Address can be empty/nil; convertAccountAddressFromProto will 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., require len(payload.Module.Address) > 0 and return a "...Module.Address is required" error) to avoid silently calling address 0x0.

Copilot uses AI. Check for mistakes.

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
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the STRUCT branch, structTag.Address is allowed to be empty (converted to 0x0) and structTag.Module/structTag.Name are not validated. A Move struct type is not well-formed without these fields, so this converter should enforce non-empty Address, Module, and Name and return a clear validation error when missing.

Copilot uses AI. Check for mistakes.
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
}
218 changes: 218 additions & 0 deletions pkg/capabilities/v2/chain-capabilities/aptos/proto_helpers_test.go
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
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The negative tests don’t currently cover (a) missing/empty module address and (b) struct type tags with empty module/name (or empty address). If the helper is intended to centralize validation, please add test cases for these required-field scenarios to lock in the expected errors.

Copilot uses AI. Check for mistakes.

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)
})
}
}
Loading