Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
135 changes: 135 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,135 @@
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("viewRequest.Payload is required")
}
if payload.Module == nil {
return nil, fmt.Errorf("viewRequest.Payload.Module is required")
}
if payload.Module.Name == "" {
return nil, fmt.Errorf("viewRequest.Payload.Module.Name is required")
}
if payload.Function == "" {
return nil, fmt.Errorf("viewRequest.Payload.Function is required")
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 validation errors in this helper reference viewRequest.Payload..., but the API accepts a *ViewPayload directly. Consider aligning the error strings with the function’s input (e.g., payload is required, payload.module is required) so callers who use the helper outside a ViewRequest context aren’t misled.

Suggested change
return nil, fmt.Errorf("viewRequest.Payload is required")
}
if payload.Module == nil {
return nil, fmt.Errorf("viewRequest.Payload.Module is required")
}
if payload.Module.Name == "" {
return nil, fmt.Errorf("viewRequest.Payload.Module.Name is required")
}
if payload.Function == "" {
return nil, fmt.Errorf("viewRequest.Payload.Function is required")
return nil, fmt.Errorf("payload is required")
}
if payload.Module == nil {
return nil, fmt.Errorf("payload.module is required")
}
if payload.Module.Name == "" {
return nil, fmt.Errorf("payload.module.name is required")
}
if payload.Function == "" {
return nil, fmt.Errorf("payload.function is required")

Copilot uses AI. Check for mistakes.
}

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

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 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
}
119 changes: 119 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,119 @@
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()

_, err := aptoscap.ConvertViewPayloadFromProto(nil)
require.ErrorContains(t, err, "viewRequest.Payload is required")

_, err = aptoscap.ConvertViewPayloadFromProto(&aptoscap.ViewPayload{Function: "name"})
require.ErrorContains(t, err, "viewRequest.Payload.Module is required")

_, err = aptoscap.ConvertViewPayloadFromProto(&aptoscap.ViewPayload{
Module: &aptoscap.ModuleID{Address: []byte{0x01}, Name: "coin"},
})
require.ErrorContains(t, err, "viewRequest.Payload.Function is required")

_, err = aptoscap.ConvertViewPayloadFromProto(&aptoscap.ViewPayload{
Module: &aptoscap.ModuleID{Address: []byte{0x01}},
Function: "name",
})
require.ErrorContains(t, err, "viewRequest.Payload.Module.Name is required")

_, err = aptoscap.ConvertViewPayloadFromProto(&aptoscap.ViewPayload{
Module: &aptoscap.ModuleID{Address: make([]byte, typesaptos.AccountAddressLength+1), Name: "coin"},
Function: "name",
})
require.ErrorContains(t, err, "module address too long")
}
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()

_, err := aptoscap.ConvertTypeTagFromProto(nil)
require.ErrorContains(t, err, "type tag is nil")

_, err = aptoscap.ConvertTypeTagFromProto(&aptoscap.TypeTag{Kind: aptoscap.TypeTagKind(255)})
require.ErrorContains(t, err, "unsupported type tag kind")

_, err = aptoscap.ConvertTypeTagFromProto(&aptoscap.TypeTag{
Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_STRUCT,
Value: &aptoscap.TypeTag_Struct{Struct: &aptoscap.StructTag{
Address: make([]byte, typesaptos.AccountAddressLength+1),
}},
})
require.ErrorContains(t, err, "struct address too long")

_, err = aptoscap.ConvertTypeTagFromProto(&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),
}},
},
}},
})
require.ErrorContains(t, err, "invalid vector element type: struct address too long")

_, err = aptoscap.ConvertTypeTagFromProto(&aptoscap.TypeTag{
Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_GENERIC,
Value: &aptoscap.TypeTag_Generic{Generic: &aptoscap.GenericTag{Index: 1 << 16}},
})
require.ErrorContains(t, err, "generic type index out of range")
}
Loading