Skip to content

Commit

Permalink
Merge branch 'main' into broadcast
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronbuchwald committed Oct 10, 2024
2 parents cd657d9 + 182e023 commit 4cd6a4e
Show file tree
Hide file tree
Showing 42 changed files with 1,088 additions and 364 deletions.
1 change: 1 addition & 0 deletions abi/abi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func TestNewABI(t *testing.T) {
Outer{},
ActionWithOutput{},
FixedBytes{},
Bools{},
}, []codec.Typed{
ActionOutput{},
})
Expand Down
1 change: 1 addition & 0 deletions abi/auto_marshal_abi_spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func TestMarshalSpecs(t *testing.T) {
{"strOnly", &MockObjectStringAndBytes{}},
{"outer", &Outer{}},
{"fixedBytes", &FixedBytes{}},
{"bools", &Bools{}},
}

for _, tc := range testCases {
Expand Down
170 changes: 170 additions & 0 deletions abi/dynamic/reflect_marshal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Copyright (C) 2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package dynamic

import (
"encoding/json"
"errors"
"fmt"
"reflect"
"regexp"
"strconv"
"strings"

"github.com/ava-labs/avalanchego/utils/wrappers"
"golang.org/x/text/cases"
"golang.org/x/text/language"

"github.com/ava-labs/hypersdk/abi"
"github.com/ava-labs/hypersdk/codec"
"github.com/ava-labs/hypersdk/consts"
)

var ErrTypeNotFound = errors.New("type not found in ABI")

func Marshal(inputABI abi.ABI, typeName string, jsonData string) ([]byte, error) {
if _, ok := findABIType(inputABI, typeName); !ok {
return nil, fmt.Errorf("marshalling %s: %w", typeName, ErrTypeNotFound)
}

typeCache := make(map[string]reflect.Type)

typ, err := getReflectType(typeName, inputABI, typeCache)
if err != nil {
return nil, fmt.Errorf("failed to get reflect type: %w", err)
}

value := reflect.New(typ).Interface()

if err := json.Unmarshal([]byte(jsonData), value); err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON data: %w", err)
}

writer := codec.NewWriter(0, consts.NetworkSizeLimit)
if err := codec.LinearCodec.MarshalInto(value, writer.Packer); err != nil {
return nil, fmt.Errorf("failed to marshal struct: %w", err)
}

return writer.Bytes(), nil
}

func Unmarshal(inputABI abi.ABI, typeName string, data []byte) (string, error) {
if _, ok := findABIType(inputABI, typeName); !ok {
return "", fmt.Errorf("unmarshalling %s: %w", typeName, ErrTypeNotFound)
}

typeCache := make(map[string]reflect.Type)

typ, err := getReflectType(typeName, inputABI, typeCache)
if err != nil {
return "", fmt.Errorf("failed to get reflect type: %w", err)
}

value := reflect.New(typ).Interface()

packer := wrappers.Packer{
Bytes: data,
MaxSize: consts.NetworkSizeLimit,
}
if err := codec.LinearCodec.UnmarshalFrom(&packer, value); err != nil {
return "", fmt.Errorf("failed to unmarshal data: %w", err)
}

jsonData, err := json.Marshal(value)
if err != nil {
return "", fmt.Errorf("failed to marshal struct to JSON: %w", err)
}

return string(jsonData), nil
}

// Matches fixed-size arrays like [32]uint8
var fixedSizeArrayRegex = regexp.MustCompile(`^\[(\d+)\](.+)$`)

