-
Notifications
You must be signed in to change notification settings - Fork 118
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
42 changed files
with
1,088 additions
and
364 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
075005590e3e3e39dffbf8e5a6d77559b8a33e621078782571695f60938126d3 | ||
3b634237434bc35076e790b52986d735f30d582e9b7b94ad357d91b33072e284 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
000100000003010001 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"bool1": false, | ||
"bool2": true, | ||
"boolArray": [ | ||
true, | ||
false, | ||
true | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
{ | ||
"to": "0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000", | ||
"to": "0x0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000", | ||
"value": 1000, | ||
"memo": "aGk=" | ||
} |
Oops, something went wrong.