Skip to content
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

Reflect Marshaler #1592

Merged
merged 15 commits into from
Oct 10, 2024
Merged
162 changes: 162 additions & 0 deletions abi/dynamic/reflect_marshal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright (C) 2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package dynamic

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

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

func DynamicMarshal(inputAbi abi.ABI, typeName string, jsonData string) ([]byte, error) {
containerman17 marked this conversation as resolved.
Show resolved Hide resolved
containerman17 marked this conversation as resolved.
Show resolved Hide resolved
// Find the type in the ABI
abiType := findABIType(inputAbi, typeName)
if abiType == nil {
return nil, fmt.Errorf("type %s not found in ABI", typeName)
}

// Create a cache to avoid rebuilding types
containerman17 marked this conversation as resolved.
Show resolved Hide resolved
typeCache := make(map[string]reflect.Type)

// Create a dynamic struct type
dynamicType := getReflectType(typeName, inputAbi, typeCache)
containerman17 marked this conversation as resolved.
Show resolved Hide resolved

// Create an instance of the dynamic struct
dynamicValue := reflect.New(dynamicType).Interface()

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

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

return writer.Bytes(), nil
}

func DynamicUnmarshal(inputAbi abi.ABI, typeName string, data []byte) (string, error) {
containerman17 marked this conversation as resolved.
Show resolved Hide resolved
// Find the type in the ABI
abiType := findABIType(inputAbi, typeName)
if abiType == nil {
return "", fmt.Errorf("type %s not found in ABI", typeName)
}

// Create a cache to avoid rebuilding types
typeCache := make(map[string]reflect.Type)

// Create a dynamic struct type
dynamicType := getReflectType(typeName, inputAbi, typeCache)

// Create an instance of the dynamic struct
dynamicValue := reflect.New(dynamicType).Interface()

// Unmarshal the data into the dynamic struct
if err := codec.LinearCodec.Unmarshal(data, dynamicValue); err != nil {
return "", fmt.Errorf("failed to unmarshal data: %w", err)
}

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

return string(jsonData), nil
}

func getReflectType(abiTypeName string, inputAbi abi.ABI, typeCache map[string]reflect.Type) reflect.Type {
switch abiTypeName {
case "string":
return reflect.TypeOf("")
case "uint8":
return reflect.TypeOf(uint8(0))
case "uint16":
return reflect.TypeOf(uint16(0))
case "uint32":
return reflect.TypeOf(uint32(0))
case "uint64":
return reflect.TypeOf(uint64(0))
case "int8":
return reflect.TypeOf(int8(0))
case "int16":
return reflect.TypeOf(int16(0))
case "int32":
return reflect.TypeOf(int32(0))
case "int64":
return reflect.TypeOf(int64(0))
case "Address":
return reflect.TypeOf(codec.Address{})
default:
if strings.HasPrefix(abiTypeName, "[]") {
elemType := getReflectType(strings.TrimPrefix(abiTypeName, "[]"), inputAbi, typeCache)
return reflect.SliceOf(elemType)
} else if strings.HasPrefix(abiTypeName, "[") {
containerman17 marked this conversation as resolved.
Show resolved Hide resolved
// Handle fixed-size arrays

sizeStr := strings.Split(abiTypeName, "]")[0]
sizeStr = strings.TrimPrefix(sizeStr, "[")

size, err := strconv.Atoi(sizeStr)
if err != nil {
return reflect.TypeOf((*interface{})(nil)).Elem()
containerman17 marked this conversation as resolved.
Show resolved Hide resolved
}
elemType := getReflectType(strings.TrimPrefix(abiTypeName, "["+sizeStr+"]"), inputAbi, typeCache)
containerman17 marked this conversation as resolved.
Show resolved Hide resolved
return reflect.ArrayOf(size, elemType)
}
// For custom types, recursively construct the struct type

// Check if type already in cache
if cachedType, ok := typeCache[abiTypeName]; ok {
containerman17 marked this conversation as resolved.
Show resolved Hide resolved
return cachedType
}

// Find the type in the ABI
abiType := findABIType(inputAbi, abiTypeName)
if abiType == nil {
// If not found, fallback to interface{}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why? Please elaborate in the comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It makes much more sense to return errors here instead of just an empty value, I agree. Yeah, that was a dumb thing to do.

Copy link
Contributor

Choose a reason for hiding this comment

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

that was a dumb thing to do

I don't actually think it was dumb as there could be reasons (e.g. you just want the JSON package to decide on the correct native type). I was just wondering what the reason was / prompting discussion about it.

return reflect.TypeOf((*interface{})(nil)).Elem()
containerman17 marked this conversation as resolved.
Show resolved Hide resolved
}

// Build fields
fields := make([]reflect.StructField, len(abiType.Fields))
containerman17 marked this conversation as resolved.
Show resolved Hide resolved
for i, field := range abiType.Fields {
fieldType := getReflectType(field.Type, inputAbi, typeCache)
containerman17 marked this conversation as resolved.
Show resolved Hide resolved
fields[i] = reflect.StructField{
Name: cases.Title(language.English).String(field.Name),
containerman17 marked this conversation as resolved.
Show resolved Hide resolved
Type: fieldType,
Tag: reflect.StructTag(fmt.Sprintf(`serialize:"true" json:"%s"`, field.Name)),
}
}
// Create struct type
structType := reflect.StructOf(fields)

// Cache the type
typeCache[abiTypeName] = structType

return structType
}
}

// Helper function to find ABI type
func findABIType(inputAbi abi.ABI, typeName string) *abi.Type {
for i := range inputAbi.Types {
containerman17 marked this conversation as resolved.
Show resolved Hide resolved
if inputAbi.Types[i].Name == typeName {
return &inputAbi.Types[i]
containerman17 marked this conversation as resolved.
Show resolved Hide resolved
}
}
return nil
}
76 changes: 76 additions & 0 deletions abi/dynamic/reflect_marshal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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)

// Load the ABI
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")

// Use DynamicMarshal to marshal the data
objectBytes, err := DynamicMarshal(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))

// Use DynamicUnmarshal to unmarshal the data
unmarshaledJSON, err := DynamicUnmarshal(abi, tc.typeName, objectBytes)
require.NoError(err)

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

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

content, err := os.ReadFile(path)
require.NoError(t, err)
return content
}
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="
}
2 changes: 1 addition & 1 deletion abi/testdata/transferField.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"transfer": {
"to": "0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000",
"to": "0x0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000",
"value": 1000,
"memo": "aGk="
}
Expand Down
4 changes: 2 additions & 2 deletions abi/testdata/transfersArray.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"transfers": [
{
"to": "0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000",
"to": "0x0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000",
"value": 1000,
"memo": "aGk="
},
{
"to": "0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000",
"to": "0x0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000",
"value": 1000,
"memo": "aGk="
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ require (
golang.org/x/crypto v0.21.0
golang.org/x/exp v0.0.0-20231127185646-65229373498e
golang.org/x/sync v0.6.0
golang.org/x/text v0.14.0
google.golang.org/grpc v1.62.0
google.golang.org/protobuf v1.34.2
gopkg.in/yaml.v2 v2.4.0
Expand Down Expand Up @@ -144,7 +145,6 @@ require (
golang.org/x/net v0.23.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.17.0 // indirect
gonum.org/v1/gonum v0.11.0 // indirect
Expand Down
Loading