func getReflectType(abiTypeName string, inputABI abi.ABI, typeCache map[string]reflect.Type) (reflect.Type, error) {
switch abiTypeName {
case "string":
return reflect.TypeOf(""), nil
case "uint8":
return reflect.TypeOf(uint8(0)), nil
case "uint16":
return reflect.TypeOf(uint16(0)), nil
case "uint32":
return reflect.TypeOf(uint32(0)), nil
case "uint64":
return reflect.TypeOf(uint64(0)), nil
case "int8":
return reflect.TypeOf(int8(0)), nil
case "int16":
return reflect.TypeOf(int16(0)), nil
case "int32":
return reflect.TypeOf(int32(0)), nil
case "int64":
return reflect.TypeOf(int64(0)), nil
case "Address":
return reflect.TypeOf(codec.Address{}), nil
default:
// golang slices
if strings.HasPrefix(abiTypeName, "[]") {
elemType, err := getReflectType(strings.TrimPrefix(abiTypeName, "[]"), inputABI, typeCache)
if err != nil {
return nil, err
}
return reflect.SliceOf(elemType), nil
}

// golang arrays

if match := fixedSizeArrayRegex.FindStringSubmatch(abiTypeName); match != nil {
sizeStr := match[1]
size, err := strconv.Atoi(sizeStr)
if err != nil {
return nil, fmt.Errorf("failed to convert size to int: %w", err)
}
elemType, err := getReflectType(match[2], inputABI, typeCache)
if err != nil {
return nil, err
}
return reflect.ArrayOf(size, elemType), nil
}

// For custom types, recursively construct the struct type
if cachedType, ok := typeCache[abiTypeName]; ok {
return cachedType, nil
}

abiType, ok := findABIType(inputABI, abiTypeName)
if !ok {
return nil, fmt.Errorf("type %s not found in ABI", abiTypeName)
}

// It is a struct, as we don't support anything else as custom types
fields := make([]reflect.StructField, len(abiType.Fields))
for i, field := range abiType.Fields {
fieldType, err := getReflectType(field.Type, inputABI, typeCache)
if err != nil {
return nil, err
}
fields[i] = reflect.StructField{
Name: cases.Title(language.English).String(field.Name),
Type: fieldType,
Tag: reflect.StructTag(fmt.Sprintf(`serialize:"true" json:"%s"`, field.Name)),
}
}

structType := reflect.StructOf(fields)
typeCache[abiTypeName] = structType

return structType, nil
}
}

func findABIType(inputABI abi.ABI, typeName string) (abi.Type, bool) {
for _, typ := range inputABI.Types {
if typ.Name == typeName {
return typ, true
}
}
return abi.Type{}, false
}
94 changes: 94 additions & 0 deletions abi/dynamic/reflect_marshal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (C) 2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package dynamic

import (
"encoding/hex"
"encoding/json"
"os"
"strings"
"testing"

"github.com/stretchr/testify/require"

"github.com/ava-labs/hypersdk/abi"
)

func TestDynamicMarshal(t *testing.T) {
require := require.New(t)

abiJSON := mustReadFile(t, "../testdata/abi.json")
var abi abi.ABI

err := json.Unmarshal(abiJSON, &abi)
require.NoError(err)

testCases := []struct {
name string
typeName string
}{
{"empty", "MockObjectSingleNumber"},
{"uint16", "MockObjectSingleNumber"},
{"numbers", "MockObjectAllNumbers"},
{"arrays", "MockObjectArrays"},
{"transfer", "MockActionTransfer"},
{"transferField", "MockActionWithTransfer"},
{"transfersArray", "MockActionWithTransferArray"},
{"strBytes", "MockObjectStringAndBytes"},
{"strByteZero", "MockObjectStringAndBytes"},
{"strBytesEmpty", "MockObjectStringAndBytes"},
{"strOnly", "MockObjectStringAndBytes"},
{"outer", "Outer"},
{"fixedBytes", "FixedBytes"},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Read the JSON data
jsonData := mustReadFile(t, "../testdata/"+tc.name+".json")

objectBytes, err := Marshal(abi, tc.typeName, string(jsonData))
require.NoError(err)

// Compare with expected hex
expectedHex := string(mustReadFile(t, "../testdata/"+tc.name+".hex"))
expectedHex = strings.TrimSpace(expectedHex)
require.Equal(expectedHex, hex.EncodeToString(objectBytes))

unmarshaledJSON, err := Unmarshal(abi, tc.typeName, objectBytes)
require.NoError(err)

// Compare with expected JSON
require.JSONEq(string(jsonData), unmarshaledJSON)
})
}
}

