Skip to content

Commit 52b7a5a

Browse files
authored
feat: add Aptos capability view proto helpers (#1992)
* feat: add aptos capability view proto helpers * fix: tighten aptos view proto helper validation * fix: tighten aptos proto helper validation * refactor: tighten aptos proto validation helpers
1 parent a324267 commit 52b7a5a

2 files changed

Lines changed: 379 additions & 0 deletions

File tree

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package aptos
2+
3+
import (
4+
"fmt"
5+
"math"
6+
7+
typesaptos "github.com/smartcontractkit/chainlink-common/pkg/types/chains/aptos"
8+
)
9+
10+
// ConvertViewPayloadFromProto converts a capability ViewPayload into Aptos domain types.
11+
// Capability requests currently accept shortened Aptos addresses, so this helper left-pads
12+
// addresses up to 32 bytes instead of requiring exact-length address bytes.
13+
func ConvertViewPayloadFromProto(payload *ViewPayload) (*typesaptos.ViewPayload, error) {
14+
if payload == nil {
15+
return nil, fmt.Errorf("payload is required")
16+
}
17+
if payload.Module == nil {
18+
return nil, fmt.Errorf("payload.module is required")
19+
}
20+
if err := requireNonEmptyBytes(payload.Module.Address, "payload.module.address"); err != nil {
21+
return nil, err
22+
}
23+
if err := requireNonEmptyString(payload.Module.Name, "payload.module.name"); err != nil {
24+
return nil, err
25+
}
26+
if err := requireNonEmptyString(payload.Function, "payload.function"); err != nil {
27+
return nil, err
28+
}
29+
30+
moduleAddress, err := convertAccountAddressFromProto(payload.Module.Address, "module")
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
argTypes := make([]typesaptos.TypeTag, 0, len(payload.ArgTypes))
36+
for i, tag := range payload.ArgTypes {
37+
converted, err := ConvertTypeTagFromProto(tag)
38+
if err != nil {
39+
return nil, fmt.Errorf("invalid arg type at index %d: %w", i, err)
40+
}
41+
argTypes = append(argTypes, *converted)
42+
}
43+
44+
return &typesaptos.ViewPayload{
45+
Module: typesaptos.ModuleID{
46+
Address: moduleAddress,
47+
Name: payload.Module.Name,
48+
},
49+
Function: payload.Function,
50+
ArgTypes: argTypes,
51+
Args: payload.Args,
52+
}, nil
53+
}
54+
55+
// ConvertTypeTagFromProto converts a capability TypeTag into Aptos domain types.
56+
func ConvertTypeTagFromProto(tag *TypeTag) (*typesaptos.TypeTag, error) {
57+
if tag == nil {
58+
return nil, fmt.Errorf("type tag is nil")
59+
}
60+
61+
switch tag.Kind {
62+
case TypeTagKind_TYPE_TAG_KIND_BOOL:
63+
return &typesaptos.TypeTag{Value: typesaptos.BoolTag{}}, nil
64+
case TypeTagKind_TYPE_TAG_KIND_U8:
65+
return &typesaptos.TypeTag{Value: typesaptos.U8Tag{}}, nil
66+
case TypeTagKind_TYPE_TAG_KIND_U16:
67+
return &typesaptos.TypeTag{Value: typesaptos.U16Tag{}}, nil
68+
case TypeTagKind_TYPE_TAG_KIND_U32:
69+
return &typesaptos.TypeTag{Value: typesaptos.U32Tag{}}, nil
70+
case TypeTagKind_TYPE_TAG_KIND_U64:
71+
return &typesaptos.TypeTag{Value: typesaptos.U64Tag{}}, nil
72+
case TypeTagKind_TYPE_TAG_KIND_U128:
73+
return &typesaptos.TypeTag{Value: typesaptos.U128Tag{}}, nil
74+
case TypeTagKind_TYPE_TAG_KIND_U256:
75+
return &typesaptos.TypeTag{Value: typesaptos.U256Tag{}}, nil
76+
case TypeTagKind_TYPE_TAG_KIND_ADDRESS:
77+
return &typesaptos.TypeTag{Value: typesaptos.AddressTag{}}, nil
78+
case TypeTagKind_TYPE_TAG_KIND_SIGNER:
79+
return &typesaptos.TypeTag{Value: typesaptos.SignerTag{}}, nil
80+
case TypeTagKind_TYPE_TAG_KIND_VECTOR:
81+
vector := tag.GetVector()
82+
if vector == nil {
83+
return nil, fmt.Errorf("vector tag missing vector value")
84+
}
85+
elementType, err := ConvertTypeTagFromProto(vector.ElementType)
86+
if err != nil {
87+
return nil, fmt.Errorf("invalid vector element type: %w", err)
88+
}
89+
return &typesaptos.TypeTag{Value: typesaptos.VectorTag{ElementType: *elementType}}, nil
90+
case TypeTagKind_TYPE_TAG_KIND_STRUCT:
91+
structTag := tag.GetStruct()
92+
if structTag == nil {
93+
return nil, fmt.Errorf("struct tag missing struct value")
94+
}
95+
if err := requireNonEmptyBytes(structTag.Address, "struct address"); err != nil {
96+
return nil, err
97+
}
98+
if err := requireNonEmptyString(structTag.Module, "struct module"); err != nil {
99+
return nil, err
100+
}
101+
if err := requireNonEmptyString(structTag.Name, "struct name"); err != nil {
102+
return nil, err
103+
}
104+
105+
structAddress, err := convertAccountAddressFromProto(structTag.Address, "struct")
106+
if err != nil {
107+
return nil, err
108+
}
109+
110+
typeParams := make([]typesaptos.TypeTag, 0, len(structTag.TypeParams))
111+
for i, tp := range structTag.TypeParams {
112+
converted, err := ConvertTypeTagFromProto(tp)
113+
if err != nil {
114+
return nil, fmt.Errorf("invalid struct type param at index %d: %w", i, err)
115+
}
116+
typeParams = append(typeParams, *converted)
117+
}
118+
119+
return &typesaptos.TypeTag{Value: typesaptos.StructTag{
120+
Address: structAddress,
121+
Module: structTag.Module,
122+
Name: structTag.Name,
123+
TypeParams: typeParams,
124+
}}, nil
125+
case TypeTagKind_TYPE_TAG_KIND_GENERIC:
126+
generic := tag.GetGeneric()
127+
if generic == nil {
128+
return nil, fmt.Errorf("generic tag missing generic value")
129+
}
130+
if generic.Index > math.MaxUint16 {
131+
return nil, fmt.Errorf("generic type index out of range: %d", generic.Index)
132+
}
133+
return &typesaptos.TypeTag{Value: typesaptos.GenericTag{Index: uint16(generic.Index)}}, nil
134+
default:
135+
return nil, fmt.Errorf("unsupported type tag kind: %v", tag.Kind)
136+
}
137+
}
138+
139+
func requireNonEmptyBytes(value []byte, field string) error {
140+
if len(value) == 0 {
141+
return fmt.Errorf("%s is required", field)
142+
}
143+
return nil
144+
}
145+
146+
func requireNonEmptyString(value string, field string) error {
147+
if value == "" {
148+
return fmt.Errorf("%s is required", field)
149+
}
150+
return nil
151+
}
152+
153+
func convertAccountAddressFromProto(address []byte, field string) (typesaptos.AccountAddress, error) {
154+
if len(address) > typesaptos.AccountAddressLength {
155+
return typesaptos.AccountAddress{}, fmt.Errorf("%s address too long: %d", field, len(address))
156+
}
157+
158+
var converted typesaptos.AccountAddress
159+
copy(converted[typesaptos.AccountAddressLength-len(address):], address)
160+
return converted, nil
161+
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package aptos_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
aptoscap "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/aptos"
9+
typesaptos "github.com/smartcontractkit/chainlink-common/pkg/types/chains/aptos"
10+
)
11+
12+
func TestConvertViewPayloadFromProto_ConvertsNestedVectorStructAndGenericTags(t *testing.T) {
13+
t.Parallel()
14+
15+
payload, err := aptoscap.ConvertViewPayloadFromProto(&aptoscap.ViewPayload{
16+
Module: &aptoscap.ModuleID{Address: []byte{0x01}, Name: "coin"},
17+
Function: "name",
18+
ArgTypes: []*aptoscap.TypeTag{
19+
{
20+
Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_VECTOR,
21+
Value: &aptoscap.TypeTag_Vector{Vector: &aptoscap.VectorTag{
22+
ElementType: &aptoscap.TypeTag{
23+
Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_STRUCT,
24+
Value: &aptoscap.TypeTag_Struct{Struct: &aptoscap.StructTag{
25+
Address: []byte{0x02},
26+
Module: "aptos_coin",
27+
Name: "Coin",
28+
TypeParams: []*aptoscap.TypeTag{
29+
{
30+
Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_GENERIC,
31+
Value: &aptoscap.TypeTag_Generic{Generic: &aptoscap.GenericTag{Index: 7}},
32+
},
33+
},
34+
}},
35+
},
36+
}},
37+
},
38+
},
39+
})
40+
require.NoError(t, err)
41+
require.NotNil(t, payload)
42+
require.Equal(t, "name", payload.Function)
43+
require.Len(t, payload.ArgTypes, 1)
44+
45+
vectorTag, ok := payload.ArgTypes[0].Value.(typesaptos.VectorTag)
46+
require.True(t, ok)
47+
structTag, ok := vectorTag.ElementType.Value.(typesaptos.StructTag)
48+
require.True(t, ok)
49+
require.Equal(t, "aptos_coin", structTag.Module)
50+
require.Equal(t, "Coin", structTag.Name)
51+
require.Len(t, structTag.TypeParams, 1)
52+
genericTag, ok := structTag.TypeParams[0].Value.(typesaptos.GenericTag)
53+
require.True(t, ok)
54+
require.EqualValues(t, 7, genericTag.Index)
55+
}
56+
57+
func TestConvertViewPayloadFromProto_RejectsInvalidPayloadInputs(t *testing.T) {
58+
t.Parallel()
59+
60+
testCases := []struct {
61+
name string
62+
payload *aptoscap.ViewPayload
63+
wantErr string
64+
}{
65+
{
66+
name: "missing payload",
67+
payload: nil,
68+
wantErr: "payload is required",
69+
},
70+
{
71+
name: "missing module",
72+
payload: &aptoscap.ViewPayload{Function: "name"},
73+
wantErr: "payload.module is required",
74+
},
75+
{
76+
name: "missing module address",
77+
payload: &aptoscap.ViewPayload{
78+
Module: &aptoscap.ModuleID{Name: "coin"},
79+
Function: "name",
80+
},
81+
wantErr: "payload.module.address is required",
82+
},
83+
{
84+
name: "missing function",
85+
payload: &aptoscap.ViewPayload{
86+
Module: &aptoscap.ModuleID{Address: []byte{0x01}, Name: "coin"},
87+
},
88+
wantErr: "payload.function is required",
89+
},
90+
{
91+
name: "missing module name",
92+
payload: &aptoscap.ViewPayload{
93+
Module: &aptoscap.ModuleID{Address: []byte{0x01}},
94+
Function: "name",
95+
},
96+
wantErr: "payload.module.name is required",
97+
},
98+
{
99+
name: "oversized module address",
100+
payload: &aptoscap.ViewPayload{
101+
Module: &aptoscap.ModuleID{Address: make([]byte, typesaptos.AccountAddressLength+1), Name: "coin"},
102+
Function: "name",
103+
},
104+
wantErr: "module address too long",
105+
},
106+
}
107+
108+
for _, tc := range testCases {
109+
tc := tc
110+
t.Run(tc.name, func(t *testing.T) {
111+
t.Parallel()
112+
113+
_, err := aptoscap.ConvertViewPayloadFromProto(tc.payload)
114+
require.ErrorContains(t, err, tc.wantErr)
115+
})
116+
}
117+
}
118+
119+
func TestConvertTypeTagFromProto_RejectsInvalidInput(t *testing.T) {
120+
t.Parallel()
121+
122+
testCases := []struct {
123+
name string
124+
tag *aptoscap.TypeTag
125+
wantErr string
126+
}{
127+
{
128+
name: "nil type tag",
129+
tag: nil,
130+
wantErr: "type tag is nil",
131+
},
132+
{
133+
name: "unsupported kind",
134+
tag: &aptoscap.TypeTag{Kind: aptoscap.TypeTagKind(255)},
135+
wantErr: "unsupported type tag kind",
136+
},
137+
{
138+
name: "missing struct address",
139+
tag: &aptoscap.TypeTag{
140+
Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_STRUCT,
141+
Value: &aptoscap.TypeTag_Struct{Struct: &aptoscap.StructTag{
142+
Module: "coin",
143+
Name: "Coin",
144+
}},
145+
},
146+
wantErr: "struct address is required",
147+
},
148+
{
149+
name: "missing struct module",
150+
tag: &aptoscap.TypeTag{
151+
Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_STRUCT,
152+
Value: &aptoscap.TypeTag_Struct{Struct: &aptoscap.StructTag{
153+
Address: []byte{0x01},
154+
Name: "Coin",
155+
}},
156+
},
157+
wantErr: "struct module is required",
158+
},
159+
{
160+
name: "missing struct name",
161+
tag: &aptoscap.TypeTag{
162+
Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_STRUCT,
163+
Value: &aptoscap.TypeTag_Struct{Struct: &aptoscap.StructTag{
164+
Address: []byte{0x01},
165+
Module: "coin",
166+
}},
167+
},
168+
wantErr: "struct name is required",
169+
},
170+
{
171+
name: "oversized struct address",
172+
tag: &aptoscap.TypeTag{
173+
Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_STRUCT,
174+
Value: &aptoscap.TypeTag_Struct{Struct: &aptoscap.StructTag{
175+
Address: make([]byte, typesaptos.AccountAddressLength+1),
176+
Module: "coin",
177+
Name: "Coin",
178+
}},
179+
},
180+
wantErr: "struct address too long",
181+
},
182+
{
183+
name: "invalid vector element type",
184+
tag: &aptoscap.TypeTag{
185+
Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_VECTOR,
186+
Value: &aptoscap.TypeTag_Vector{Vector: &aptoscap.VectorTag{
187+
ElementType: &aptoscap.TypeTag{
188+
Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_STRUCT,
189+
Value: &aptoscap.TypeTag_Struct{Struct: &aptoscap.StructTag{
190+
Address: make([]byte, typesaptos.AccountAddressLength+1),
191+
Module: "coin",
192+
Name: "Coin",
193+
}},
194+
},
195+
}},
196+
},
197+
wantErr: "invalid vector element type: struct address too long",
198+
},
199+
{
200+
name: "generic index out of range",
201+
tag: &aptoscap.TypeTag{
202+
Kind: aptoscap.TypeTagKind_TYPE_TAG_KIND_GENERIC,
203+
Value: &aptoscap.TypeTag_Generic{Generic: &aptoscap.GenericTag{Index: 1 << 16}},
204+
},
205+
wantErr: "generic type index out of range",
206+
},
207+
}
208+
209+
for _, tc := range testCases {
210+
tc := tc
211+
t.Run(tc.name, func(t *testing.T) {
212+
t.Parallel()
213+
214+
_, err := aptoscap.ConvertTypeTagFromProto(tc.tag)
215+
require.ErrorContains(t, err, tc.wantErr)
216+
})
217+
}
218+
}

0 commit comments

Comments
 (0)