func TestDynamicMarshalErrors(t *testing.T) {
require := require.New(t)

abiJSON := mustReadFile(t, "../testdata/abi.json")
var abi abi.ABI

err := json.Unmarshal(abiJSON, &abi)
require.NoError(err)

// Test malformed JSON
malformedJSON := `{"uint8": 42, "uint16": 1000, "uint32": 100000, "uint64": 10000000000, "int8": -42, "int16": -1000, "int32": -100000, "int64": -10000000000,`
_, err = Marshal(abi, "MockObjectAllNumbers", malformedJSON)
require.Contains(err.Error(), "unexpected end of JSON input")

// Test wrong struct name
jsonData := mustReadFile(t, "../testdata/numbers.json")
_, err = Marshal(abi, "NonExistentObject", string(jsonData))
require.ErrorIs(err, ErrTypeNotFound)
}

func mustReadFile(t *testing.T, path string) []byte {
t.Helper()

content, err := os.ReadFile(path)
require.NoError(t, err)
return content
}
10 changes: 10 additions & 0 deletions abi/mockabi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ type FixedBytes struct {
ThirtyTwoBytes [32]uint8 `serialize:"true" json:"thirtyTwoBytes"`
}

type Bools struct {
Bool1 bool `serialize:"true" json:"bool1"`
Bool2 bool `serialize:"true" json:"bool2"`
BoolArray []bool `serialize:"true" json:"boolArray"`
}

type ActionOutput struct {
Field1 uint16 `serialize:"true" json:"field1"`
}
Expand Down Expand Up @@ -114,6 +120,10 @@ func (FixedBytes) GetTypeID() uint8 {
return 9
}

func (Bools) GetTypeID() uint8 {
return 10
}

func (ActionOutput) GetTypeID() uint8 {
return 0
}
2 changes: 1 addition & 1 deletion abi/testdata/abi.hash.hex
Original file line number Diff line number Diff line change
@@ -1 +1 @@
075005590e3e3e39dffbf8e5a6d77559b8a33e621078782571695f60938126d3
3b634237434bc35076e790b52986d735f30d582e9b7b94ad357d91b33072e284
33 changes: 27 additions & 6 deletions abi/testdata/abi.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
{
"id": 9,
"name": "FixedBytes"
},
{
"id": 10,
"name": "Bools"
}
],
"outputs": [
Expand Down Expand Up @@ -210,15 +214,16 @@
]
},
{
"name": "ActionWithOutput",
"fields": [
{
"name": "field1",
"type": "uint8"
}
],
"name": "ActionWithOutput"
]
},
{
"name": "FixedBytes",
"fields": [
{
"name": "twoBytes",
Expand All @@ -228,17 +233,33 @@
"name": "thirtyTwoBytes",
"type": "[32]uint8"
}
],
"name": "FixedBytes"
]
},
{
"name": "Bools",
"fields": [
{
"name": "bool1",
"type": "bool"
},
{
"name": "bool2",
"type": "bool"
},
{
"name": "boolArray",
"type": "[]bool"
}
]
},
{
"name": "ActionOutput",
"fields": [
{
"name": "field1",
"type": "uint16"
}
],
"name": "ActionOutput"
]
}
]
}
1 change: 1 addition & 0 deletions abi/testdata/bools.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
000100000003010001
9 changes: 9 additions & 0 deletions abi/testdata/bools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"bool1": false,
"bool2": true,
"boolArray": [
true,
false,
true
]
}
2 changes: 1 addition & 1 deletion abi/testdata/transfer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"to": "0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000",
"to": "0x0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000",
"value": 1000,
"memo": "aGk="
}
Loading

0 comments on commit 4cd6a4e

Please sign in to comment.