From fc00a77b7528c752a67a905ac40e78349b501e4c Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Fri, 27 Sep 2024 04:13:28 +0000 Subject: [PATCH 01/53] reflect marshaler --- abi/dynamic/reflect_marshal.go | 162 ++++++++++++++++++++++++++++ abi/dynamic/reflect_marshal_test.go | 76 +++++++++++++ abi/testdata/transfer.json | 2 +- abi/testdata/transferField.json | 2 +- abi/testdata/transfersArray.json | 4 +- 5 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 abi/dynamic/reflect_marshal.go create mode 100644 abi/dynamic/reflect_marshal_test.go diff --git a/abi/dynamic/reflect_marshal.go b/abi/dynamic/reflect_marshal.go new file mode 100644 index 0000000000..634652b4b4 --- /dev/null +++ b/abi/dynamic/reflect_marshal.go @@ -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) { + // 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 + 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 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) { + // 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, "[") { + // 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() + } + elemType := getReflectType(strings.TrimPrefix(abiTypeName, "["+sizeStr+"]"), inputAbi, typeCache) + 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 { + return cachedType + } + + // Find the type in the ABI + abiType := findABIType(inputAbi, abiTypeName) + if abiType == nil { + // If not found, fallback to interface{} + return reflect.TypeOf((*interface{})(nil)).Elem() + } + + // Build fields + fields := make([]reflect.StructField, len(abiType.Fields)) + for i, field := range abiType.Fields { + fieldType := getReflectType(field.Type, inputAbi, typeCache) + 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)), + } + } + // 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 { + if inputAbi.Types[i].Name == typeName { + return &inputAbi.Types[i] + } + } + return nil +} diff --git a/abi/dynamic/reflect_marshal_test.go b/abi/dynamic/reflect_marshal_test.go new file mode 100644 index 0000000000..530b9f04ad --- /dev/null +++ b/abi/dynamic/reflect_marshal_test.go @@ -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 +} diff --git a/abi/testdata/transfer.json b/abi/testdata/transfer.json index c099bc7d5e..e350cf98ab 100644 --- a/abi/testdata/transfer.json +++ b/abi/testdata/transfer.json @@ -1,5 +1,5 @@ { - "to": "0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000", + "to": "0x0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000", "value": 1000, "memo": "aGk=" } diff --git a/abi/testdata/transferField.json b/abi/testdata/transferField.json index 549b53fbd6..fb5833315f 100644 --- a/abi/testdata/transferField.json +++ b/abi/testdata/transferField.json @@ -1,6 +1,6 @@ { "transfer": { - "to": "0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000", + "to": "0x0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000", "value": 1000, "memo": "aGk=" } diff --git a/abi/testdata/transfersArray.json b/abi/testdata/transfersArray.json index d4b3989b33..7ce4ab7ccf 100644 --- a/abi/testdata/transfersArray.json +++ b/abi/testdata/transfersArray.json @@ -1,12 +1,12 @@ { "transfers": [ { - "to": "0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000", + "to": "0x0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000", "value": 1000, "memo": "aGk=" }, { - "to": "0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000", + "to": "0x0102030405060708090a0b0c0d0e0f101112131400000000000000000000000000", "value": 1000, "memo": "aGk=" } From 09a83676e5d18b759519ee3f1567da6cce08dd41 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Fri, 27 Sep 2024 04:22:27 +0000 Subject: [PATCH 02/53] go mod tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index b0f7e5ac8c..837948775e 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 From ee78c67fee2f01da7514b777a57558a2b26e0267 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Mon, 30 Sep 2024 04:45:46 +0000 Subject: [PATCH 03/53] nits by ARR4N --- abi/dynamic/reflect_marshal.go | 140 +++++++++++++++------------- abi/dynamic/reflect_marshal_test.go | 8 +- 2 files changed, 77 insertions(+), 71 deletions(-) diff --git a/abi/dynamic/reflect_marshal.go b/abi/dynamic/reflect_marshal.go index 634652b4b4..0df3dff891 100644 --- a/abi/dynamic/reflect_marshal.go +++ b/abi/dynamic/reflect_marshal.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "reflect" + "regexp" "strconv" "strings" @@ -18,58 +19,60 @@ import ( "github.com/ava-labs/hypersdk/consts" ) -func DynamicMarshal(inputAbi abi.ABI, typeName string, jsonData string) ([]byte, error) { - // Find the type in the ABI - abiType := findABIType(inputAbi, typeName) - if abiType == nil { +var ( + // Matches fixed-size arrays like [32]uint8 + fixedSizeArrayRegex = regexp.MustCompile(`^\[(\d+)\](.+)$`) +) + +func Marshal(inputABI abi.ABI, typeName string, jsonData string) ([]byte, error) { + _, ok := findABIType(inputABI, typeName) + if !ok { return nil, 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) + typ, err := getReflectType(typeName, inputABI, typeCache) + if err != nil { + return nil, fmt.Errorf("failed to get reflect type: %w", err) + } - // Create an instance of the dynamic struct - dynamicValue := reflect.New(dynamicType).Interface() + value := reflect.New(typ).Interface() - // Unmarshal JSON data into the dynamic struct - if err := json.Unmarshal([]byte(jsonData), dynamicValue); err != nil { + err = json.Unmarshal([]byte(jsonData), value) + if 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 { + err = codec.LinearCodec.MarshalInto(value, writer.Packer) + if 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) { - // Find the type in the ABI - abiType := findABIType(inputAbi, typeName) - if abiType == nil { +func Unmarshal(inputABI abi.ABI, typeName string, data []byte) (string, error) { + _, ok := findABIType(inputABI, typeName) + if !ok { 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) + dynamicType, err := getReflectType(typeName, inputABI, typeCache) + if err != nil { + return "", fmt.Errorf("failed to get reflect type: %w", err) + } - // 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 { + err = codec.LinearCodec.Unmarshal(data, dynamicValue) + if 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) @@ -78,85 +81,90 @@ func DynamicUnmarshal(inputAbi abi.ABI, typeName string, data []byte) (string, e return string(jsonData), nil } -func getReflectType(abiTypeName string, inputAbi abi.ABI, typeCache map[string]reflect.Type) reflect.Type { +func getReflectType(abiTypeName string, inputABI abi.ABI, typeCache map[string]reflect.Type) (reflect.Type, error) { switch abiTypeName { case "string": - return reflect.TypeOf("") + return reflect.TypeOf(""), nil case "uint8": - return reflect.TypeOf(uint8(0)) + return reflect.TypeOf(uint8(0)), nil case "uint16": - return reflect.TypeOf(uint16(0)) + return reflect.TypeOf(uint16(0)), nil case "uint32": - return reflect.TypeOf(uint32(0)) + return reflect.TypeOf(uint32(0)), nil case "uint64": - return reflect.TypeOf(uint64(0)) + return reflect.TypeOf(uint64(0)), nil case "int8": - return reflect.TypeOf(int8(0)) + return reflect.TypeOf(int8(0)), nil case "int16": - return reflect.TypeOf(int16(0)) + return reflect.TypeOf(int16(0)), nil case "int32": - return reflect.TypeOf(int32(0)) + return reflect.TypeOf(int32(0)), nil case "int64": - return reflect.TypeOf(int64(0)) + return reflect.TypeOf(int64(0)), nil case "Address": - return reflect.TypeOf(codec.Address{}) + return reflect.TypeOf(codec.Address{}), nil default: + //golang slices if strings.HasPrefix(abiTypeName, "[]") { - elemType := getReflectType(strings.TrimPrefix(abiTypeName, "[]"), inputAbi, typeCache) - return reflect.SliceOf(elemType) - } else if strings.HasPrefix(abiTypeName, "[") { - // Handle fixed-size arrays - - sizeStr := strings.Split(abiTypeName, "]")[0] - sizeStr = strings.TrimPrefix(sizeStr, "[") + elemType, err := getReflectType(strings.TrimPrefix(abiTypeName, "[]"), inputABI, typeCache) + if err != nil { + return nil, err + } + return reflect.SliceOf(elemType), nil + } + //golang arrays + match := fixedSizeArrayRegex.FindStringSubmatch(abiTypeName) //`^\[(\d+)\](.+)$` + if match != nil { + sizeStr := match[1] size, err := strconv.Atoi(sizeStr) if err != nil { - return reflect.TypeOf((*interface{})(nil)).Elem() + return nil, fmt.Errorf("failed to convert size to int: %w", err) } - elemType := getReflectType(strings.TrimPrefix(abiTypeName, "["+sizeStr+"]"), inputAbi, typeCache) - return reflect.ArrayOf(size, elemType) + 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 - // Check if type already in cache - if cachedType, ok := typeCache[abiTypeName]; ok { - return cachedType + // For custom types, recursively construct the struct type + cachedType, ok := typeCache[abiTypeName] + if ok { + return cachedType, nil } - // Find the type in the ABI - abiType := findABIType(inputAbi, abiTypeName) - if abiType == nil { - // If not found, fallback to interface{} - return reflect.TypeOf((*interface{})(nil)).Elem() + abiType, ok := findABIType(inputABI, abiTypeName) + if !ok { + return nil, fmt.Errorf("type %s not found in ABI", abiTypeName) } - // Build fields + // 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 := getReflectType(field.Type, inputAbi, typeCache) + 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)), } } - // Create struct type - structType := reflect.StructOf(fields) - // Cache the type + structType := reflect.StructOf(fields) typeCache[abiTypeName] = structType - return structType + return structType, nil } } -// Helper function to find ABI type -func findABIType(inputAbi abi.ABI, typeName string) *abi.Type { - for i := range inputAbi.Types { - if inputAbi.Types[i].Name == typeName { - return &inputAbi.Types[i] +func findABIType(inputABI abi.ABI, typeName string) (abi.Type, bool) { + for _, typ := range inputABI.Types { + if typ.Name == typeName { + return typ, true } } - return nil + return abi.Type{}, false } diff --git a/abi/dynamic/reflect_marshal_test.go b/abi/dynamic/reflect_marshal_test.go index 530b9f04ad..252d4594a3 100644 --- a/abi/dynamic/reflect_marshal_test.go +++ b/abi/dynamic/reflect_marshal_test.go @@ -18,9 +18,9 @@ import ( 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) @@ -48,8 +48,7 @@ func TestDynamicMarshal(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)) + objectBytes, err := Marshal(abi, tc.typeName, string(jsonData)) require.NoError(err) // Compare with expected hex @@ -57,8 +56,7 @@ func TestDynamicMarshal(t *testing.T) { expectedHex = strings.TrimSpace(expectedHex) require.Equal(expectedHex, hex.EncodeToString(objectBytes)) - // Use DynamicUnmarshal to unmarshal the data - unmarshaledJSON, err := DynamicUnmarshal(abi, tc.typeName, objectBytes) + unmarshaledJSON, err := Unmarshal(abi, tc.typeName, objectBytes) require.NoError(err) // Compare with expected JSON From f6a144a4ab210b892b17d2a3b245393f001359ef Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Mon, 30 Sep 2024 04:46:44 +0000 Subject: [PATCH 04/53] lint --- abi/dynamic/reflect_marshal.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/abi/dynamic/reflect_marshal.go b/abi/dynamic/reflect_marshal.go index 0df3dff891..63af35206a 100644 --- a/abi/dynamic/reflect_marshal.go +++ b/abi/dynamic/reflect_marshal.go @@ -19,10 +19,8 @@ import ( "github.com/ava-labs/hypersdk/consts" ) -var ( - // Matches fixed-size arrays like [32]uint8 - fixedSizeArrayRegex = regexp.MustCompile(`^\[(\d+)\](.+)$`) -) +// Matches fixed-size arrays like [32]uint8 +var fixedSizeArrayRegex = regexp.MustCompile(`^\[(\d+)\](.+)$`) func Marshal(inputABI abi.ABI, typeName string, jsonData string) ([]byte, error) { _, ok := findABIType(inputABI, typeName) @@ -104,7 +102,7 @@ func getReflectType(abiTypeName string, inputABI abi.ABI, typeCache map[string]r case "Address": return reflect.TypeOf(codec.Address{}), nil default: - //golang slices + // golang slices if strings.HasPrefix(abiTypeName, "[]") { elemType, err := getReflectType(strings.TrimPrefix(abiTypeName, "[]"), inputABI, typeCache) if err != nil { @@ -113,8 +111,8 @@ func getReflectType(abiTypeName string, inputABI abi.ABI, typeCache map[string]r return reflect.SliceOf(elemType), nil } - //golang arrays - match := fixedSizeArrayRegex.FindStringSubmatch(abiTypeName) //`^\[(\d+)\](.+)$` + // golang arrays + match := fixedSizeArrayRegex.FindStringSubmatch(abiTypeName) // ^\[(\d+)\](.+)$ if match != nil { sizeStr := match[1] size, err := strconv.Atoi(sizeStr) From 15153e2ca6d2bf5e326a457afcc0b0cb721247bc Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Tue, 1 Oct 2024 02:15:14 +0000 Subject: [PATCH 05/53] some error tests --- abi/dynamic/reflect_marshal_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/abi/dynamic/reflect_marshal_test.go b/abi/dynamic/reflect_marshal_test.go index 252d4594a3..61299340fe 100644 --- a/abi/dynamic/reflect_marshal_test.go +++ b/abi/dynamic/reflect_marshal_test.go @@ -64,6 +64,29 @@ func TestDynamicMarshal(t *testing.T) { }) } } +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) + + t.Run("malformed JSON", func(t *testing.T) { + malformedJSON := `{"uint8": 42, "uint16": 1000, "uint32": 100000, "uint64": 10000000000, "int8": -42, "int16": -1000, "int32": -100000, "int64": -10000000000,` + _, err := Marshal(abi, "MockObjectAllNumbers", malformedJSON) + require.Error(err) + require.Contains(err.Error(), "unexpected end of JSON input") + }) + + t.Run("wrong struct name", func(t *testing.T) { + jsonData := mustReadFile(t, "../testdata/numbers.json") + _, err := Marshal(abi, "NonExistentObject", string(jsonData)) + require.Error(err) + require.Contains(err.Error(), "type NonExistentObject not found in ABI") + }) +} func mustReadFile(t *testing.T, path string) []byte { t.Helper() From e70a7e09e3aa3f77f2e285ea916dfd95590c6b6b Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:10:42 +0000 Subject: [PATCH 06/53] tidy --- go.mod | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9ba5de889b..0072fe61c1 100644 --- a/go.mod +++ b/go.mod @@ -26,8 +26,8 @@ require ( go.uber.org/zap v1.26.0 golang.org/x/crypto v0.22.0 golang.org/x/exp v0.0.0-20231127185646-65229373498e - golang.org/x/sync v0.7.0 - golang.org/x/text v0.14.0 + golang.org/x/sync v0.7.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 @@ -144,7 +144,6 @@ require ( golang.org/x/net v0.24.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/term v0.19.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 From b67fbaea4e1f56fddb78c509c760ff3885b9840a Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:12:56 +0000 Subject: [PATCH 07/53] replace codec.LinearCodec.Unmarshal for UnmarshalFrom --- abi/dynamic/reflect_marshal.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/abi/dynamic/reflect_marshal.go b/abi/dynamic/reflect_marshal.go index 63af35206a..b5c79d930f 100644 --- a/abi/dynamic/reflect_marshal.go +++ b/abi/dynamic/reflect_marshal.go @@ -14,6 +14,7 @@ import ( "golang.org/x/text/cases" "golang.org/x/text/language" + "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/hypersdk/abi" "github.com/ava-labs/hypersdk/codec" "github.com/ava-labs/hypersdk/consts" @@ -66,7 +67,11 @@ func Unmarshal(inputABI abi.ABI, typeName string, data []byte) (string, error) { dynamicValue := reflect.New(dynamicType).Interface() - err = codec.LinearCodec.Unmarshal(data, dynamicValue) + packer := wrappers.Packer{ + Bytes: data, + MaxSize: consts.NetworkSizeLimit, + } + err = codec.LinearCodec.UnmarshalFrom(&packer, dynamicValue) if err != nil { return "", fmt.Errorf("failed to unmarshal data: %w", err) } From ddb5e6fe80a1d45d3fb72ce05291d55aefee8fe1 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:13:44 +0000 Subject: [PATCH 08/53] format --- abi/dynamic/reflect_marshal.go | 2 +- abi/dynamic/reflect_marshal_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/abi/dynamic/reflect_marshal.go b/abi/dynamic/reflect_marshal.go index b5c79d930f..6d6acf2e63 100644 --- a/abi/dynamic/reflect_marshal.go +++ b/abi/dynamic/reflect_marshal.go @@ -11,10 +11,10 @@ import ( "strconv" "strings" + "github.com/ava-labs/avalanchego/utils/wrappers" "golang.org/x/text/cases" "golang.org/x/text/language" - "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/hypersdk/abi" "github.com/ava-labs/hypersdk/codec" "github.com/ava-labs/hypersdk/consts" diff --git a/abi/dynamic/reflect_marshal_test.go b/abi/dynamic/reflect_marshal_test.go index 61299340fe..91fce1de5d 100644 --- a/abi/dynamic/reflect_marshal_test.go +++ b/abi/dynamic/reflect_marshal_test.go @@ -64,6 +64,7 @@ func TestDynamicMarshal(t *testing.T) { }) } } + func TestDynamicMarshalErrors(t *testing.T) { require := require.New(t) From cb969a918723f3e476a38b1b899f3e90ac5c436c Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 10 Oct 2024 02:24:11 +0000 Subject: [PATCH 09/53] nits --- abi/dynamic/reflect_marshal.go | 39 +++++++++++++---------------- abi/dynamic/reflect_marshal_test.go | 22 +++++++--------- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/abi/dynamic/reflect_marshal.go b/abi/dynamic/reflect_marshal.go index 6d6acf2e63..90c07a1b3f 100644 --- a/abi/dynamic/reflect_marshal.go +++ b/abi/dynamic/reflect_marshal.go @@ -5,6 +5,7 @@ package dynamic import ( "encoding/json" + "errors" "fmt" "reflect" "regexp" @@ -20,13 +21,11 @@ import ( "github.com/ava-labs/hypersdk/consts" ) -// Matches fixed-size arrays like [32]uint8 -var fixedSizeArrayRegex = regexp.MustCompile(`^\[(\d+)\](.+)$`) +var ErrTypeNotFound = errors.New("type not found in ABI") func Marshal(inputABI abi.ABI, typeName string, jsonData string) ([]byte, error) { - _, ok := findABIType(inputABI, typeName) - if !ok { - return nil, fmt.Errorf("type %s not found in ABI", typeName) + if _, ok := findABIType(inputABI, typeName); !ok { + return nil, fmt.Errorf("marshalling %s: %w", typeName, ErrTypeNotFound) } typeCache := make(map[string]reflect.Type) @@ -38,14 +37,12 @@ func Marshal(inputABI abi.ABI, typeName string, jsonData string) ([]byte, error) value := reflect.New(typ).Interface() - err = json.Unmarshal([]byte(jsonData), value) - if err != nil { + 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) - err = codec.LinearCodec.MarshalInto(value, writer.Packer) - if err != nil { + if err := codec.LinearCodec.MarshalInto(value, writer.Packer); err != nil { return nil, fmt.Errorf("failed to marshal struct: %w", err) } @@ -53,30 +50,28 @@ func Marshal(inputABI abi.ABI, typeName string, jsonData string) ([]byte, error) } func Unmarshal(inputABI abi.ABI, typeName string, data []byte) (string, error) { - _, ok := findABIType(inputABI, typeName) - if !ok { - return "", fmt.Errorf("type %s not found in ABI", typeName) + if _, ok := findABIType(inputABI, typeName); !ok { + return "", fmt.Errorf("unmarshalling %s: %w", typeName, ErrTypeNotFound) } typeCache := make(map[string]reflect.Type) - dynamicType, err := getReflectType(typeName, inputABI, typeCache) + typ, err := getReflectType(typeName, inputABI, typeCache) if err != nil { return "", fmt.Errorf("failed to get reflect type: %w", err) } - dynamicValue := reflect.New(dynamicType).Interface() + value := reflect.New(typ).Interface() packer := wrappers.Packer{ Bytes: data, MaxSize: consts.NetworkSizeLimit, } - err = codec.LinearCodec.UnmarshalFrom(&packer, dynamicValue) - if err != nil { + if err := codec.LinearCodec.UnmarshalFrom(&packer, value); err != nil { return "", fmt.Errorf("failed to unmarshal data: %w", err) } - jsonData, err := json.Marshal(dynamicValue) + jsonData, err := json.Marshal(value) if err != nil { return "", fmt.Errorf("failed to marshal struct to JSON: %w", err) } @@ -84,6 +79,9 @@ func Unmarshal(inputABI abi.ABI, typeName string, data []byte) (string, error) { 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": @@ -117,8 +115,8 @@ func getReflectType(abiTypeName string, inputABI abi.ABI, typeCache map[string]r } // golang arrays - match := fixedSizeArrayRegex.FindStringSubmatch(abiTypeName) // ^\[(\d+)\](.+)$ - if match != nil { + + if match := fixedSizeArrayRegex.FindStringSubmatch(abiTypeName); match != nil { sizeStr := match[1] size, err := strconv.Atoi(sizeStr) if err != nil { @@ -132,8 +130,7 @@ func getReflectType(abiTypeName string, inputABI abi.ABI, typeCache map[string]r } // For custom types, recursively construct the struct type - cachedType, ok := typeCache[abiTypeName] - if ok { + if cachedType, ok := typeCache[abiTypeName]; ok { return cachedType, nil } diff --git a/abi/dynamic/reflect_marshal_test.go b/abi/dynamic/reflect_marshal_test.go index 91fce1de5d..da10409f79 100644 --- a/abi/dynamic/reflect_marshal_test.go +++ b/abi/dynamic/reflect_marshal_test.go @@ -74,19 +74,15 @@ func TestDynamicMarshalErrors(t *testing.T) { err := json.Unmarshal(abiJSON, &abi) require.NoError(err) - t.Run("malformed JSON", func(t *testing.T) { - malformedJSON := `{"uint8": 42, "uint16": 1000, "uint32": 100000, "uint64": 10000000000, "int8": -42, "int16": -1000, "int32": -100000, "int64": -10000000000,` - _, err := Marshal(abi, "MockObjectAllNumbers", malformedJSON) - require.Error(err) - require.Contains(err.Error(), "unexpected end of JSON input") - }) - - t.Run("wrong struct name", func(t *testing.T) { - jsonData := mustReadFile(t, "../testdata/numbers.json") - _, err := Marshal(abi, "NonExistentObject", string(jsonData)) - require.Error(err) - require.Contains(err.Error(), "type NonExistentObject not found in ABI") - }) + // 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(), "failed to unmarshal JSON data") + + // 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 { From d81655d94f01e95e9ced97a239ea2536fdb83906 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 10 Oct 2024 13:24:24 +0000 Subject: [PATCH 10/53] fix error name --- abi/dynamic/reflect_marshal_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/abi/dynamic/reflect_marshal_test.go b/abi/dynamic/reflect_marshal_test.go index da10409f79..23bc296b9a 100644 --- a/abi/dynamic/reflect_marshal_test.go +++ b/abi/dynamic/reflect_marshal_test.go @@ -77,7 +77,7 @@ func TestDynamicMarshalErrors(t *testing.T) { // 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(), "failed to unmarshal JSON data") + require.Contains(err.Error(), "unexpected end of JSON input") // Test wrong struct name jsonData := mustReadFile(t, "../testdata/numbers.json") From 11dd3211bcc765f174738234a5d0baee9284998b Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Mon, 14 Oct 2024 05:32:46 +0000 Subject: [PATCH 11/53] working with addresses --- cmd/cli/README.md | 23 ++++++++ cmd/cli/cli.go | 35 +++++++++++ cmd/cli/config.go | 131 +++++++++++++++++++++++++++++++++++++++++ cmd/cli/key.go | 22 +++++++ cmd/cli/key_address.go | 46 +++++++++++++++ cmd/cli/key_set.go | 86 +++++++++++++++++++++++++++ 6 files changed, 343 insertions(+) create mode 100644 cmd/cli/README.md create mode 100644 cmd/cli/cli.go create mode 100644 cmd/cli/config.go create mode 100644 cmd/cli/key.go create mode 100644 cmd/cli/key_address.go create mode 100644 cmd/cli/key_set.go diff --git a/cmd/cli/README.md b/cmd/cli/README.md new file mode 100644 index 0000000000..8b88262b64 --- /dev/null +++ b/cmd/cli/README.md @@ -0,0 +1,23 @@ +We need a CLI to do read only and write actions in transactions. + +Requirements: +- JSON/human-readable output. You can add `-o json` or `-o human` to any request +- action payload through interactive UI and through a one line tx, meaning any action argument, that's missing would be asked interactively +- Only flat actons are supported, no arrays, slices embeded structs, maps and struct fields. +- Keeps the private key and the endpoint in the user's home folder in `~/.hypersdk-cli/config.cfg` +- Argument `--endpoint` added to any request, overrides the request endpoint. If `--endpoint` is not set and `~/.hypersdk-cli/config.cfg` doesn't have endpoint field, errors +- Supports ed25519 keys only (enforce default, this is a default opinionated CLI) + +API: +`key address` - prints current key address +`key generate` - generates a new address +`key balance` - balance +`key set` set private key from file, base64 string or hex string presented in the first argument +`endpoint` - prints URL of the current endpoint +`endpoint ping` - checks connectivity with the current endpoing +`endpoint set --endpoint=https://hello.world:1234` sets endpoint +`actions` - print the list of actions available in the ABI +`tx send [action name] [action params]` - sends a transaction with a single action +`tx read [action name] [action params]` - simulates a single action transaction +`abi` - prints current ABI (list of actions and data types) + diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go new file mode 100644 index 0000000000..4a19d53c96 --- /dev/null +++ b/cmd/cli/cli.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "hypersdk-cli", + Short: "HyperSDK CLI for interacting with HyperSDK-based chains", + Long: `A CLI application for performing read and write actions on HyperSDK-based chains.`, +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + os.Exit(0) +} + +func init() { + rootCmd.PersistentFlags().StringP("output", "o", "human", "Output format (human or json)") + rootCmd.PersistentFlags().String("endpoint", "", "Override the default endpoint") + rootCmd.PersistentFlags().String("key", "", "Private ED25519 key as hex string") + + // Add key commands + rootCmd.AddCommand(keyCmd) +} + +func main() { + Execute() +} diff --git a/cmd/cli/config.go b/cmd/cli/config.go new file mode 100644 index 0000000000..967aa55c30 --- /dev/null +++ b/cmd/cli/config.go @@ -0,0 +1,131 @@ +package main + +import ( + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" +) + +func printValue(v fmt.Stringer, cmd *cobra.Command) error { + output, err := getConfigValue(cmd, "output") + if err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("failed to get output format: %w", err) + } + + if output == "json" { + jsonBytes, err := json.MarshalIndent(v, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + return nil + } else if output == "human" { + fmt.Println(v.String()) + return nil + } else { + return fmt.Errorf("invalid output format: %s", output) + } +} + +func getConfigValue(cmd *cobra.Command, name string) (string, error) { + // Check if the value is among flags + if value, err := cmd.Flags().GetString(name); err == nil && value != "" { + return value, nil + } + + // If not in flags, check the config file + config, err := readConfig() + if err != nil { + return "", fmt.Errorf("failed to read config: %w", err) + } + + if value, ok := config[name]; ok { + return value, nil + } + + return "", fmt.Errorf("value for %s not found", name) +} + +func updateConfig(name, value string) error { + config, err := readConfig() + if err != nil { + return fmt.Errorf("failed to read config: %w", err) + } + + config[name] = value + return writeConfig(config) +} + +func readConfig() (map[string]string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get home directory: %w", err) + } + + configPath := filepath.Join(homeDir, ".hypersdk-cli", "config.cfg") + data, err := os.ReadFile(configPath) + if err != nil { + if os.IsNotExist(err) { + return make(map[string]string), nil + } + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + config := make(map[string]string) + lines := strings.Split(string(data), "\n") + for _, line := range lines { + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + config[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + } + } + + return config, nil +} + +func writeConfig(config map[string]string) error { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %w", err) + } + + configDir := filepath.Join(homeDir, ".hypersdk-cli") + if err := os.MkdirAll(configDir, 0755); err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + + configPath := filepath.Join(configDir, "config.cfg") + var buf strings.Builder + for key, value := range config { + buf.WriteString(fmt.Sprintf("%s = %s\n", key, value)) + } + + if err := os.WriteFile(configPath, []byte(buf.String()), 0644); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + return nil +} + +func decodeWhatever(whatever string) ([]byte, error) { + if decoded, err := hex.DecodeString(whatever); err == nil { + return decoded, nil + } + + if decoded, err := base64.StdEncoding.DecodeString(whatever); err == nil { + return decoded, nil + } + + if fileContents, err := os.ReadFile(whatever); err == nil { + return fileContents, nil + } + + return nil, fmt.Errorf("unable to decode input as base64, hex, or read as file path") +} diff --git a/cmd/cli/key.go b/cmd/cli/key.go new file mode 100644 index 0000000000..3b0c3ba7f4 --- /dev/null +++ b/cmd/cli/key.go @@ -0,0 +1,22 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +var keyCmd = &cobra.Command{ + Use: "key", + Short: "Manage keys", +} + +var keyBalanceCmd = &cobra.Command{ + Use: "balance", + Short: "Print current key balance", + RunE: func(cmd *cobra.Command, args []string) error { + return nil + }, +} + +func init() { + keyCmd.AddCommand(keyAddressCmd, keyBalanceCmd) +} diff --git a/cmd/cli/key_address.go b/cmd/cli/key_address.go new file mode 100644 index 0000000000..817f26b3cb --- /dev/null +++ b/cmd/cli/key_address.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + + "github.com/ava-labs/hypersdk/auth" + "github.com/spf13/cobra" +) + +var keyAddressCmd = &cobra.Command{ + Use: "address", + Short: "Print current key address", + RunE: func(cmd *cobra.Command, args []string) error { + keyString, err := getConfigValue(cmd, "key") + if err != nil { + return fmt.Errorf("failed to get key: %w", err) + } + + key, err := privateKeyFromString(keyString) + if err != nil { + return fmt.Errorf("failed to decode key: %w", err) + } + + addr := auth.NewED25519Address(key.PublicKey()) + addrString, err := addr.MarshalText() + if err != nil { + return fmt.Errorf("failed to marshal address: %w", err) + } + + return printValue(keyAddressCmdResponse{ + Address: string(addrString), + }, cmd) + }, +} + +type keyAddressCmdResponse struct { + Address string `json:"address"` +} + +func (r keyAddressCmdResponse) String() string { + return r.Address +} + +func init() { + keyCmd.AddCommand(keySetCmd, keyGenerateCmd) +} diff --git a/cmd/cli/key_set.go b/cmd/cli/key_set.go new file mode 100644 index 0000000000..5d5a3dd357 --- /dev/null +++ b/cmd/cli/key_set.go @@ -0,0 +1,86 @@ +package main + +import ( + "encoding/hex" + "fmt" + + "github.com/ava-labs/hypersdk/auth" + "github.com/ava-labs/hypersdk/crypto/ed25519" + "github.com/spf13/cobra" +) + +var keyGenerateCmd = &cobra.Command{ + Use: "generate", + Short: "Generate a new key", + RunE: func(cmd *cobra.Command, args []string) error { + newKey, err := ed25519.GeneratePrivateKey() + if err != nil { + return fmt.Errorf("failed to generate key: %w", err) + } + + return checkAndSavePrivateKey(hex.EncodeToString(newKey[:]), cmd) + }, +} + +var keySetCmd = &cobra.Command{ + Use: "set", + Short: "Set the private ED25519 key", + RunE: func(cmd *cobra.Command, args []string) error { + //read directly from the flag instead of calling getConfigValue + keyString, err := cmd.Flags().GetString("key") + if err != nil { + return fmt.Errorf("failed to get key: %w", err) + } + if keyString == "" { + return fmt.Errorf("--key is required") + } + + return checkAndSavePrivateKey(keyString, cmd) + }, +} + +func checkAndSavePrivateKey(keyString string, cmd *cobra.Command) error { + key, err := privateKeyFromString(keyString) + if err != nil { + return fmt.Errorf("failed to decode key: %w", err) + } + + err = updateConfig("key", hex.EncodeToString(key[:])) + if err != nil { + return fmt.Errorf("failed to update config: %w", err) + } + + addr := auth.NewED25519Address(key.PublicKey()) + + addrString, err := addr.MarshalText() + if err != nil { + return fmt.Errorf("failed to marshal address: %w", err) + } + + return printValue(keySetCmdResponse{ + Address: string(addrString), + }, cmd) +} + +func privateKeyFromString(keyStr string) (ed25519.PrivateKey, error) { + keyBytes, err := hex.DecodeString(keyStr) + if err != nil { + return ed25519.EmptyPrivateKey, fmt.Errorf("failed to decode key: %w", err) + } + if len(keyBytes) != ed25519.PrivateKeyLen { + return ed25519.EmptyPrivateKey, fmt.Errorf("invalid private key length: %d", len(keyBytes)) + } + return ed25519.PrivateKey(keyBytes), nil +} + +type keySetCmdResponse struct { + Address string `json:"address"` +} + +func (r keySetCmdResponse) String() string { + return fmt.Sprintf("✅ Key added successfully!\nAddress: %s", r.Address) +} + +func init() { + keyCmd.AddCommand(keySetCmd, keyGenerateCmd) +} From 1c8581a4a7e9c6aaa24484b370277e116ace1be2 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Mon, 14 Oct 2024 05:54:31 +0000 Subject: [PATCH 12/53] ping --- cmd/cli/README.md | 10 +++--- cmd/cli/{key_address.go => address.go} | 8 ++--- cmd/cli/cli.go | 3 -- cmd/cli/config.go | 2 +- cmd/cli/endpoint.go | 12 +++++++ cmd/cli/endpoint_set.go | 49 ++++++++++++++++++++++++++ cmd/cli/key.go | 10 +----- cmd/cli/key_set.go | 6 ++-- cmd/cli/ping.go | 44 +++++++++++++++++++++++ 9 files changed, 119 insertions(+), 25 deletions(-) rename cmd/cli/{key_address.go => address.go} (85%) create mode 100644 cmd/cli/endpoint.go create mode 100644 cmd/cli/endpoint_set.go create mode 100644 cmd/cli/ping.go diff --git a/cmd/cli/README.md b/cmd/cli/README.md index 8b88262b64..f548f8ef6c 100644 --- a/cmd/cli/README.md +++ b/cmd/cli/README.md @@ -9,12 +9,12 @@ Requirements: - Supports ed25519 keys only (enforce default, this is a default opinionated CLI) API: -`key address` - prints current key address -`key generate` - generates a new address -`key balance` - balance -`key set` set private key from file, base64 string or hex string presented in the first argument +✅ `address` - prints current key address +✅ `key generate` - generates a new address +`balance` - balance +✅ `key set` set private key from file, base64 string or hex string presented in the first argument `endpoint` - prints URL of the current endpoint -`endpoint ping` - checks connectivity with the current endpoing +`ping` - checks connectivity with the current endpoing `endpoint set --endpoint=https://hello.world:1234` sets endpoint `actions` - print the list of actions available in the ABI `tx send [action name] [action params]` - sends a transaction with a single action diff --git a/cmd/cli/key_address.go b/cmd/cli/address.go similarity index 85% rename from cmd/cli/key_address.go rename to cmd/cli/address.go index 817f26b3cb..1fac19b6a4 100644 --- a/cmd/cli/key_address.go +++ b/cmd/cli/address.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -var keyAddressCmd = &cobra.Command{ +var addressCmd = &cobra.Command{ Use: "address", Short: "Print current key address", RunE: func(cmd *cobra.Command, args []string) error { @@ -27,9 +27,9 @@ var keyAddressCmd = &cobra.Command{ return fmt.Errorf("failed to marshal address: %w", err) } - return printValue(keyAddressCmdResponse{ + return printValue(cmd, keyAddressCmdResponse{ Address: string(addrString), - }, cmd) + }) }, } @@ -42,5 +42,5 @@ func (r keyAddressCmdResponse) String() string { } func init() { - keyCmd.AddCommand(keySetCmd, keyGenerateCmd) + rootCmd.AddCommand(addressCmd) } diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index 4a19d53c96..71bbf5f8af 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -25,9 +25,6 @@ func init() { rootCmd.PersistentFlags().StringP("output", "o", "human", "Output format (human or json)") rootCmd.PersistentFlags().String("endpoint", "", "Override the default endpoint") rootCmd.PersistentFlags().String("key", "", "Private ED25519 key as hex string") - - // Add key commands - rootCmd.AddCommand(keyCmd) } func main() { diff --git a/cmd/cli/config.go b/cmd/cli/config.go index 967aa55c30..2a45c0d4f8 100644 --- a/cmd/cli/config.go +++ b/cmd/cli/config.go @@ -13,7 +13,7 @@ import ( "github.com/spf13/cobra" ) -func printValue(v fmt.Stringer, cmd *cobra.Command) error { +func printValue(cmd *cobra.Command, v fmt.Stringer) error { output, err := getConfigValue(cmd, "output") if err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("failed to get output format: %w", err) diff --git a/cmd/cli/endpoint.go b/cmd/cli/endpoint.go new file mode 100644 index 0000000000..413ddd0b6f --- /dev/null +++ b/cmd/cli/endpoint.go @@ -0,0 +1,12 @@ +package main + +import "github.com/spf13/cobra" + +var endpointCmd = &cobra.Command{ + Use: "endpoint", + Short: "Manage endpoint", +} + +func init() { + rootCmd.AddCommand(endpointCmd) +} diff --git a/cmd/cli/endpoint_set.go b/cmd/cli/endpoint_set.go new file mode 100644 index 0000000000..8b772aeeaf --- /dev/null +++ b/cmd/cli/endpoint_set.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var endpointSetCmd = &cobra.Command{ + Use: "set", + Short: "Set the endpoint URL", + RunE: func(cmd *cobra.Command, args []string) error { + endpoint, err := cmd.Flags().GetString("endpoint") + if err != nil { + return fmt.Errorf("failed to get endpoint flag: %w", err) + } + + if endpoint == "" { + return fmt.Errorf("endpoint is required") + } + + if err := updateConfig("endpoint", endpoint); err != nil { + return fmt.Errorf("failed to update config: %w", err) + } + + return printValue(cmd, endpointSetCmdResponse{ + Endpoint: endpoint, + }) + }, +} + +type endpointSetCmdResponse struct { + Endpoint string `json:"endpoint"` + PingSucceed bool `json:"ping"` +} + +func (r endpointSetCmdResponse) String() string { + pingStatus := "Ping failed" + if r.PingSucceed { + pingStatus = "Ping succeeded" + } + return fmt.Sprintf("Endpoint set to: %s\n%s", r.Endpoint, pingStatus) +} + +func init() { + endpointCmd.AddCommand(endpointSetCmd) + endpointSetCmd.Flags().String("endpoint", "", "Endpoint URL to set") + endpointSetCmd.MarkFlagRequired("endpoint") +} diff --git a/cmd/cli/key.go b/cmd/cli/key.go index 3b0c3ba7f4..964ac55808 100644 --- a/cmd/cli/key.go +++ b/cmd/cli/key.go @@ -9,14 +9,6 @@ var keyCmd = &cobra.Command{ Short: "Manage keys", } -var keyBalanceCmd = &cobra.Command{ - Use: "balance", - Short: "Print current key balance", - RunE: func(cmd *cobra.Command, args []string) error { - return nil - }, -} - func init() { - keyCmd.AddCommand(keyAddressCmd, keyBalanceCmd) + rootCmd.AddCommand(keyCmd) } diff --git a/cmd/cli/key_set.go b/cmd/cli/key_set.go index 5d5a3dd357..f3ab551216 100644 --- a/cmd/cli/key_set.go +++ b/cmd/cli/key_set.go @@ -57,13 +57,13 @@ func checkAndSavePrivateKey(keyString string, cmd *cobra.Command) error { return fmt.Errorf("failed to marshal address: %w", err) } - return printValue(keySetCmdResponse{ + return printValue(cmd, keySetCmdResponse{ Address: string(addrString), - }, cmd) + }) } func privateKeyFromString(keyStr string) (ed25519.PrivateKey, error) { - keyBytes, err := hex.DecodeString(keyStr) + keyBytes, err := decodeWhatever(keyStr) if err != nil { return ed25519.EmptyPrivateKey, fmt.Errorf("failed to decode key: %w", err) } diff --git a/cmd/cli/ping.go b/cmd/cli/ping.go new file mode 100644 index 0000000000..aa491f83e2 --- /dev/null +++ b/cmd/cli/ping.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "fmt" + + "github.com/ava-labs/hypersdk/api/jsonrpc" + "github.com/spf13/cobra" +) + +var endpointPingCmd = &cobra.Command{ + Use: "ping", + Short: "Ping the endpoint", + RunE: func(cmd *cobra.Command, args []string) error { + endpoint, err := getConfigValue(cmd, "endpoint") + if err != nil { + return fmt.Errorf("failed to get endpoint: %w", err) + } + client := jsonrpc.NewJSONRPCClient(endpoint) + + success, err := client.Ping(context.Background()) + if err != nil { + return fmt.Errorf("failed to ping: %w", err) + } + return printValue(cmd, pingResponse{ + Success: success, + }) + }, +} + +type pingResponse struct { + Success bool `json:"success"` +} + +func (r pingResponse) String() string { + if r.Success { + return "✅ Ping succeeded" + } + return "❌ Ping failed" +} + +func init() { + rootCmd.AddCommand(endpointPingCmd) +} From ab5d63f5607d95d7b6948102d85c3c1cd93b3a57 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Mon, 14 Oct 2024 06:01:41 +0000 Subject: [PATCH 13/53] reorganize ping --- cmd/cli/endpoint_set.go | 22 ++++++++++++++++------ cmd/cli/ping.go | 13 ++++++++----- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/cmd/cli/endpoint_set.go b/cmd/cli/endpoint_set.go index 8b772aeeaf..58168cb2be 100644 --- a/cmd/cli/endpoint_set.go +++ b/cmd/cli/endpoint_set.go @@ -1,8 +1,10 @@ package main import ( + "context" "fmt" + "github.com/ava-labs/hypersdk/api/jsonrpc" "github.com/spf13/cobra" ) @@ -23,22 +25,30 @@ var endpointSetCmd = &cobra.Command{ return fmt.Errorf("failed to update config: %w", err) } + client := jsonrpc.NewJSONRPCClient(endpoint) + success, err := client.Ping(context.Background()) + pingErr := "" + if err != nil { + pingErr = err.Error() + } + return printValue(cmd, endpointSetCmdResponse{ Endpoint: endpoint, + pingResponse: pingResponse{ + PingSucceed: success, + PingError: pingErr, + }, }) }, } type endpointSetCmdResponse struct { - Endpoint string `json:"endpoint"` - PingSucceed bool `json:"ping"` + pingResponse + Endpoint string `json:"endpoint"` } func (r endpointSetCmdResponse) String() string { - pingStatus := "Ping failed" - if r.PingSucceed { - pingStatus = "Ping succeeded" - } + pingStatus := r.pingResponse.String() return fmt.Sprintf("Endpoint set to: %s\n%s", r.Endpoint, pingStatus) } diff --git a/cmd/cli/ping.go b/cmd/cli/ping.go index aa491f83e2..86e1b1ae26 100644 --- a/cmd/cli/ping.go +++ b/cmd/cli/ping.go @@ -19,24 +19,27 @@ var endpointPingCmd = &cobra.Command{ client := jsonrpc.NewJSONRPCClient(endpoint) success, err := client.Ping(context.Background()) + pingErr := "" if err != nil { - return fmt.Errorf("failed to ping: %w", err) + pingErr = err.Error() } return printValue(cmd, pingResponse{ - Success: success, + PingSucceed: success, + PingError: pingErr, }) }, } type pingResponse struct { - Success bool `json:"success"` + PingSucceed bool `json:"ping_succeed"` + PingError string `json:"ping_error"` } func (r pingResponse) String() string { - if r.Success { + if r.PingSucceed { return "✅ Ping succeeded" } - return "❌ Ping failed" + return "❌ Ping failed with error: " + r.PingError } func init() { From cb01730b8a8740684e4a741e128cc7b44d7e4fe2 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Mon, 14 Oct 2024 06:15:15 +0000 Subject: [PATCH 14/53] actions --- cmd/cli/README.md | 12 +++---- cmd/cli/actions.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 cmd/cli/actions.go diff --git a/cmd/cli/README.md b/cmd/cli/README.md index f548f8ef6c..0b84f8f276 100644 --- a/cmd/cli/README.md +++ b/cmd/cli/README.md @@ -14,10 +14,8 @@ API: `balance` - balance ✅ `key set` set private key from file, base64 string or hex string presented in the first argument `endpoint` - prints URL of the current endpoint -`ping` - checks connectivity with the current endpoing -`endpoint set --endpoint=https://hello.world:1234` sets endpoint -`actions` - print the list of actions available in the ABI -`tx send [action name] [action params]` - sends a transaction with a single action -`tx read [action name] [action params]` - simulates a single action transaction -`abi` - prints current ABI (list of actions and data types) - +✅ `ping` - checks connectivity with the current endpoing +✅ `endpoint set --endpoint=https://hello.world:1234` sets endpoint +✅ `actions` - print the list of actions available in the ABI, for JSON it prints ABI JSON +`tx [action name] [action params]` - sends a transaction with a single action +`read [action name] [action params]` - simulates a single action transaction diff --git a/cmd/cli/actions.go b/cmd/cli/actions.go new file mode 100644 index 0000000000..3721b0ec56 --- /dev/null +++ b/cmd/cli/actions.go @@ -0,0 +1,90 @@ +package main + +import ( + "context" + "fmt" + + "github.com/ava-labs/hypersdk/abi" + "github.com/ava-labs/hypersdk/api/jsonrpc" + "github.com/spf13/cobra" +) + +var actionsCmd = &cobra.Command{ + Use: "actions", + Short: "Print the list of actions available in the ABI", + RunE: func(cmd *cobra.Command, args []string) error { + endpoint, err := getConfigValue(cmd, "endpoint") + if err != nil { + return fmt.Errorf("failed to get endpoint: %w", err) + } + client := jsonrpc.NewJSONRPCClient(endpoint) + + abi, err := client.GetABI(context.Background()) + if err != nil { + return fmt.Errorf("failed to get ABI: %w", err) + } + + return printValue(cmd, abiWrapper{ABI: abi}) + }, +} + +type abiWrapper struct { + ABI abi.ABI +} + +func (a abiWrapper) String() string { + result := "" + for _, action := range a.ABI.Actions { + result += fmt.Sprintf("---\n%s\n\n", action.Name) + typ, found := findTypeByName(a.ABI.Types, action.Name) + if !found { + result += fmt.Sprintf(" Error: Type not found for action %s\n", action.Name) + continue + } else { + result += "Inputs:\n" + for _, field := range typ.Fields { + result += fmt.Sprintf(" %s: %s\n", field.Name, field.Type) + } + } + + output, found := findOutputByID(a.ABI.Outputs, action.ID) + if !found { + result += fmt.Sprintf("No outputs for %s with id %d\n", action.Name, action.ID) + continue + } + + typ, found = findTypeByName(a.ABI.Types, output.Name) + if !found { + result += fmt.Sprintf(" Error: Type not found for output %s\n", output.Name) + continue + } + result += "\nOutputs:\n" + for _, field := range typ.Fields { + result += fmt.Sprintf(" %s: %s\n", field.Name, field.Type) + } + + } + return result +} + +func findOutputByID(outputs []abi.TypedStruct, id uint8) (abi.TypedStruct, bool) { + for _, output := range outputs { + if output.ID == id { + return output, true + } + } + return abi.TypedStruct{}, false +} + +func findTypeByName(types []abi.Type, name string) (abi.Type, bool) { + for _, typ := range types { + if typ.Name == name { + return typ, true + } + } + return abi.Type{}, false +} + +func init() { + rootCmd.AddCommand(actionsCmd) +} From bf3dc6458b60541f2cbc1d4a9e33f9a5c069f536 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Mon, 14 Oct 2024 06:18:30 +0000 Subject: [PATCH 15/53] endpoint command --- cmd/cli/README.md | 2 +- cmd/cli/endpoint.go | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/cmd/cli/README.md b/cmd/cli/README.md index 0b84f8f276..e6107a8391 100644 --- a/cmd/cli/README.md +++ b/cmd/cli/README.md @@ -13,7 +13,7 @@ API: ✅ `key generate` - generates a new address `balance` - balance ✅ `key set` set private key from file, base64 string or hex string presented in the first argument -`endpoint` - prints URL of the current endpoint +✅ `endpoint` - prints URL of the current endpoint ✅ `ping` - checks connectivity with the current endpoing ✅ `endpoint set --endpoint=https://hello.world:1234` sets endpoint ✅ `actions` - print the list of actions available in the ABI, for JSON it prints ABI JSON diff --git a/cmd/cli/endpoint.go b/cmd/cli/endpoint.go index 413ddd0b6f..0c5807cfa3 100644 --- a/cmd/cli/endpoint.go +++ b/cmd/cli/endpoint.go @@ -1,10 +1,31 @@ package main -import "github.com/spf13/cobra" +import ( + "fmt" + + "github.com/spf13/cobra" +) var endpointCmd = &cobra.Command{ Use: "endpoint", Short: "Manage endpoint", + RunE: func(cmd *cobra.Command, args []string) error { + endpoint, err := getConfigValue(cmd, "endpoint") + if err != nil { + return fmt.Errorf("failed to get endpoint: %w", err) + } + return printValue(cmd, endpointCmdResponse{ + Endpoint: endpoint, + }) + }, +} + +type endpointCmdResponse struct { + Endpoint string `json:"endpoint"` +} + +func (r endpointCmdResponse) String() string { + return r.Endpoint } func init() { From af035e86fe1fd426025ee166b92fd158d4d0b403 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Mon, 21 Oct 2024 06:17:25 +0000 Subject: [PATCH 16/53] read result --- abi/dynamic/reflect_marshal.go | 166 ----------------------- abi/dynamic/reflect_marshal_test.go | 196 +++++++++++++++++++++++++++- 2 files changed, 193 insertions(+), 169 deletions(-) diff --git a/abi/dynamic/reflect_marshal.go b/abi/dynamic/reflect_marshal.go index 90c07a1b3f..f364288875 100644 --- a/abi/dynamic/reflect_marshal.go +++ b/abi/dynamic/reflect_marshal.go @@ -2,169 +2,3 @@ // 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 -} diff --git a/abi/dynamic/reflect_marshal_test.go b/abi/dynamic/reflect_marshal_test.go index 23bc296b9a..6380a9a7c2 100644 --- a/abi/dynamic/reflect_marshal_test.go +++ b/abi/dynamic/reflect_marshal_test.go @@ -6,15 +6,203 @@ package dynamic import ( "encoding/hex" "encoding/json" + "errors" + "fmt" "os" + "reflect" + "regexp" + "strconv" "strings" "testing" "github.com/stretchr/testify/require" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "github.com/ava-labs/avalanchego/utils/wrappers" "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 := inputABI.FindTypeByName(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) + } + + var typeID byte + found := false + for _, action := range inputABI.Actions { + if action.Name == typeName { + typeID = byte(action.ID) + found = true + break + } + } + if !found { + return nil, fmt.Errorf("action %s not found in ABI", typeName) + } + + writer := codec.NewWriter(1, consts.NetworkSizeLimit) + writer.PackByte(typeID) + 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 UnmarshalOutput(inputABI abi.ABI, data []byte) (string, error) { + if len(data) == 0 { + return "", nil + } + + typeID := data[0] + outputType, ok := inputABI.FindOutputByID(typeID) + if !ok { + return "", fmt.Errorf("output with id %d not found in ABI", typeID) + } + + return Unmarshal(inputABI, data[1:], outputType.Name) +} + +func UnmarshalAction(inputABI abi.ABI, data []byte) (string, error) { + if len(data) == 0 { + return "", nil + } + + typeID := data[0] + actionType, ok := inputABI.FindActionByID(typeID) + if !ok { + return "", fmt.Errorf("action with id %d not found in ABI", typeID) + } + + return Unmarshal(inputABI, data[1:], actionType.Name) +} + +func Unmarshal(inputABI abi.ABI, data []byte, typeName string) (string, error) { + 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 := inputABI.FindTypeByName(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)) + fmt.Printf("\nabiTypeName=%s, abiType.Fields %v\n", abiTypeName, 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 TestDynamicMarshal(t *testing.T) { require := require.New(t) @@ -52,11 +240,13 @@ func TestDynamicMarshal(t *testing.T) { require.NoError(err) // Compare with expected hex - expectedHex := string(mustReadFile(t, "../testdata/"+tc.name+".hex")) - expectedHex = strings.TrimSpace(expectedHex) + expectedTypeID, found := abi.FindActionByName(tc.typeName) + require.True(found, "action %s not found in ABI", tc.typeName) + + expectedHex := hex.EncodeToString([]byte{expectedTypeID.ID}) + strings.TrimSpace(string(mustReadFile(t, "../testdata/"+tc.name+".hex"))) require.Equal(expectedHex, hex.EncodeToString(objectBytes)) - unmarshaledJSON, err := Unmarshal(abi, tc.typeName, objectBytes) + unmarshaledJSON, err := UnmarshalAction(abi, objectBytes) require.NoError(err) // Compare with expected JSON From f3f62bfffb4345be68b9c97a5c111942e57c9e34 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Mon, 21 Oct 2024 06:17:27 +0000 Subject: [PATCH 17/53] read result --- abi/abi.go | 45 ++++++ abi/dynamic/reflect_marshal.go | 195 +++++++++++++++++++++++++ abi/dynamic/reflect_marshal_test.go | 188 ------------------------ api/jsonrpc/client.go | 13 +- cmd/cli/actions.go | 24 +-- cmd/cli/read.go | 177 ++++++++++++++++++++++ examples/morpheusvm/storage/storage.go | 2 +- 7 files changed, 423 insertions(+), 221 deletions(-) create mode 100644 cmd/cli/read.go diff --git a/abi/abi.go b/abi/abi.go index d605aebacb..1004b9c812 100644 --- a/abi/abi.go +++ b/abi/abi.go @@ -200,3 +200,48 @@ func describeStruct(t reflect.Type) ([]Field, []reflect.Type, error) { return fields, otherStructsSeen, nil } + +func (a *ABI) FindOutputByID(id uint8) (TypedStruct, bool) { + for _, output := range a.Outputs { + if output.ID == id { + return output, true + } + } + return TypedStruct{}, false +} + +func (a *ABI) FindActionByID(id uint8) (TypedStruct, bool) { + for _, action := range a.Actions { + if action.ID == id { + return action, true + } + } + return TypedStruct{}, false +} + +func (a *ABI) FindOutputByName(name string) (TypedStruct, bool) { + for _, output := range a.Outputs { + if output.Name == name { + return output, true + } + } + return TypedStruct{}, false +} + +func (a *ABI) FindActionByName(name string) (TypedStruct, bool) { + for _, action := range a.Actions { + if action.Name == name { + return action, true + } + } + return TypedStruct{}, false +} + +func (a *ABI) FindTypeByName(name string) (Type, bool) { + for _, typ := range a.Types { + if typ.Name == name { + return typ, true + } + } + return Type{}, false +} diff --git a/abi/dynamic/reflect_marshal.go b/abi/dynamic/reflect_marshal.go index f364288875..b8c90e383c 100644 --- a/abi/dynamic/reflect_marshal.go +++ b/abi/dynamic/reflect_marshal.go @@ -2,3 +2,198 @@ // 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" + "github.com/ava-labs/hypersdk/abi" + "github.com/ava-labs/hypersdk/codec" + "github.com/ava-labs/hypersdk/consts" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +var ErrTypeNotFound = errors.New("type not found in ABI") + +func Marshal(inputABI abi.ABI, typeName string, jsonData string) ([]byte, error) { + if _, ok := inputABI.FindTypeByName(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) + } + + var typeID byte + found := false + for _, action := range inputABI.Actions { + if action.Name == typeName { + typeID = byte(action.ID) + found = true + break + } + } + if !found { + return nil, fmt.Errorf("action %s not found in ABI", typeName) + } + + writer := codec.NewWriter(1, consts.NetworkSizeLimit) + writer.PackByte(typeID) + 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 UnmarshalOutput(inputABI abi.ABI, data []byte) (string, error) { + if len(data) == 0 { + return "", nil + } + + typeID := data[0] + outputType, ok := inputABI.FindOutputByID(typeID) + if !ok { + return "", fmt.Errorf("output with id %d not found in ABI", typeID) + } + + return Unmarshal(inputABI, data[1:], outputType.Name) +} + +func UnmarshalAction(inputABI abi.ABI, data []byte) (string, error) { + if len(data) == 0 { + return "", nil + } + + typeID := data[0] + actionType, ok := inputABI.FindActionByID(typeID) + if !ok { + return "", fmt.Errorf("action with id %d not found in ABI", typeID) + } + + return Unmarshal(inputABI, data[1:], actionType.Name) +} + +func Unmarshal(inputABI abi.ABI, data []byte, typeName string) (string, error) { + 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 := inputABI.FindTypeByName(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 + } +} diff --git a/abi/dynamic/reflect_marshal_test.go b/abi/dynamic/reflect_marshal_test.go index 6380a9a7c2..1b0c41a65e 100644 --- a/abi/dynamic/reflect_marshal_test.go +++ b/abi/dynamic/reflect_marshal_test.go @@ -6,203 +6,15 @@ package dynamic import ( "encoding/hex" "encoding/json" - "errors" - "fmt" "os" - "reflect" - "regexp" - "strconv" "strings" "testing" "github.com/stretchr/testify/require" - "golang.org/x/text/cases" - "golang.org/x/text/language" - "github.com/ava-labs/avalanchego/utils/wrappers" "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 := inputABI.FindTypeByName(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) - } - - var typeID byte - found := false - for _, action := range inputABI.Actions { - if action.Name == typeName { - typeID = byte(action.ID) - found = true - break - } - } - if !found { - return nil, fmt.Errorf("action %s not found in ABI", typeName) - } - - writer := codec.NewWriter(1, consts.NetworkSizeLimit) - writer.PackByte(typeID) - 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 UnmarshalOutput(inputABI abi.ABI, data []byte) (string, error) { - if len(data) == 0 { - return "", nil - } - - typeID := data[0] - outputType, ok := inputABI.FindOutputByID(typeID) - if !ok { - return "", fmt.Errorf("output with id %d not found in ABI", typeID) - } - - return Unmarshal(inputABI, data[1:], outputType.Name) -} - -func UnmarshalAction(inputABI abi.ABI, data []byte) (string, error) { - if len(data) == 0 { - return "", nil - } - - typeID := data[0] - actionType, ok := inputABI.FindActionByID(typeID) - if !ok { - return "", fmt.Errorf("action with id %d not found in ABI", typeID) - } - - return Unmarshal(inputABI, data[1:], actionType.Name) -} - -func Unmarshal(inputABI abi.ABI, data []byte, typeName string) (string, error) { - 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 := inputABI.FindTypeByName(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)) - fmt.Printf("\nabiTypeName=%s, abiType.Fields %v\n", abiTypeName, 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 TestDynamicMarshal(t *testing.T) { require := require.New(t) diff --git a/api/jsonrpc/client.go b/api/jsonrpc/client.go index ea3b29acaa..f851570704 100644 --- a/api/jsonrpc/client.go +++ b/api/jsonrpc/client.go @@ -194,19 +194,10 @@ func (cli *JSONRPCClient) GetABI(ctx context.Context) (abi.ABI, error) { return resp.ABI, err } -func (cli *JSONRPCClient) ExecuteActions(ctx context.Context, actor codec.Address, actions []chain.Action) ([][]byte, error) { - actionsMarshaled := make([][]byte, 0, len(actions)) - for _, action := range actions { - actionBytes, err := chain.MarshalTyped(action) - if err != nil { - return nil, fmt.Errorf("failed to marshal action: %w", err) - } - actionsMarshaled = append(actionsMarshaled, actionBytes) - } - +func (cli *JSONRPCClient) ExecuteActions(ctx context.Context, actor codec.Address, actionsBytes [][]byte) ([][]byte, error) { args := &ExecuteActionArgs{ Actor: actor, - Actions: actionsMarshaled, + Actions: actionsBytes, } resp := new(ExecuteActionReply) diff --git a/cmd/cli/actions.go b/cmd/cli/actions.go index 3721b0ec56..eb7fb94bda 100644 --- a/cmd/cli/actions.go +++ b/cmd/cli/actions.go @@ -36,7 +36,7 @@ func (a abiWrapper) String() string { result := "" for _, action := range a.ABI.Actions { result += fmt.Sprintf("---\n%s\n\n", action.Name) - typ, found := findTypeByName(a.ABI.Types, action.Name) + typ, found := a.ABI.FindTypeByName(action.Name) if !found { result += fmt.Sprintf(" Error: Type not found for action %s\n", action.Name) continue @@ -47,13 +47,13 @@ func (a abiWrapper) String() string { } } - output, found := findOutputByID(a.ABI.Outputs, action.ID) + output, found := a.ABI.FindOutputByID(action.ID) if !found { result += fmt.Sprintf("No outputs for %s with id %d\n", action.Name, action.ID) continue } - typ, found = findTypeByName(a.ABI.Types, output.Name) + typ, found = a.ABI.FindTypeByName(output.Name) if !found { result += fmt.Sprintf(" Error: Type not found for output %s\n", output.Name) continue @@ -67,24 +67,6 @@ func (a abiWrapper) String() string { return result } -func findOutputByID(outputs []abi.TypedStruct, id uint8) (abi.TypedStruct, bool) { - for _, output := range outputs { - if output.ID == id { - return output, true - } - } - return abi.TypedStruct{}, false -} - -func findTypeByName(types []abi.Type, name string) (abi.Type, bool) { - for _, typ := range types { - if typ.Name == name { - return typ, true - } - } - return abi.Type{}, false -} - func init() { rootCmd.AddCommand(actionsCmd) } diff --git a/cmd/cli/read.go b/cmd/cli/read.go new file mode 100644 index 0000000000..11bc9eba3e --- /dev/null +++ b/cmd/cli/read.go @@ -0,0 +1,177 @@ +package main + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/ava-labs/hypersdk/abi/dynamic" + "github.com/ava-labs/hypersdk/api/jsonrpc" + "github.com/ava-labs/hypersdk/auth" + "github.com/ava-labs/hypersdk/codec" + "github.com/spf13/cobra" +) + +var readCmd = &cobra.Command{ + Use: "read [action]", + Short: "Read data from the chain", + RunE: func(cmd *cobra.Command, args []string) error { + //1. figure out sender address + senderStr, err := cmd.Flags().GetString("sender") + if err != nil { + return fmt.Errorf("failed to get sender: %w", err) + } + + var sender codec.Address + + if senderStr != "" { + sender, err = codec.StringToAddress(senderStr) + if err != nil { + return fmt.Errorf("failed to convert sender to address: %w", err) + } + } else { + //ok, infer user's address from the private key + keyString, err := getConfigValue(cmd, "key") + if err != nil { + return fmt.Errorf("failed to get key from config: %w", err) + } + key, err := privateKeyFromString(keyString) + if err != nil { + return fmt.Errorf("failed to decode key: %w", err) + } + sender = auth.NewED25519Address(key.PublicKey()) + } + + //2. create client + endpoint, err := getConfigValue(cmd, "endpoint") + if err != nil { + return fmt.Errorf("failed to get endpoint: %w", err) + } + client := jsonrpc.NewJSONRPCClient(endpoint) + + //3. get abi + abi, err := client.GetABI(context.Background()) + if err != nil { + return fmt.Errorf("failed to get abi: %w", err) + } + + //4. get action name from args + if len(args) == 0 { + return fmt.Errorf("action name is required") + } + actionName := args[0] + _, found := abi.FindActionByName(actionName) + if !found { + return fmt.Errorf("failed to find action: %s", actionName) + } + + typ, found := abi.FindTypeByName(actionName) + if !found { + return fmt.Errorf("failed to find type: %s", actionName) + } + + //4. get key-value pairs + inputKV, err := cmd.Flags().GetStringToString("data") + if err != nil { + return fmt.Errorf("failed to get data key-value pairs: %w", err) + } + + kvPairs := make(map[string]interface{}) + for _, field := range typ.Fields { + if field.Type == "Address" { + value, ok := inputKV[field.Name] + if !ok { + return fmt.Errorf("missing required field: %s", field.Name) + } + kvPairs[field.Name] = value + } else if field.Type == "uint64" { + value, ok := inputKV[field.Name] + if !ok { + return fmt.Errorf("missing required field: %s", field.Name) + } + parsedValue, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse %s as uint64: %w", field.Name, err) + } + kvPairs[field.Name] = parsedValue + } else if field.Type == "[]uint8" { + value, ok := inputKV[field.Name] + if !ok { + continue + } + if value == "" { + kvPairs[field.Name] = []uint8{} + } else { + decodedValue, err := base64.StdEncoding.DecodeString(value) + if err != nil { + return fmt.Errorf("failed to decode base64 for %s: %w", field.Name, err) + } + kvPairs[field.Name] = decodedValue + } + } else { + return fmt.Errorf("unsupported field type: %s", field.Type) + } + } + + //5. create action using kvPairs + jsonPayload, err := json.Marshal(kvPairs) + if err != nil { + return fmt.Errorf("failed to marshal kvPairs: %w", err) + } + + actionBytes, err := dynamic.Marshal(abi, actionName, string(jsonPayload)) + if err != nil { + return fmt.Errorf("failed to marshal action: %w", err) + } + + results, err := client.ExecuteActions(context.Background(), sender, [][]byte{actionBytes}) + if err != nil { + return fmt.Errorf("failed to execute actions: %w", err) + } + + if len(results) == 0 { + return fmt.Errorf("no results returned from action execution") + } + + resultJSON, err := dynamic.UnmarshalOutput(abi, results[0]) + if err != nil { + return fmt.Errorf("failed to unmarshal result: %w", err) + } + + var resultStruct map[string]interface{} + err = json.Unmarshal([]byte(resultJSON), &resultStruct) + if err != nil { + return fmt.Errorf("failed to unmarshal result JSON: %w", err) + } + + return printValue(cmd, readResponse{ + Result: resultStruct, + }) + }, +} + +type readResponse struct { + Result map[string]interface{} `json:"result"` +} + +func (r readResponse) String() string { + var result strings.Builder + for key, value := range r.Result { + jsonValue, err := json.Marshal(value) + if err != nil { + // If marshaling fails, use the default string representation + jsonValue = []byte(fmt.Sprintf("%v", value)) + } + result.WriteString(fmt.Sprintf("%s: %s\n", key, string(jsonValue))) + } + return result.String() +} + +func init() { + readCmd.Flags().String("sender", "", "Address of the sender in hex") + readCmd.Flags().StringToString("data", nil, "Key-value pairs for the action data (e.g., key1=value1,key2=value2)") + rootCmd.AddCommand(readCmd) +} diff --git a/examples/morpheusvm/storage/storage.go b/examples/morpheusvm/storage/storage.go index 5c7ff3914b..23a62f0ed6 100644 --- a/examples/morpheusvm/storage/storage.go +++ b/examples/morpheusvm/storage/storage.go @@ -159,7 +159,7 @@ func SubBalance( ) (uint64, error) { key, bal, ok, err := getBalance(ctx, mu, addr) if !ok { - return 0, ErrInvalidAddress + return 0, ErrInvalidBalance } if err != nil { return 0, err From 9c2c631a0a74491fd482bcf5b24a3c174a2a785e Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:08:48 +0000 Subject: [PATCH 18/53] add support for 0x --- cmd/cli/{cli.go => main.go} | 0 cmd/cli/read.go | 53 ++++++++++++++++++++++--------------- codec/hex.go | 4 +++ codec/hex_test.go | 15 +++++++++++ 4 files changed, 50 insertions(+), 22 deletions(-) rename cmd/cli/{cli.go => main.go} (100%) diff --git a/cmd/cli/cli.go b/cmd/cli/main.go similarity index 100% rename from cmd/cli/cli.go rename to cmd/cli/main.go diff --git a/cmd/cli/read.go b/cmd/cli/read.go index 11bc9eba3e..233d02a622 100644 --- a/cmd/cli/read.go +++ b/cmd/cli/read.go @@ -127,45 +127,54 @@ var readCmd = &cobra.Command{ return fmt.Errorf("failed to marshal action: %w", err) } - results, err := client.ExecuteActions(context.Background(), sender, [][]byte{actionBytes}) - if err != nil { - return fmt.Errorf("failed to execute actions: %w", err) - } + results, executeErr := client.ExecuteActions(context.Background(), sender, [][]byte{actionBytes}) + var resultStruct map[string]interface{} - if len(results) == 0 { - return fmt.Errorf("no results returned from action execution") - } + if len(results) == 1 { + resultJSON, err := dynamic.UnmarshalOutput(abi, results[0]) + if err != nil { + return fmt.Errorf("failed to unmarshal result: %w", err) + } - resultJSON, err := dynamic.UnmarshalOutput(abi, results[0]) - if err != nil { - return fmt.Errorf("failed to unmarshal result: %w", err) + err = json.Unmarshal([]byte(resultJSON), &resultStruct) + if err != nil { + return fmt.Errorf("failed to unmarshal result JSON: %w", err) + } } - var resultStruct map[string]interface{} - err = json.Unmarshal([]byte(resultJSON), &resultStruct) - if err != nil { - return fmt.Errorf("failed to unmarshal result JSON: %w", err) + errorString := "" + if executeErr != nil { + errorString = executeErr.Error() } return printValue(cmd, readResponse{ - Result: resultStruct, + Result: resultStruct, + Success: executeErr == nil, + Error: errorString, }) }, } type readResponse struct { - Result map[string]interface{} `json:"result"` + Result map[string]interface{} `json:"result"` + Success bool `json:"success"` + Error string `json:"error"` } func (r readResponse) String() string { var result strings.Builder - for key, value := range r.Result { - jsonValue, err := json.Marshal(value) - if err != nil { - // If marshaling fails, use the default string representation - jsonValue = []byte(fmt.Sprintf("%v", value)) + if r.Success { + result.WriteString("✅ Read succeeded\n\n") + result.WriteString("Result:\n") + for key, value := range r.Result { + jsonValue, err := json.Marshal(value) + if err != nil { + jsonValue = []byte(fmt.Sprintf("%v", value)) + } + result.WriteString(fmt.Sprintf(" %s: %s\n", key, string(jsonValue))) } - result.WriteString(fmt.Sprintf("%s: %s\n", key, string(jsonValue))) + } else { + result.WriteString(fmt.Sprintf("❌ Read failed: %s\n", r.Error)) } return result.String() } diff --git a/codec/hex.go b/codec/hex.go index 44d2e5d479..11bb929ece 100644 --- a/codec/hex.go +++ b/codec/hex.go @@ -13,6 +13,10 @@ func ToHex(b []byte) string { // LoadHex Converts hex encoded string into bytes. Returns // an error if key is invalid. func LoadHex(s string, expectedSize int) ([]byte, error) { + if len(s) >= 2 && s[:2] == "0x" { + s = s[2:] + } + bytes, err := hex.DecodeString(s) if err != nil { return nil, err diff --git a/codec/hex_test.go b/codec/hex_test.go index a610df8dc4..cfa0138bcd 100644 --- a/codec/hex_test.go +++ b/codec/hex_test.go @@ -29,3 +29,18 @@ func TestBytesHex(t *testing.T) { require.NoError(json.Unmarshal(jsonMarshalledBytes, &jsonUnmarshalledBytes)) require.Equal(b, []byte(jsonUnmarshalledBytes)) } + +func TestLoadHex(t *testing.T) { + require := require.New(t) + + var actual []byte + var err error + + actual, err = LoadHex("0x1234", 2) + require.NoError(err) + require.Equal([]byte{0x12, 0x34}, actual) + + actual, err = LoadHex("1234", 2) + require.NoError(err) + require.Equal([]byte{0x12, 0x34}, actual) +} From 36bb8e1a3982fcba154651e6efadca16d9cb8667 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:21:04 +0000 Subject: [PATCH 19/53] request keys --- cli/prompt/prompt.go | 14 ++---- cmd/cli/README.md | 5 ++ cmd/cli/read.go | 113 ++++++++++++++++++++++++++++--------------- 3 files changed, 83 insertions(+), 49 deletions(-) diff --git a/cli/prompt/prompt.go b/cli/prompt/prompt.go index a5febf57b5..e1ed38939d 100644 --- a/cli/prompt/prompt.go +++ b/cli/prompt/prompt.go @@ -4,7 +4,6 @@ package prompt import ( - "encoding/hex" "errors" "fmt" "strconv" @@ -34,10 +33,7 @@ func Bytes(label string) ([]byte, error) { promptText := promptui.Prompt{ Label: label, Validate: func(input string) error { - if len(input) == 0 { - return ErrInputEmpty - } - _, err := hex.DecodeString(input) + _, err := codec.LoadHex(input, -1) return err }, } @@ -45,17 +41,15 @@ func Bytes(label string) ([]byte, error) { if err != nil { return nil, err } - return hex.DecodeString(hexString) + return codec.LoadHex(hexString, -1) } func Address(label string) (codec.Address, error) { promptText := promptui.Prompt{ Label: label, Validate: func(input string) error { - if len(input) == 0 { - return ErrInputEmpty - } - return nil + _, err := codec.StringToAddress(strings.TrimSpace(input)) + return err }, } recipient, err := promptText.Run() diff --git a/cmd/cli/README.md b/cmd/cli/README.md index e6107a8391..4be9884cd5 100644 --- a/cmd/cli/README.md +++ b/cmd/cli/README.md @@ -19,3 +19,8 @@ API: ✅ `actions` - print the list of actions available in the ABI, for JSON it prints ABI JSON `tx [action name] [action params]` - sends a transaction with a single action `read [action name] [action params]` - simulates a single action transaction + + +```bash +go run ./cmd/cli/ read Transfer --key=./examples/morpheusvm/demo.pk --data to=0x000000000000000000000000000000000000000000000000000000000000000000,value=12 +``` diff --git a/cmd/cli/read.go b/cmd/cli/read.go index 233d02a622..b0e966e95e 100644 --- a/cmd/cli/read.go +++ b/cmd/cli/read.go @@ -8,9 +8,11 @@ import ( "strconv" "strings" + "github.com/ava-labs/hypersdk/abi" "github.com/ava-labs/hypersdk/abi/dynamic" "github.com/ava-labs/hypersdk/api/jsonrpc" "github.com/ava-labs/hypersdk/auth" + "github.com/ava-labs/hypersdk/cli/prompt" "github.com/ava-labs/hypersdk/codec" "github.com/spf13/cobra" ) @@ -74,45 +76,23 @@ var readCmd = &cobra.Command{ } //4. get key-value pairs - inputKV, err := cmd.Flags().GetStringToString("data") + inputData, err := cmd.Flags().GetStringToString("data") if err != nil { return fmt.Errorf("failed to get data key-value pairs: %w", err) } - kvPairs := make(map[string]interface{}) - for _, field := range typ.Fields { - if field.Type == "Address" { - value, ok := inputKV[field.Name] - if !ok { - return fmt.Errorf("missing required field: %s", field.Name) - } - kvPairs[field.Name] = value - } else if field.Type == "uint64" { - value, ok := inputKV[field.Name] - if !ok { - return fmt.Errorf("missing required field: %s", field.Name) - } - parsedValue, err := strconv.ParseUint(value, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse %s as uint64: %w", field.Name, err) - } - kvPairs[field.Name] = parsedValue - } else if field.Type == "[]uint8" { - value, ok := inputKV[field.Name] - if !ok { - continue - } - if value == "" { - kvPairs[field.Name] = []uint8{} - } else { - decodedValue, err := base64.StdEncoding.DecodeString(value) - if err != nil { - return fmt.Errorf("failed to decode base64 for %s: %w", field.Name, err) - } - kvPairs[field.Name] = decodedValue - } - } else { - return fmt.Errorf("unsupported field type: %s", field.Type) + shouldAskForFlags := len(inputData) == 0 + + var kvPairs map[string]interface{} + if shouldAskForFlags { + kvPairs, err = askForFlags(typ) + if err != nil { + return fmt.Errorf("failed to ask for flags: %w", err) + } + } else { + kvPairs, err = fillFromInputData(typ, inputData) + if err != nil { + return fmt.Errorf("failed to fill from kvData: %w", err) } } @@ -155,6 +135,62 @@ var readCmd = &cobra.Command{ }, } +func fillFromInputData(typ abi.Type, kvData map[string]string) (map[string]interface{}, error) { + kvPairs := make(map[string]interface{}) + for _, field := range typ.Fields { + value, ok := kvData[field.Name] + if !ok { + continue + } + switch field.Type { + case "Address": + kvPairs[field.Name] = value + case "uint64": + parsedValue, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse %s as uint64: %w", field.Name, err) + } + kvPairs[field.Name] = parsedValue + case "[]uint8": + if value == "" { + kvPairs[field.Name] = []uint8{} + } else { + decodedValue, err := base64.StdEncoding.DecodeString(value) + if err != nil { + return nil, fmt.Errorf("failed to decode base64 for %s: %w", field.Name, err) + } + kvPairs[field.Name] = decodedValue + } + default: + return nil, fmt.Errorf("unsupported field type: %s", field.Type) + } + } + return kvPairs, nil +} + +func askForFlags(typ abi.Type) (map[string]interface{}, error) { + kvPairs := make(map[string]interface{}) + for _, field := range typ.Fields { + var err error + var value interface{} + switch field.Type { + case "Address": + value, err = prompt.Address(field.Name) + case "uint64": + value, err = prompt.Amount(field.Name, ^uint64(0), nil) + case "[]uint8": + value, err = prompt.Bytes(field.Name) + default: + return nil, fmt.Errorf("unsupported field type: %s", field.Type) + } + if err != nil { + return nil, fmt.Errorf("failed to get input for %s field: %w", field.Name, err) + } + kvPairs[field.Name] = value + } + return kvPairs, nil +} + type readResponse struct { Result map[string]interface{} `json:"result"` Success bool `json:"success"` @@ -164,17 +200,16 @@ type readResponse struct { func (r readResponse) String() string { var result strings.Builder if r.Success { - result.WriteString("✅ Read succeeded\n\n") - result.WriteString("Result:\n") + result.WriteString("✅ Result:\n") for key, value := range r.Result { jsonValue, err := json.Marshal(value) if err != nil { jsonValue = []byte(fmt.Sprintf("%v", value)) } - result.WriteString(fmt.Sprintf(" %s: %s\n", key, string(jsonValue))) + result.WriteString(fmt.Sprintf("%s: %s\n", key, string(jsonValue))) } } else { - result.WriteString(fmt.Sprintf("❌ Read failed: %s\n", r.Error)) + result.WriteString(fmt.Sprintf("❌ Error: %s\n", r.Error)) } return result.String() } From b3263fbca4a73d85a4bb9cc5946bebbe9b334699 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:30:51 +0000 Subject: [PATCH 20/53] use hex --- cmd/cli/read.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/cli/read.go b/cmd/cli/read.go index b0e966e95e..01830fe927 100644 --- a/cmd/cli/read.go +++ b/cmd/cli/read.go @@ -2,7 +2,6 @@ package main import ( "context" - "encoding/base64" "encoding/json" "fmt" "strconv" @@ -155,9 +154,9 @@ func fillFromInputData(typ abi.Type, kvData map[string]string) (map[string]inter if value == "" { kvPairs[field.Name] = []uint8{} } else { - decodedValue, err := base64.StdEncoding.DecodeString(value) + decodedValue, err := codec.LoadHex(value, -1) if err != nil { - return nil, fmt.Errorf("failed to decode base64 for %s: %w", field.Name, err) + return nil, fmt.Errorf("failed to decode hex for %s: %w", field.Name, err) } kvPairs[field.Name] = decodedValue } From 4cb50cfb8d44ed0e2391fabe3ffaed87416e59f6 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:58:40 +0000 Subject: [PATCH 21/53] parse int and uint --- cli/prompt/prompt.go | 37 +++++++++-- cli/spam.go | 20 +++--- cmd/cli/README.md | 1 + cmd/cli/config.go | 21 ++++--- cmd/cli/read.go | 112 ++++++++++++++++++++++----------- consts/consts.go | 10 +-- internal/window/window_test.go | 4 +- 7 files changed, 138 insertions(+), 67 deletions(-) diff --git a/cli/prompt/prompt.go b/cli/prompt/prompt.go index e1ed38939d..0d3a5e37c9 100644 --- a/cli/prompt/prompt.go +++ b/cli/prompt/prompt.go @@ -147,18 +147,17 @@ func Amount( rawAmount = strings.TrimSpace(rawAmount) return utils.ParseBalance(rawAmount) } - func Int( label string, - max int, -) (int, error) { + max int64, +) (int64, error) { promptText := promptui.Prompt{ Label: label, Validate: func(input string) error { if len(input) == 0 { return ErrInputEmpty } - amount, err := strconv.Atoi(input) + amount, err := strconv.ParseInt(input, 10, 64) if err != nil { return err } @@ -176,7 +175,35 @@ func Int( return 0, err } rawAmount = strings.TrimSpace(rawAmount) - return strconv.Atoi(rawAmount) + return strconv.ParseInt(rawAmount, 10, 64) +} + +func Uint( + label string, + max uint64, +) (uint64, error) { + promptText := promptui.Prompt{ + Label: label, + Validate: func(input string) error { + if len(input) == 0 { + return ErrInputEmpty + } + amount, err := strconv.ParseUint(input, 10, 64) + if err != nil { + return err + } + if amount > max { + return fmt.Errorf("%d must be <= %d", amount, max) + } + return nil + }, + } + rawAmount, err := promptText.Run() + if err != nil { + return 0, err + } + rawAmount = strings.TrimSpace(rawAmount) + return strconv.ParseUint(rawAmount, 10, 64) } func Float( diff --git a/cli/spam.go b/cli/spam.go index 7391394b69..58ab890b18 100644 --- a/cli/spam.go +++ b/cli/spam.go @@ -48,7 +48,7 @@ func (h *Handler) BuildSpammer(sh throughput.SpamHelper, defaults bool) (*throug return throughput.NewSpammer(sc, sh) } // Collect parameters - numAccounts, err := prompt.Int("number of accounts", consts.MaxInt) + numAccounts, err := prompt.Int("number of accounts", int64(consts.MaxInt)) if err != nil { return nil, err } @@ -64,19 +64,19 @@ func (h *Handler) BuildSpammer(sh throughput.SpamHelper, defaults bool) (*throug return nil, err } - txsPerSecond, err := prompt.Int("txs to try and issue per second", consts.MaxInt) + txsPerSecond, err := prompt.Int("txs to try and issue per second", int64(consts.MaxInt)) if err != nil { return nil, err } - minTxsPerSecond, err := prompt.Int("minimum txs to issue per second", consts.MaxInt) + minTxsPerSecond, err := prompt.Int("minimum txs to issue per second", int64(consts.MaxInt)) if err != nil { return nil, err } - txsPerSecondStep, err := prompt.Int("txs to increase per second", consts.MaxInt) + txsPerSecondStep, err := prompt.Int("txs to increase per second", int64(consts.MaxInt)) if err != nil { return nil, err } - numClients, err := prompt.Int("number of clients per node", consts.MaxInt) + numClients, err := prompt.Int("number of clients per node", int64(consts.MaxInt)) if err != nil { return nil, err } @@ -86,11 +86,11 @@ func (h *Handler) BuildSpammer(sh throughput.SpamHelper, defaults bool) (*throug key, sZipf, vZipf, - txsPerSecond, - minTxsPerSecond, - txsPerSecondStep, - numClients, - numAccounts, + int(txsPerSecond), + int(minTxsPerSecond), + int(txsPerSecondStep), + int(numClients), + int(numAccounts), ) return throughput.NewSpammer(sc, sh) diff --git a/cmd/cli/README.md b/cmd/cli/README.md index 4be9884cd5..6db90a98ff 100644 --- a/cmd/cli/README.md +++ b/cmd/cli/README.md @@ -7,6 +7,7 @@ Requirements: - Keeps the private key and the endpoint in the user's home folder in `~/.hypersdk-cli/config.cfg` - Argument `--endpoint` added to any request, overrides the request endpoint. If `--endpoint` is not set and `~/.hypersdk-cli/config.cfg` doesn't have endpoint field, errors - Supports ed25519 keys only (enforce default, this is a default opinionated CLI) +- If --data supplied, or json selected as an output would not ask for the action arguments API: ✅ `address` - prints current key address diff --git a/cmd/cli/config.go b/cmd/cli/config.go index 2a45c0d4f8..1c7b12f0e5 100644 --- a/cmd/cli/config.go +++ b/cmd/cli/config.go @@ -4,7 +4,6 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" - "errors" "fmt" "os" "path/filepath" @@ -13,24 +12,30 @@ import ( "github.com/spf13/cobra" ) -func printValue(cmd *cobra.Command, v fmt.Stringer) error { +func isJSONOutputRequested(cmd *cobra.Command) (bool, error) { output, err := getConfigValue(cmd, "output") - if err != nil && !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("failed to get output format: %w", err) + if err != nil { + return false, fmt.Errorf("failed to get output format: %w", err) } + return strings.ToLower(output) == "json", nil +} - if output == "json" { +func printValue(cmd *cobra.Command, v fmt.Stringer) error { + isJSON, err := isJSONOutputRequested(cmd) + if err != nil { + return err + } + + if isJSON { jsonBytes, err := json.MarshalIndent(v, "", " ") if err != nil { return fmt.Errorf("failed to marshal JSON: %w", err) } fmt.Println(string(jsonBytes)) return nil - } else if output == "human" { + } else { fmt.Println(v.String()) return nil - } else { - return fmt.Errorf("invalid output format: %s", output) } } diff --git a/cmd/cli/read.go b/cmd/cli/read.go index 01830fe927..429c072b0d 100644 --- a/cmd/cli/read.go +++ b/cmd/cli/read.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "math" "strconv" "strings" @@ -74,28 +75,12 @@ var readCmd = &cobra.Command{ return fmt.Errorf("failed to find type: %s", actionName) } - //4. get key-value pairs - inputData, err := cmd.Flags().GetStringToString("data") + //5. create action using kvPairs + kvPairs, err := fillAction(cmd, typ) if err != nil { - return fmt.Errorf("failed to get data key-value pairs: %w", err) - } - - shouldAskForFlags := len(inputData) == 0 - - var kvPairs map[string]interface{} - if shouldAskForFlags { - kvPairs, err = askForFlags(typ) - if err != nil { - return fmt.Errorf("failed to ask for flags: %w", err) - } - } else { - kvPairs, err = fillFromInputData(typ, inputData) - if err != nil { - return fmt.Errorf("failed to fill from kvData: %w", err) - } + return err } - //5. create action using kvPairs jsonPayload, err := json.Marshal(kvPairs) if err != nil { return fmt.Errorf("failed to marshal kvPairs: %w", err) @@ -134,6 +119,36 @@ var readCmd = &cobra.Command{ }, } +func fillAction(cmd *cobra.Command, typ abi.Type) (map[string]interface{}, error) { + // get key-value pairs + inputData, err := cmd.Flags().GetStringToString("data") + if err != nil { + return nil, fmt.Errorf("failed to get data key-value pairs: %w", err) + } + + isJSONOutput, err := isJSONOutputRequested(cmd) + if err != nil { + return nil, fmt.Errorf("failed to get output format: %w", err) + } + + isInteractive := len(inputData) == 0 && !isJSONOutput + + var kvPairs map[string]interface{} + if isInteractive { + kvPairs, err = askForFlags(typ) + if err != nil { + return nil, fmt.Errorf("failed to ask for flags: %w", err) + } + } else { + kvPairs, err = fillFromInputData(typ, inputData) + if err != nil { + return nil, fmt.Errorf("failed to fill from kvData: %w", err) + } + } + + return kvPairs, nil +} + func fillFromInputData(typ abi.Type, kvData map[string]string) (map[string]interface{}, error) { kvPairs := make(map[string]interface{}) for _, field := range typ.Fields { @@ -141,46 +156,69 @@ func fillFromInputData(typ abi.Type, kvData map[string]string) (map[string]inter if !ok { continue } + var parsedValue interface{} + var err error switch field.Type { case "Address": - kvPairs[field.Name] = value - case "uint64": - parsedValue, err := strconv.ParseUint(value, 10, 64) - if err != nil { - return nil, fmt.Errorf("failed to parse %s as uint64: %w", field.Name, err) - } - kvPairs[field.Name] = parsedValue + parsedValue = value + case "uint8", "uint16", "uint32", "uint", "uint64": + parsedValue, err = strconv.ParseUint(value, 10, 64) + case "int8", "int16", "int32", "int", "int64": + parsedValue, err = strconv.ParseInt(value, 10, 64) case "[]uint8": if value == "" { - kvPairs[field.Name] = []uint8{} + parsedValue = []uint8{} } else { - decodedValue, err := codec.LoadHex(value, -1) - if err != nil { - return nil, fmt.Errorf("failed to decode hex for %s: %w", field.Name, err) - } - kvPairs[field.Name] = decodedValue + parsedValue, err = codec.LoadHex(value, -1) } + case "string": + parsedValue = value + case "bool": + parsedValue, err = strconv.ParseBool(value) default: return nil, fmt.Errorf("unsupported field type: %s", field.Type) } + if err != nil { + return nil, fmt.Errorf("failed to parse %s: %w", field.Name, err) + } + kvPairs[field.Name] = parsedValue } return kvPairs, nil } func askForFlags(typ abi.Type) (map[string]interface{}, error) { kvPairs := make(map[string]interface{}) + for _, field := range typ.Fields { var err error var value interface{} switch field.Type { case "Address": value, err = prompt.Address(field.Name) - case "uint64": - value, err = prompt.Amount(field.Name, ^uint64(0), nil) + case "uint8": + value, err = prompt.Uint(field.Name, math.MaxUint8) + case "uint16": + value, err = prompt.Uint(field.Name, math.MaxUint16) + case "uint32": + value, err = prompt.Uint(field.Name, math.MaxUint32) + case "uint", "uint64": + value, err = prompt.Uint(field.Name, math.MaxUint64) + case "int8": + value, err = prompt.Int(field.Name, math.MaxInt8) + case "int16": + value, err = prompt.Int(field.Name, math.MaxInt16) + case "int32": + value, err = prompt.Int(field.Name, math.MaxInt32) + case "int", "int64": + value, err = prompt.Int(field.Name, math.MaxInt64) case "[]uint8": value, err = prompt.Bytes(field.Name) + case "string": + value, err = prompt.String(field.Name, 0, 1024) + case "bool": + value, err = prompt.Bool(field.Name) default: - return nil, fmt.Errorf("unsupported field type: %s", field.Type) + return nil, fmt.Errorf("unsupported field type in CLI: %s", field.Type) } if err != nil { return nil, fmt.Errorf("failed to get input for %s field: %w", field.Name, err) @@ -199,7 +237,7 @@ type readResponse struct { func (r readResponse) String() string { var result strings.Builder if r.Success { - result.WriteString("✅ Result:\n") + result.WriteString("✅ Read-only execution successful:\n") for key, value := range r.Result { jsonValue, err := json.Marshal(value) if err != nil { @@ -208,7 +246,7 @@ func (r readResponse) String() string { result.WriteString(fmt.Sprintf("%s: %s\n", key, string(jsonValue))) } } else { - result.WriteString(fmt.Sprintf("❌ Error: %s\n", r.Error)) + result.WriteString(fmt.Sprintf("❌ Read-only execution failed: %s\n", r.Error)) } return result.String() } diff --git a/consts/consts.go b/consts/consts.go index 377c8f8d21..00afcfeab9 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -21,13 +21,13 @@ const ( // no more than 50 KiB of overhead but is likely much less) NetworkSizeLimit = 2_044_723 // 1.95 MiB - MaxUint8 = ^uint8(0) - MaxUint16 = ^uint16(0) + MaxUint8 = math.MaxUint8 + MaxUint16 = math.MaxUint16 MaxUint8Offset = 7 - MaxUint = ^uint(0) - MaxInt = int(MaxUint >> 1) + MaxUint = math.MaxUint + MaxInt = int(math.MaxUint >> 1) MaxUint64Offset = 63 - MaxUint64 = ^uint64(0) + MaxUint64 = math.MaxUint64 MaxFloat64 = math.MaxFloat64 MillisecondsPerSecond = 1000 Decimals = 9 diff --git a/internal/window/window_test.go b/internal/window/window_test.go index d0ddade09e..550a69105f 100644 --- a/internal/window/window_test.go +++ b/internal/window/window_test.go @@ -116,12 +116,12 @@ func TestUint64WindowOverflow(t *testing.T) { } sum := Sum(uint64Window) - require.Equal(consts.MaxUint64, sum) + require.Equal(uint64(consts.MaxUint64), sum) for i := 0; i < 10; i++ { Update(&uint64Window, i*8, uint64(i)) sum = Sum(uint64Window) - require.Equal(consts.MaxUint64, sum) + require.Equal(uint64(consts.MaxUint64), sum) } } From 42ae749441a35898ba11d9bead9201425ef4ec56 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 01:23:36 +0000 Subject: [PATCH 22/53] update readme --- cmd/cli/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/cli/README.md b/cmd/cli/README.md index 6db90a98ff..470dc92d56 100644 --- a/cmd/cli/README.md +++ b/cmd/cli/README.md @@ -19,7 +19,7 @@ API: ✅ `endpoint set --endpoint=https://hello.world:1234` sets endpoint ✅ `actions` - print the list of actions available in the ABI, for JSON it prints ABI JSON `tx [action name] [action params]` - sends a transaction with a single action -`read [action name] [action params]` - simulates a single action transaction +✅ `read [action name] [action params]` - simulates a single action transaction ```bash From 27a38a306cdcec123188279fa01f15005562f1d4 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 01:23:51 +0000 Subject: [PATCH 23/53] remove base64 --- cmd/cli/config.go | 13 ++++--------- cmd/cli/key_set.go | 2 +- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/cmd/cli/config.go b/cmd/cli/config.go index 1c7b12f0e5..12ebc97e76 100644 --- a/cmd/cli/config.go +++ b/cmd/cli/config.go @@ -1,14 +1,13 @@ package main import ( - "encoding/base64" - "encoding/hex" "encoding/json" "fmt" "os" "path/filepath" "strings" + "github.com/ava-labs/hypersdk/codec" "github.com/spf13/cobra" ) @@ -119,12 +118,8 @@ func writeConfig(config map[string]string) error { return nil } -func decodeWhatever(whatever string) ([]byte, error) { - if decoded, err := hex.DecodeString(whatever); err == nil { - return decoded, nil - } - - if decoded, err := base64.StdEncoding.DecodeString(whatever); err == nil { +func decodeFileOrHex(whatever string) ([]byte, error) { + if decoded, err := codec.LoadHex(whatever, -1); err == nil { return decoded, nil } @@ -132,5 +127,5 @@ func decodeWhatever(whatever string) ([]byte, error) { return fileContents, nil } - return nil, fmt.Errorf("unable to decode input as base64, hex, or read as file path") + return nil, fmt.Errorf("unable to decode input as hex, or read as file path") } diff --git a/cmd/cli/key_set.go b/cmd/cli/key_set.go index f3ab551216..c1613d0a42 100644 --- a/cmd/cli/key_set.go +++ b/cmd/cli/key_set.go @@ -63,7 +63,7 @@ func checkAndSavePrivateKey(keyString string, cmd *cobra.Command) error { } func privateKeyFromString(keyStr string) (ed25519.PrivateKey, error) { - keyBytes, err := decodeWhatever(keyStr) + keyBytes, err := decodeFileOrHex(keyStr) if err != nil { return ed25519.EmptyPrivateKey, fmt.Errorf("failed to decode key: %w", err) } From cd1547843994ee725a37f233051ea07643b69243 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 01:24:09 +0000 Subject: [PATCH 24/53] update morpheus address --- examples/morpheusvm/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/morpheusvm/README.md b/examples/morpheusvm/README.md index bfe4fae0ca..d2441b1782 100644 --- a/examples/morpheusvm/README.md +++ b/examples/morpheusvm/README.md @@ -108,7 +108,7 @@ This should return the following JSON: } ``` -_By default, this allocates all funds on the network to `morpheus1qrzvk4zlwj9zsacqgtufx7zvapd3quufqpxk5rsdd4633m4wz2fdjk97rwu`. The private +_By default, this allocates all funds on the network to `0x00c4cb545f748a28770042f893784ce85b107389004d6a0e0d6d7518eeae1292d9`. The private key for this address is `0x323b1d8f4eed5f0da9da93071b034f2dce9d2d22692c172f3cb252a64ddfafd01b057de320297c29ad0c1f589ea216869cf1938d88c9fbd70d6748323dbf2fa7`. For convenience, this key has is also stored at `demo.pk`._ @@ -162,7 +162,7 @@ Next, you'll need to add the chains you created and the default key to the If the key is added corretcly, you'll see the following log: ``` database: .morpheus-cli -imported address: morpheus1qrzvk4zlwj9zsacqgtufx7zvapd3quufqpxk5rsdd4633m4wz2fdjk97rwu +imported address: 0x00c4cb545f748a28770042f893784ce85b107389004d6a0e0d6d7518eeae1292d9 ``` Next, you'll need to store the URL of the nodes running on your Subnet: @@ -211,10 +211,10 @@ If successful, the balance response should look like this: ``` database: .morpheus-cli 2024/09/09 10:52:49 [JOB 1] WAL file .morpheus-cli/000044.log with log number 000044 stopped reading at offset: 0; replayed 0 keys in 0 batches -address: morpheus1qrzvk4zlwj9zsacqgtufx7zvapd3quufqpxk5rsdd4633m4wz2fdjk97rwu +address: 0x00c4cb545f748a28770042f893784ce85b107389004d6a0e0d6d7518eeae1292d9 chainID: JkJpw8ZPExTushPYYN4C8f7RHxjDRX8MAGGUGAdRRPEC2M3fx uri: http://127.0.0.1:9650/ext/bc/morpheusvm -address: morpheus1qrzvk4zlwj9zsacqgtufx7zvapd3quufqpxk5rsdd4633m4wz2fdjk97rwu balance: 1000.000000000 RED +address: 0x00c4cb545f748a28770042f893784ce85b107389004d6a0e0d6d7518eeae1292d9 balance: 1000.000000000 RED ``` ### Generate Another Address @@ -243,7 +243,7 @@ database: .morpheus-cli 2024/09/09 10:53:51 [JOB 1] WAL file .morpheus-cli/000047.log with log number 000047 stopped reading at offset: 0; replayed 0 keys in 0 batches chainID: JkJpw8ZPExTushPYYN4C8f7RHxjDRX8MAGGUGAdRRPEC2M3fx stored keys: 2 -0) address: morpheus1qrzvk4zlwj9zsacqgtufx7zvapd3quufqpxk5rsdd4633m4wz2fdjk97rwu balance: 10000000000.000000000 RED +0) address: 0x00c4cb545f748a28770042f893784ce85b107389004d6a0e0d6d7518eeae1292d9 balance: 10000000000.000000000 RED 1) address: morpheus1q8pyshaqzx4q9stqdt88hyg22axjwrvl0w9wgczct5fnfev9gcnrsqwjdn0 balance: 0.000000000 RED set default key: 0 ``` @@ -285,7 +285,7 @@ select chainID: 0 uri: http://127.0.0.1:9650/ext/bc/morpheusvm watching for new blocks on 2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk 👀 height:1 txs:1 units:440 root:WspVPrHNAwBcJRJPVwt7TW6WT4E74dN8DuD3WXueQTMt5FDdi -✅ sceRdaoqu2AAyLdHCdQkENZaXngGjRoc8nFdGyG8D9pCbTjbk actor: morpheus1qrzvk4zlwj9zsacqgtufx7zvapd3quufqpxk5rsdd4633m4wz2fdjk97rwu units: 440 summary (*actions.Transfer): [10.000000000 RED -> morpheus1q8rc050907hx39vfejpawjydmwe6uujw0njx9s6skzdpp3cm2he5s036p07] +✅ sceRdaoqu2AAyLdHCdQkENZaXngGjRoc8nFdGyG8D9pCbTjbk actor: 0x00c4cb545f748a28770042f893784ce85b107389004d6a0e0d6d7518eeae1292d9 units: 440 summary (*actions.Transfer): [10.000000000 RED -> morpheus1q8rc050907hx39vfejpawjydmwe6uujw0njx9s6skzdpp3cm2he5s036p07] ``` If you are running this on a local network, you may see that all blocks are empty. From 3f0561d1884419ca2ce2bea78a85cc4d8b35aef1 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 03:12:03 +0000 Subject: [PATCH 25/53] sign tx example --- examples/morpheusvm/tests/sign_tx_test.go | 101 ++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 examples/morpheusvm/tests/sign_tx_test.go diff --git a/examples/morpheusvm/tests/sign_tx_test.go b/examples/morpheusvm/tests/sign_tx_test.go new file mode 100644 index 0000000000..678d0f3561 --- /dev/null +++ b/examples/morpheusvm/tests/sign_tx_test.go @@ -0,0 +1,101 @@ +package tests + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "testing" + + "github.com/ava-labs/hypersdk/abi/dynamic" + "github.com/ava-labs/hypersdk/crypto/ed25519" + + "github.com/ava-labs/hypersdk/auth" + "github.com/ava-labs/hypersdk/chain" + "github.com/ava-labs/hypersdk/codec" + "github.com/ava-labs/hypersdk/consts" + "github.com/ava-labs/hypersdk/examples/morpheusvm/actions" + "github.com/ava-labs/hypersdk/examples/morpheusvm/vm" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/hypersdk/api/jsonrpc" +) + +func TestSignTx(t *testing.T) { + keyBytes, err := codec.LoadHex("0x323b1d8f4eed5f0da9da93071b034f2dce9d2d22692c172f3cb252a64ddfafd01b057de320297c29ad0c1f589ea216869cf1938d88c9fbd70d6748323dbf2fa7", -1) + require.NoError(t, err) + + key := ed25519.PrivateKey(keyBytes) + + factory := auth.NewED25519Factory(key) + + action := actions.Transfer{ + To: [33]byte{1, 2, 3, 4}, + Value: 1000000000, + Memo: []byte("test memo"), + } + + hyperVMRPC := vm.NewJSONRPCClient("http://localhost:9650/ext/bc/morpheusvm/") + hyperSDKRPC := jsonrpc.NewJSONRPCClient("http://localhost:9650/ext/bc/morpheusvm/") + + parser, err := hyperVMRPC.Parser(context.Background()) + require.NoError(t, err) + + _, tx, _, err := hyperSDKRPC.GenerateTransaction(context.Background(), parser, []chain.Action{&action}, factory) + require.NoError(t, err) + + unsignedBytes, err := tx.UnsignedBytes() + require.NoError(t, err) + fmt.Println("unsignedBytes", hex.EncodeToString(unsignedBytes)) + + signedBytes := tx.Bytes() + fmt.Println("signedBytes", hex.EncodeToString(signedBytes)) + + base := tx.Base + p := codec.NewWriter(base.Size(), consts.NetworkSizeLimit) + base.Marshal(p) + baseBytes := p.Bytes() + fmt.Println("baseBytes", hex.EncodeToString(baseBytes)) + + abi, err := hyperSDKRPC.GetABI(context.Background()) + require.NoError(t, err) + + jsonPayload, err := json.Marshal(action) + require.NoError(t, err) + + actionBytes, err := dynamic.Marshal(abi, "Transfer", string(jsonPayload)) + require.NoError(t, err) + + fmt.Println("actionBytes", hex.EncodeToString(actionBytes)) + + // Concatenate baseBytes, action count (0x01), and actionBytes + actualUnsignedBytes := make([]byte, 0, len(baseBytes)+1+len(actionBytes)) + actualUnsignedBytes = append(actualUnsignedBytes, baseBytes...) + actualUnsignedBytes = append(actualUnsignedBytes, 0x01) // Single action + actualUnsignedBytes = append(actualUnsignedBytes, actionBytes...) + + // Compare with original unsigned bytes + require.Equal(t, unsignedBytes, actualUnsignedBytes, "unsigned bytes do not match") + + actualAuth, err := factory.Sign(actualUnsignedBytes) // Sign the actualUnsignedBytes with the factory + require.NoError(t, err) + + signedBytesDebugStr := hex.EncodeToString(signedBytes) + actualUnsignedBytesStr := hex.EncodeToString(actualUnsignedBytes) + fmt.Printf("signedBytes: %s\n", signedBytesDebugStr) + + signedBytesDebugStr = strings.Replace(signedBytesDebugStr, actualUnsignedBytesStr, "_actualUnsignedBytesStr_", -1) + fmt.Printf("signedBytes: %s\n", signedBytesDebugStr) + + p = codec.NewWriter(actualAuth.Size(), consts.NetworkSizeLimit) + actualAuth.Marshal(p) + actualAuthBytes := append([]byte{actualAuth.GetTypeID()}, p.Bytes()...) + + actualAuthBytesStr := hex.EncodeToString(actualAuthBytes) + signedBytesDebugStr = strings.Replace(signedBytesDebugStr, actualAuthBytesStr, "_actualAuthBytesStr_", -1) + fmt.Printf("signedBytes: %s\n", signedBytesDebugStr) + + actualTxBytes := append(actualUnsignedBytes, actualAuthBytes...) + require.Equal(t, signedBytes, actualTxBytes, "signed bytes do not match") +} From 7018f7475d9a27815b4266a81a2a9fd7d78552be Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 03:32:14 +0000 Subject: [PATCH 26/53] sign multi actions --- examples/morpheusvm/tests/sign_tx_test.go | 99 +++++++++++++---------- 1 file changed, 56 insertions(+), 43 deletions(-) diff --git a/examples/morpheusvm/tests/sign_tx_test.go b/examples/morpheusvm/tests/sign_tx_test.go index 678d0f3561..4d00e82921 100644 --- a/examples/morpheusvm/tests/sign_tx_test.go +++ b/examples/morpheusvm/tests/sign_tx_test.go @@ -2,12 +2,10 @@ package tests import ( "context" - "encoding/hex" "encoding/json" - "fmt" - "strings" "testing" + "github.com/ava-labs/hypersdk/abi" "github.com/ava-labs/hypersdk/abi/dynamic" "github.com/ava-labs/hypersdk/crypto/ed25519" @@ -30,10 +28,16 @@ func TestSignTx(t *testing.T) { factory := auth.NewED25519Factory(key) - action := actions.Transfer{ + action1 := actions.Transfer{ To: [33]byte{1, 2, 3, 4}, Value: 1000000000, - Memo: []byte("test memo"), + Memo: []byte("test memo 1"), + } + + action2 := actions.Transfer{ + To: [33]byte{5, 6, 7, 8}, + Value: 2000000000, + Memo: []byte("test memo 2"), } hyperVMRPC := vm.NewJSONRPCClient("http://localhost:9650/ext/bc/morpheusvm/") @@ -42,60 +46,69 @@ func TestSignTx(t *testing.T) { parser, err := hyperVMRPC.Parser(context.Background()) require.NoError(t, err) - _, tx, _, err := hyperSDKRPC.GenerateTransaction(context.Background(), parser, []chain.Action{&action}, factory) + _, tx, _, err := hyperSDKRPC.GenerateTransaction(context.Background(), parser, []chain.Action{&action1, &action2}, factory) require.NoError(t, err) - unsignedBytes, err := tx.UnsignedBytes() - require.NoError(t, err) - fmt.Println("unsignedBytes", hex.EncodeToString(unsignedBytes)) - signedBytes := tx.Bytes() - fmt.Println("signedBytes", hex.EncodeToString(signedBytes)) - - base := tx.Base - p := codec.NewWriter(base.Size(), consts.NetworkSizeLimit) - base.Marshal(p) - baseBytes := p.Bytes() - fmt.Println("baseBytes", hex.EncodeToString(baseBytes)) abi, err := hyperSDKRPC.GetABI(context.Background()) require.NoError(t, err) - jsonPayload, err := json.Marshal(action) + // Use the manual signing function + manuallySignedBytes, err := SignTxManually([]chain.Action{&action1, &action2}, "Transfer", tx.Base, abi, key) require.NoError(t, err) - actionBytes, err := dynamic.Marshal(abi, "Transfer", string(jsonPayload)) - require.NoError(t, err) + // Compare results + require.Equal(t, signedBytes, manuallySignedBytes, "signed bytes do not match") +} + +// base := &chain.Base{ +// ChainID: chainID, +// Timestamp: time.Now().Unix()*1000 + 60000, +// MaxFee: 1_000_000, +// } - fmt.Println("actionBytes", hex.EncodeToString(actionBytes)) +func SignTxManually(actions []chain.Action, actionType string, base *chain.Base, abi abi.ABI, privateKey ed25519.PrivateKey) ([]byte, error) { + // Create auth factory + factory := auth.NewED25519Factory(privateKey) - // Concatenate baseBytes, action count (0x01), and actionBytes - actualUnsignedBytes := make([]byte, 0, len(baseBytes)+1+len(actionBytes)) - actualUnsignedBytes = append(actualUnsignedBytes, baseBytes...) - actualUnsignedBytes = append(actualUnsignedBytes, 0x01) // Single action - actualUnsignedBytes = append(actualUnsignedBytes, actionBytes...) + // Marshal base + p := codec.NewWriter(base.Size(), consts.NetworkSizeLimit) + base.Marshal(p) + baseBytes := p.Bytes() - // Compare with original unsigned bytes - require.Equal(t, unsignedBytes, actualUnsignedBytes, "unsigned bytes do not match") + // Build unsigned bytes starting with base and number of actions + unsignedBytes := make([]byte, 0) + unsignedBytes = append(unsignedBytes, baseBytes...) + unsignedBytes = append(unsignedBytes, byte(len(actions))) // Number of actions - actualAuth, err := factory.Sign(actualUnsignedBytes) // Sign the actualUnsignedBytes with the factory - require.NoError(t, err) + // Marshal and append each action + for _, action := range actions { + jsonPayload, err := json.Marshal(action) + if err != nil { + return nil, err + } - signedBytesDebugStr := hex.EncodeToString(signedBytes) - actualUnsignedBytesStr := hex.EncodeToString(actualUnsignedBytes) - fmt.Printf("signedBytes: %s\n", signedBytesDebugStr) + actionBytes, err := dynamic.Marshal(abi, actionType, string(jsonPayload)) + if err != nil { + return nil, err + } - signedBytesDebugStr = strings.Replace(signedBytesDebugStr, actualUnsignedBytesStr, "_actualUnsignedBytesStr_", -1) - fmt.Printf("signedBytes: %s\n", signedBytesDebugStr) + unsignedBytes = append(unsignedBytes, actionBytes...) + } - p = codec.NewWriter(actualAuth.Size(), consts.NetworkSizeLimit) - actualAuth.Marshal(p) - actualAuthBytes := append([]byte{actualAuth.GetTypeID()}, p.Bytes()...) + // Sign the transaction + auth, err := factory.Sign(unsignedBytes) + if err != nil { + return nil, err + } - actualAuthBytesStr := hex.EncodeToString(actualAuthBytes) - signedBytesDebugStr = strings.Replace(signedBytesDebugStr, actualAuthBytesStr, "_actualAuthBytesStr_", -1) - fmt.Printf("signedBytes: %s\n", signedBytesDebugStr) + // Marshal auth + p = codec.NewWriter(auth.Size(), consts.NetworkSizeLimit) + auth.Marshal(p) + authBytes := append([]byte{auth.GetTypeID()}, p.Bytes()...) - actualTxBytes := append(actualUnsignedBytes, actualAuthBytes...) - require.Equal(t, signedBytes, actualTxBytes, "signed bytes do not match") + // Combine everything into final signed transaction + signedBytes := append(unsignedBytes, authBytes...) + return signedBytes, nil } From 2dd46dc44b67395e449f9d38fe3178da63c3bb00 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 04:05:10 +0000 Subject: [PATCH 27/53] stuck with txs hanging --- api/ws/client.go | 6 +- cmd/cli/README.md | 5 + cmd/cli/transaction.go | 193 ++++++++++++++++++++++ examples/morpheusvm/tests/sign_tx_test.go | 42 +++-- 4 files changed, 223 insertions(+), 23 deletions(-) create mode 100644 cmd/cli/transaction.go diff --git a/api/ws/client.go b/api/ws/client.go index ed97aee342..86ae111720 100644 --- a/api/ws/client.go +++ b/api/ws/client.go @@ -165,10 +165,14 @@ func (c *WebSocketClient) ListenBlock( // IssueTx sends [tx] to the streaming rpc server. func (c *WebSocketClient) RegisterTx(tx *chain.Transaction) error { + return c.RegisterRawTx(tx.Bytes()) +} + +func (c *WebSocketClient) RegisterRawTx(txBytes []byte) error { if c.closed { return ErrClosed } - return c.mb.Send(append([]byte{TxMode}, tx.Bytes()...)) + return c.mb.Send(append([]byte{TxMode}, txBytes...)) } // ListenTx listens for responses from the streamingServer. diff --git a/cmd/cli/README.md b/cmd/cli/README.md index 470dc92d56..6b0172eb83 100644 --- a/cmd/cli/README.md +++ b/cmd/cli/README.md @@ -25,3 +25,8 @@ API: ```bash go run ./cmd/cli/ read Transfer --key=./examples/morpheusvm/demo.pk --data to=0x000000000000000000000000000000000000000000000000000000000000000000,value=12 ``` + +Notes: +- balance request is impossible to implement right now, as we don't have standardized balance RPC method on HyperSDK (not VM) level. +- maxFee is hardcoded to 1_000_000 for now + diff --git a/cmd/cli/transaction.go b/cmd/cli/transaction.go new file mode 100644 index 0000000000..85ea30d173 --- /dev/null +++ b/cmd/cli/transaction.go @@ -0,0 +1,193 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "time" + + "github.com/ava-labs/hypersdk/abi/dynamic" + "github.com/ava-labs/hypersdk/api/jsonrpc" + "github.com/ava-labs/hypersdk/api/ws" + "github.com/ava-labs/hypersdk/auth" + "github.com/ava-labs/hypersdk/chain" + "github.com/ava-labs/hypersdk/codec" + "github.com/ava-labs/hypersdk/consts" + "github.com/ava-labs/hypersdk/crypto/ed25519" + "github.com/ava-labs/hypersdk/utils" + "github.com/spf13/cobra" +) + +var txCmd = &cobra.Command{ + Use: "tx [action]", + Short: "Execute a transaction on the chain", + RunE: func(cmd *cobra.Command, args []string) error { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + //1. figure out sender address + keyString, err := getConfigValue(cmd, "key") + if err != nil { + return fmt.Errorf("failed to get key from config: %w", err) + } + key, err := privateKeyFromString(keyString) + if err != nil { + return fmt.Errorf("failed to decode key: %w", err) + } + + //2. create client + endpoint, err := getConfigValue(cmd, "endpoint") + if err != nil { + return fmt.Errorf("failed to get endpoint: %w", err) + } + client := jsonrpc.NewJSONRPCClient(endpoint) + + //3. get abi + abi, err := client.GetABI(ctx) + if err != nil { + return fmt.Errorf("failed to get abi: %w", err) + } + + //4. get action name from args + if len(args) == 0 { + return fmt.Errorf("action name is required") + } + actionName := args[0] + _, found := abi.FindActionByName(actionName) + if !found { + return fmt.Errorf("failed to find action: %s", actionName) + } + + typ, found := abi.FindTypeByName(actionName) + if !found { + return fmt.Errorf("failed to find type: %s", actionName) + } + + //5. create action using kvPairs + kvPairs, err := fillAction(cmd, typ) + if err != nil { + return err + } + + jsonPayload, err := json.Marshal(kvPairs) + if err != nil { + return fmt.Errorf("failed to marshal kvPairs: %w", err) + } + + actionBytes, err := dynamic.Marshal(abi, actionName, string(jsonPayload)) + if err != nil { + return fmt.Errorf("failed to marshal action: %w", err) + } + + _, _, chainID, err := client.Network(ctx) + if err != nil { + return fmt.Errorf("failed to get network info: %w", err) + } + + base := &chain.Base{ + ChainID: chainID, + Timestamp: time.Now().Unix()*1000 + 59000, + MaxFee: 1_000_000, + } + + signedBytes, err := SignTxManually([][]byte{actionBytes}, base, key) + if err != nil { + return fmt.Errorf("failed to sign tx: %w", err) + } + + // signedBytesOverrideHex := "00000192bcaf8728bc268cf27206903fbd7975e22f25a6190b8b7666858744aa8803c5d3e4997a56000000000000c0940200010203040000000000000000000000000000000000000000000000000000000000000000003b9aca000000000b74657374206d656d6f20310005060708000000000000000000000000000000000000000000000000000000000000000000773594000000000b74657374206d656d6f2032001b057de320297c29ad0c1f589ea216869cf1938d88c9fbd70d6748323dbf2fa70d9c361e5e0b495cbf2c5a2a1b68a71f01cba00062ee417ee8ea4652b40a8f953695db3ed217be20d202fe1ff053b7ad3e557c92dc3125d5c4c953673ed62a04" + + // if signedBytesOverrideHex != "" { + // signedBytes, err = hex.DecodeString(signedBytesOverrideHex) + // if err != nil { + // return fmt.Errorf("failed to decode signedBytesOverrideHex: %w", err) + // } + // } + + wsClient, err := ws.NewWebSocketClient(endpoint, 10*time.Second, 100, 1000000) + if err != nil { + return fmt.Errorf("failed to create ws client: %w", err) + } + + if err := wsClient.RegisterRawTx(signedBytes); err != nil { + return fmt.Errorf("failed to register tx: %w", err) + } + + expectedTxID := utils.ToID(signedBytes) + + fmt.Println("expectedTxID", expectedTxID) + + // Listen for the transaction result + var result *chain.Result + for { + txID, txErr, txResult, err := wsClient.ListenTx(ctx) + fmt.Println("txID", txID) + if err != nil { + if ctx.Err() == context.DeadlineExceeded { + return fmt.Errorf("failed to listen for transaction: %w", ctx.Err()) + } + return fmt.Errorf("failed to listen for transaction: %w", err) + } + if txErr != nil { + return txErr + } + if txID == expectedTxID { + result = txResult + break + } + log.Printf("Skipping unexpected transaction: %s\n", txID) + } + + // Check transaction result + if !result.Success { + return fmt.Errorf("transaction failed: %s", result.Error) + } + + //TODO: decode outputs + //TODO: print outputs + //TODO: print errors as json/stringer + + return nil + }, +} + +func init() { + txCmd.Flags().StringToString("data", nil, "Key-value pairs for the action data (e.g., key1=value1,key2=value2)") + rootCmd.AddCommand(txCmd) +} + +func SignTxManually(actionsTxBytes [][]byte, base *chain.Base, privateKey ed25519.PrivateKey) ([]byte, error) { + // Create auth factory + factory := auth.NewED25519Factory(privateKey) + + // Marshal base + p := codec.NewWriter(base.Size(), consts.NetworkSizeLimit) + base.Marshal(p) + baseBytes := p.Bytes() + + // Build unsigned bytes starting with base and number of actions + unsignedBytes := make([]byte, 0) + unsignedBytes = append(unsignedBytes, baseBytes...) + unsignedBytes = append(unsignedBytes, byte(len(actionsTxBytes))) // Number of actions + + // Append each action's bytes + for _, actionBytes := range actionsTxBytes { + unsignedBytes = append(unsignedBytes, actionBytes...) + } + + // Sign the transaction + auth, err := factory.Sign(unsignedBytes) + if err != nil { + return nil, err + } + + // Marshal auth + p = codec.NewWriter(auth.Size(), consts.NetworkSizeLimit) + auth.Marshal(p) + authBytes := append([]byte{auth.GetTypeID()}, p.Bytes()...) + + // Combine everything into final signed transaction + signedBytes := append(unsignedBytes, authBytes...) + return signedBytes, nil +} diff --git a/examples/morpheusvm/tests/sign_tx_test.go b/examples/morpheusvm/tests/sign_tx_test.go index 4d00e82921..390fc087b2 100644 --- a/examples/morpheusvm/tests/sign_tx_test.go +++ b/examples/morpheusvm/tests/sign_tx_test.go @@ -2,10 +2,10 @@ package tests import ( "context" + "encoding/hex" "encoding/json" "testing" - "github.com/ava-labs/hypersdk/abi" "github.com/ava-labs/hypersdk/abi/dynamic" "github.com/ava-labs/hypersdk/crypto/ed25519" @@ -54,21 +54,29 @@ func TestSignTx(t *testing.T) { abi, err := hyperSDKRPC.GetABI(context.Background()) require.NoError(t, err) + // Marshal actions to bytes + actionBytes := make([][]byte, 0) + for _, action := range []chain.Action{&action1, &action2} { + jsonPayload, err := json.Marshal(action) + require.NoError(t, err) + + bytes, err := dynamic.Marshal(abi, "Transfer", string(jsonPayload)) + require.NoError(t, err) + actionBytes = append(actionBytes, bytes) + } + // Use the manual signing function - manuallySignedBytes, err := SignTxManually([]chain.Action{&action1, &action2}, "Transfer", tx.Base, abi, key) + manuallySignedBytes, err := SignTxManually(actionBytes, tx.Base, key) require.NoError(t, err) // Compare results require.Equal(t, signedBytes, manuallySignedBytes, "signed bytes do not match") -} -// base := &chain.Base{ -// ChainID: chainID, -// Timestamp: time.Now().Unix()*1000 + 60000, -// MaxFee: 1_000_000, -// } + require.FailNow(t, hex.EncodeToString(manuallySignedBytes)) -func SignTxManually(actions []chain.Action, actionType string, base *chain.Base, abi abi.ABI, privateKey ed25519.PrivateKey) ([]byte, error) { +} + +func SignTxManually(actionsTxBytes [][]byte, base *chain.Base, privateKey ed25519.PrivateKey) ([]byte, error) { // Create auth factory factory := auth.NewED25519Factory(privateKey) @@ -80,20 +88,10 @@ func SignTxManually(actions []chain.Action, actionType string, base *chain.Base, // Build unsigned bytes starting with base and number of actions unsignedBytes := make([]byte, 0) unsignedBytes = append(unsignedBytes, baseBytes...) - unsignedBytes = append(unsignedBytes, byte(len(actions))) // Number of actions - - // Marshal and append each action - for _, action := range actions { - jsonPayload, err := json.Marshal(action) - if err != nil { - return nil, err - } - - actionBytes, err := dynamic.Marshal(abi, actionType, string(jsonPayload)) - if err != nil { - return nil, err - } + unsignedBytes = append(unsignedBytes, byte(len(actionsTxBytes))) // Number of actions + // Append each action's bytes + for _, actionBytes := range actionsTxBytes { unsignedBytes = append(unsignedBytes, actionBytes...) } From adaf68d7e859f174281a561ed99e7dffa1eb0ec2 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 04:10:31 +0000 Subject: [PATCH 28/53] readme --- cmd/cli/README.md | 138 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 114 insertions(+), 24 deletions(-) diff --git a/cmd/cli/README.md b/cmd/cli/README.md index 6b0172eb83..6a2fa283b8 100644 --- a/cmd/cli/README.md +++ b/cmd/cli/README.md @@ -1,32 +1,122 @@ -We need a CLI to do read only and write actions in transactions. +# HyperSDK CLI -Requirements: -- JSON/human-readable output. You can add `-o json` or `-o human` to any request -- action payload through interactive UI and through a one line tx, meaning any action argument, that's missing would be asked interactively -- Only flat actons are supported, no arrays, slices embeded structs, maps and struct fields. -- Keeps the private key and the endpoint in the user's home folder in `~/.hypersdk-cli/config.cfg` -- Argument `--endpoint` added to any request, overrides the request endpoint. If `--endpoint` is not set and `~/.hypersdk-cli/config.cfg` doesn't have endpoint field, errors -- Supports ed25519 keys only (enforce default, this is a default opinionated CLI) -- If --data supplied, or json selected as an output would not ask for the action arguments +A command-line interface for interacting with HyperSDK-based chains. -API: -✅ `address` - prints current key address -✅ `key generate` - generates a new address -`balance` - balance -✅ `key set` set private key from file, base64 string or hex string presented in the first argument -✅ `endpoint` - prints URL of the current endpoint -✅ `ping` - checks connectivity with the current endpoing -✅ `endpoint set --endpoint=https://hello.world:1234` sets endpoint -✅ `actions` - print the list of actions available in the ABI, for JSON it prints ABI JSON -`tx [action name] [action params]` - sends a transaction with a single action -✅ `read [action name] [action params]` - simulates a single action transaction +## Installation +Just `go run ./cmd/cli/ ` for now + +## Configuration + +The CLI stores configuration in `~/.hypersdk-cli/config.cfg`. This includes: +- Private key +- Endpoint URL + +## Global Flags + +- `--endpoint`: Override the default endpoint for a single command +- `-o, --output`: Set output format (`human` or `json`) + +## Commands + +### address + +Print the current key address. + +```bash +go run ./cmd/cli/ address +``` + +### key + +Manage keys. + +#### generate + +Generate a new ED25519 key pair. + +```bash +go run ./cmd/cli/ key generate +``` + +#### set + +Set the private ED25519 key. + +```bash +go run ./cmd/cli/ key set --key= +``` + +### endpoint + +Print the current endpoint URL. + +```bash +go run ./cmd/cli/ endpoint +``` + +#### set + +Set the endpoint URL. + +```bash +go run ./cmd/cli/ endpoint set --endpoint=http://localhost:9650/ext/bc/morpheusvm/ +``` + +### ping + +Check connectivity with the current endpoint. + +```bash +go run ./cmd/cli/ ping +``` + +### actions + +Print the list of actions available in the ABI. + +```bash +go run ./cmd/cli/ actions +``` + +For JSON output: + +```bash +go run ./cmd/cli/ actions -o json +``` + +### read + +Simulate a single action transaction. + +```bash +go run ./cmd/cli/ read Transfer --data to=0x000000000000000000000000000000000000000000000000000000000000000000,value=12 +``` + +For interactive input remove --data from the comand line: + +```bash +go run ./cmd/cli/ read Transfer +``` + +### tx + +Send a transaction with a single action. + +```bash +go run ./cmd/cli/ tx Transfer --data to=0x000000000000000000000000000000000000000000000000000000000000000000,value=12,memo= +``` + +For interactive input: ```bash -go run ./cmd/cli/ read Transfer --key=./examples/morpheusvm/demo.pk --data to=0x000000000000000000000000000000000000000000000000000000000000000000,value=12 +go run ./cmd/cli/ tx Transfer ``` -Notes: -- balance request is impossible to implement right now, as we don't have standardized balance RPC method on HyperSDK (not VM) level. -- maxFee is hardcoded to 1_000_000 for now +## Notes +- The `balance` command is not currently implemented due to the lack of a standardized balance RPC method at the HyperSDK level. +- The `maxFee` for transactions is currently hardcoded to 1,000,000. +- Only flat actions are supported. Arrays, slices, embedded structs, maps, and struct fields are not supported. +- The CLI supports ED25519 keys only. +- If `--data` is supplied or JSON output is selected, the CLI will not ask for action arguments interactively. From 0d9e79c1c38c9e0611d29144040a8def8f2f60e3 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 05:20:59 +0000 Subject: [PATCH 29/53] rename package --- cmd/{cli => hypersdk-cli}/README.md | 28 +++++---- cmd/{cli => hypersdk-cli}/actions.go | 0 cmd/{cli => hypersdk-cli}/address.go | 0 cmd/{cli => hypersdk-cli}/config.go | 0 cmd/{cli => hypersdk-cli}/endpoint.go | 0 cmd/{cli => hypersdk-cli}/endpoint_set.go | 0 cmd/{cli => hypersdk-cli}/key.go | 0 cmd/{cli => hypersdk-cli}/key_set.go | 0 cmd/{cli => hypersdk-cli}/main.go | 0 cmd/{cli => hypersdk-cli}/ping.go | 0 cmd/{cli => hypersdk-cli}/read.go | 0 cmd/{cli => hypersdk-cli}/transaction.go | 76 +++++++++++++++-------- 12 files changed, 66 insertions(+), 38 deletions(-) rename cmd/{cli => hypersdk-cli}/README.md (70%) rename cmd/{cli => hypersdk-cli}/actions.go (100%) rename cmd/{cli => hypersdk-cli}/address.go (100%) rename cmd/{cli => hypersdk-cli}/config.go (100%) rename cmd/{cli => hypersdk-cli}/endpoint.go (100%) rename cmd/{cli => hypersdk-cli}/endpoint_set.go (100%) rename cmd/{cli => hypersdk-cli}/key.go (100%) rename cmd/{cli => hypersdk-cli}/key_set.go (100%) rename cmd/{cli => hypersdk-cli}/main.go (100%) rename cmd/{cli => hypersdk-cli}/ping.go (100%) rename cmd/{cli => hypersdk-cli}/read.go (100%) rename cmd/{cli => hypersdk-cli}/transaction.go (73%) diff --git a/cmd/cli/README.md b/cmd/hypersdk-cli/README.md similarity index 70% rename from cmd/cli/README.md rename to cmd/hypersdk-cli/README.md index 6a2fa283b8..ec4c15018e 100644 --- a/cmd/cli/README.md +++ b/cmd/hypersdk-cli/README.md @@ -4,7 +4,9 @@ A command-line interface for interacting with HyperSDK-based chains. ## Installation -Just `go run ./cmd/cli/ ` for now +```bash +go install github.com/ava-labs/hypersdk/cmd/hypersdk-cli@cli +``` ## Configuration @@ -24,7 +26,7 @@ The CLI stores configuration in `~/.hypersdk-cli/config.cfg`. This includes: Print the current key address. ```bash -go run ./cmd/cli/ address +hypersdk-cli address ``` ### key @@ -36,7 +38,7 @@ Manage keys. Generate a new ED25519 key pair. ```bash -go run ./cmd/cli/ key generate +hypersdk-cli key generate ``` #### set @@ -44,7 +46,7 @@ go run ./cmd/cli/ key generate Set the private ED25519 key. ```bash -go run ./cmd/cli/ key set --key= +hypersdk-cli key set --key= ``` ### endpoint @@ -52,7 +54,7 @@ go run ./cmd/cli/ key set --key= Print the current endpoint URL. ```bash -go run ./cmd/cli/ endpoint +hypersdk-cli endpoint ``` #### set @@ -60,7 +62,7 @@ go run ./cmd/cli/ endpoint Set the endpoint URL. ```bash -go run ./cmd/cli/ endpoint set --endpoint=http://localhost:9650/ext/bc/morpheusvm/ +hypersdk-cli endpoint set --endpoint=http://localhost:9650/ext/bc/morpheusvm/ ``` ### ping @@ -68,7 +70,7 @@ go run ./cmd/cli/ endpoint set --endpoint=http://localhost:9650/ext/bc/morpheusv Check connectivity with the current endpoint. ```bash -go run ./cmd/cli/ ping +hypersdk-cli ping ``` ### actions @@ -76,13 +78,13 @@ go run ./cmd/cli/ ping Print the list of actions available in the ABI. ```bash -go run ./cmd/cli/ actions +hypersdk-cli actions ``` For JSON output: ```bash -go run ./cmd/cli/ actions -o json +hypersdk-cli actions -o json ``` ### read @@ -90,13 +92,13 @@ go run ./cmd/cli/ actions -o json Simulate a single action transaction. ```bash -go run ./cmd/cli/ read Transfer --data to=0x000000000000000000000000000000000000000000000000000000000000000000,value=12 +hypersdk-cli read Transfer --data to=0x000000000000000000000000000000000000000000000000000000000000000000,value=12 ``` For interactive input remove --data from the comand line: ```bash -go run ./cmd/cli/ read Transfer +hypersdk-cli read Transfer ``` ### tx @@ -104,13 +106,13 @@ go run ./cmd/cli/ read Transfer Send a transaction with a single action. ```bash -go run ./cmd/cli/ tx Transfer --data to=0x000000000000000000000000000000000000000000000000000000000000000000,value=12,memo= +hypersdk-cli tx Transfer --data to=0x000000000000000000000000000000000000000000000000000000000000000000,value=12,memo= ``` For interactive input: ```bash -go run ./cmd/cli/ tx Transfer +hypersdk-cli tx Transfer ``` ## Notes diff --git a/cmd/cli/actions.go b/cmd/hypersdk-cli/actions.go similarity index 100% rename from cmd/cli/actions.go rename to cmd/hypersdk-cli/actions.go diff --git a/cmd/cli/address.go b/cmd/hypersdk-cli/address.go similarity index 100% rename from cmd/cli/address.go rename to cmd/hypersdk-cli/address.go diff --git a/cmd/cli/config.go b/cmd/hypersdk-cli/config.go similarity index 100% rename from cmd/cli/config.go rename to cmd/hypersdk-cli/config.go diff --git a/cmd/cli/endpoint.go b/cmd/hypersdk-cli/endpoint.go similarity index 100% rename from cmd/cli/endpoint.go rename to cmd/hypersdk-cli/endpoint.go diff --git a/cmd/cli/endpoint_set.go b/cmd/hypersdk-cli/endpoint_set.go similarity index 100% rename from cmd/cli/endpoint_set.go rename to cmd/hypersdk-cli/endpoint_set.go diff --git a/cmd/cli/key.go b/cmd/hypersdk-cli/key.go similarity index 100% rename from cmd/cli/key.go rename to cmd/hypersdk-cli/key.go diff --git a/cmd/cli/key_set.go b/cmd/hypersdk-cli/key_set.go similarity index 100% rename from cmd/cli/key_set.go rename to cmd/hypersdk-cli/key_set.go diff --git a/cmd/cli/main.go b/cmd/hypersdk-cli/main.go similarity index 100% rename from cmd/cli/main.go rename to cmd/hypersdk-cli/main.go diff --git a/cmd/cli/ping.go b/cmd/hypersdk-cli/ping.go similarity index 100% rename from cmd/cli/ping.go rename to cmd/hypersdk-cli/ping.go diff --git a/cmd/cli/read.go b/cmd/hypersdk-cli/read.go similarity index 100% rename from cmd/cli/read.go rename to cmd/hypersdk-cli/read.go diff --git a/cmd/cli/transaction.go b/cmd/hypersdk-cli/transaction.go similarity index 73% rename from cmd/cli/transaction.go rename to cmd/hypersdk-cli/transaction.go index 85ea30d173..3f8b7918c0 100644 --- a/cmd/cli/transaction.go +++ b/cmd/hypersdk-cli/transaction.go @@ -4,9 +4,10 @@ import ( "context" "encoding/json" "fmt" - "log" + "strings" "time" + "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/hypersdk/abi/dynamic" "github.com/ava-labs/hypersdk/api/jsonrpc" "github.com/ava-labs/hypersdk/api/ws" @@ -26,7 +27,7 @@ var txCmd = &cobra.Command{ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - //1. figure out sender address + //1. Decode key keyString, err := getConfigValue(cmd, "key") if err != nil { return fmt.Errorf("failed to get key from config: %w", err) @@ -87,7 +88,7 @@ var txCmd = &cobra.Command{ base := &chain.Base{ ChainID: chainID, - Timestamp: time.Now().Unix()*1000 + 59000, + Timestamp: time.Now().Unix()*1000 + 60*1000, MaxFee: 1_000_000, } @@ -96,15 +97,6 @@ var txCmd = &cobra.Command{ return fmt.Errorf("failed to sign tx: %w", err) } - // signedBytesOverrideHex := "00000192bcaf8728bc268cf27206903fbd7975e22f25a6190b8b7666858744aa8803c5d3e4997a56000000000000c0940200010203040000000000000000000000000000000000000000000000000000000000000000003b9aca000000000b74657374206d656d6f20310005060708000000000000000000000000000000000000000000000000000000000000000000773594000000000b74657374206d656d6f2032001b057de320297c29ad0c1f589ea216869cf1938d88c9fbd70d6748323dbf2fa70d9c361e5e0b495cbf2c5a2a1b68a71f01cba00062ee417ee8ea4652b40a8f953695db3ed217be20d202fe1ff053b7ad3e557c92dc3125d5c4c953673ed62a04" - - // if signedBytesOverrideHex != "" { - // signedBytes, err = hex.DecodeString(signedBytesOverrideHex) - // if err != nil { - // return fmt.Errorf("failed to decode signedBytesOverrideHex: %w", err) - // } - // } - wsClient, err := ws.NewWebSocketClient(endpoint, 10*time.Second, 100, 1000000) if err != nil { return fmt.Errorf("failed to create ws client: %w", err) @@ -115,14 +107,10 @@ var txCmd = &cobra.Command{ } expectedTxID := utils.ToID(signedBytes) - - fmt.Println("expectedTxID", expectedTxID) - // Listen for the transaction result var result *chain.Result for { txID, txErr, txResult, err := wsClient.ListenTx(ctx) - fmt.Println("txID", txID) if err != nil { if ctx.Err() == context.DeadlineExceeded { return fmt.Errorf("failed to listen for transaction: %w", ctx.Err()) @@ -136,22 +124,60 @@ var txCmd = &cobra.Command{ result = txResult break } - log.Printf("Skipping unexpected transaction: %s\n", txID) } - // Check transaction result - if !result.Success { - return fmt.Errorf("transaction failed: %s", result.Error) - } + var resultStruct map[string]interface{} + if result.Success { + if len(result.Outputs) == 1 { + resultJSON, err := dynamic.UnmarshalOutput(abi, result.Outputs[0]) + if err != nil { + return fmt.Errorf("failed to unmarshal result: %w", err) + } - //TODO: decode outputs - //TODO: print outputs - //TODO: print errors as json/stringer + err = json.Unmarshal([]byte(resultJSON), &resultStruct) + if err != nil { + return fmt.Errorf("failed to unmarshal result JSON: %w", err) + } + } else if len(result.Outputs) > 1 { + return fmt.Errorf("expected 1 output, got %d", len(result.Outputs)) + } + } - return nil + return printValue(cmd, txResponse{ + Result: resultStruct, + Success: result.Success, + TxID: expectedTxID, + Error: string(result.Error), + }) }, } +type txResponse struct { + Result map[string]interface{} `json:"result"` + Success bool `json:"success"` + TxID ids.ID `json:"txId"` + Error string `json:"error"` +} + +func (r txResponse) String() string { + var result strings.Builder + if r.Success { + result.WriteString(fmt.Sprintf("✅ Transaction successful (txID: %s)\n\n", r.TxID)) + if r.Result != nil { + for key, value := range r.Result { + jsonValue, err := json.Marshal(value) + if err != nil { + jsonValue = []byte(fmt.Sprintf("%v", value)) + } + result.WriteString(fmt.Sprintf("%s: %s\n", key, string(jsonValue))) + } + } + } else { + result.WriteString(fmt.Sprintf("❌ Transaction failed (txID: %s): %s\n", r.TxID, r.Error)) + } + return strings.TrimSpace(result.String()) +} + func init() { txCmd.Flags().StringToString("data", nil, "Key-value pairs for the action data (e.g., key1=value1,key2=value2)") rootCmd.AddCommand(txCmd) From 6c764e48ac613b5c5d4935eda003481ac87d17d8 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 05:29:11 +0000 Subject: [PATCH 30/53] update readme --- cmd/hypersdk-cli/README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cmd/hypersdk-cli/README.md b/cmd/hypersdk-cli/README.md index ec4c15018e..ad9f5f04b2 100644 --- a/cmd/hypersdk-cli/README.md +++ b/cmd/hypersdk-cli/README.md @@ -5,9 +5,11 @@ A command-line interface for interacting with HyperSDK-based chains. ## Installation ```bash -go install github.com/ava-labs/hypersdk/cmd/hypersdk-cli@cli +go install github.com/ava-labs/hypersdk/cmd/hypersdk-cli@0d9e79c1c38c9e0611d29144040a8def8f2f60e3 ``` +TODO: Has to be @latest + ## Configuration The CLI stores configuration in `~/.hypersdk-cli/config.cfg`. This includes: @@ -43,12 +45,14 @@ hypersdk-cli key generate #### set -Set the private ED25519 key. +Set the private ED25519 key. ```bash -hypersdk-cli key set --key= +hypersdk-cli key set --key=0x323b1d8f4eed5f0da9da93071b034f2dce9d2d22692c172f3cb252a64ddfafd01b057de320297c29ad0c1f589ea216869cf1938d88c9fbd70d6748323dbf2fa7 ``` +`--key` could also be file path like `examples/morpheusvm/demo.pk` + ### endpoint Print the current endpoint URL. From 978190ad1ca7a05cffc7864ae7ff87e35acf92f9 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 05:37:19 +0000 Subject: [PATCH 31/53] return uint and linmt --- cli/prompt/prompt.go | 22 ++++++++++++++-------- cli/spam.go | 10 +++++----- cmd/hypersdk-cli/actions.go | 6 +++++- cmd/hypersdk-cli/address.go | 6 +++++- cmd/hypersdk-cli/config.go | 10 +++++++--- cmd/hypersdk-cli/endpoint.go | 3 +++ cmd/hypersdk-cli/endpoint_set.go | 6 +++++- cmd/hypersdk-cli/key.go | 3 +++ cmd/hypersdk-cli/key_set.go | 8 ++++++-- cmd/hypersdk-cli/main.go | 3 +++ cmd/hypersdk-cli/ping.go | 6 +++++- cmd/hypersdk-cli/read.go | 18 +++++++++++------- cmd/hypersdk-cli/transaction.go | 16 ++++++++++------ examples/morpheusvm/tests/sign_tx_test.go | 13 +++++++------ 14 files changed, 89 insertions(+), 41 deletions(-) diff --git a/cli/prompt/prompt.go b/cli/prompt/prompt.go index 0d3a5e37c9..a18e1468f2 100644 --- a/cli/prompt/prompt.go +++ b/cli/prompt/prompt.go @@ -147,17 +147,18 @@ func Amount( rawAmount = strings.TrimSpace(rawAmount) return utils.ParseBalance(rawAmount) } + func Int( label string, - max int64, -) (int64, error) { + max int, +) (int, error) { promptText := promptui.Prompt{ Label: label, Validate: func(input string) error { if len(input) == 0 { return ErrInputEmpty } - amount, err := strconv.ParseInt(input, 10, 64) + amount, err := strconv.Atoi(input) if err != nil { return err } @@ -175,23 +176,24 @@ func Int( return 0, err } rawAmount = strings.TrimSpace(rawAmount) - return strconv.ParseInt(rawAmount, 10, 64) + return strconv.Atoi(rawAmount) } func Uint( label string, - max uint64, -) (uint64, error) { + max uint, +) (uint, error) { promptText := promptui.Prompt{ Label: label, Validate: func(input string) error { if len(input) == 0 { return ErrInputEmpty } - amount, err := strconv.ParseUint(input, 10, 64) + amount64, err := strconv.ParseUint(input, 10, 64) if err != nil { return err } + amount := uint(amount64) if amount > max { return fmt.Errorf("%d must be <= %d", amount, max) } @@ -203,7 +205,11 @@ func Uint( return 0, err } rawAmount = strings.TrimSpace(rawAmount) - return strconv.ParseUint(rawAmount, 10, 64) + amount64, err := strconv.ParseUint(rawAmount, 10, 64) + if err != nil { + return 0, err + } + return uint(amount64), nil } func Float( diff --git a/cli/spam.go b/cli/spam.go index 58ab890b18..53e05d7931 100644 --- a/cli/spam.go +++ b/cli/spam.go @@ -48,7 +48,7 @@ func (h *Handler) BuildSpammer(sh throughput.SpamHelper, defaults bool) (*throug return throughput.NewSpammer(sc, sh) } // Collect parameters - numAccounts, err := prompt.Int("number of accounts", int64(consts.MaxInt)) + numAccounts, err := prompt.Int("number of accounts", consts.MaxInt) if err != nil { return nil, err } @@ -64,19 +64,19 @@ func (h *Handler) BuildSpammer(sh throughput.SpamHelper, defaults bool) (*throug return nil, err } - txsPerSecond, err := prompt.Int("txs to try and issue per second", int64(consts.MaxInt)) + txsPerSecond, err := prompt.Int("txs to try and issue per second", consts.MaxInt) if err != nil { return nil, err } - minTxsPerSecond, err := prompt.Int("minimum txs to issue per second", int64(consts.MaxInt)) + minTxsPerSecond, err := prompt.Int("minimum txs to issue per second", consts.MaxInt) if err != nil { return nil, err } - txsPerSecondStep, err := prompt.Int("txs to increase per second", int64(consts.MaxInt)) + txsPerSecondStep, err := prompt.Int("txs to increase per second", consts.MaxInt) if err != nil { return nil, err } - numClients, err := prompt.Int("number of clients per node", int64(consts.MaxInt)) + numClients, err := prompt.Int("number of clients per node", consts.MaxInt) if err != nil { return nil, err } diff --git a/cmd/hypersdk-cli/actions.go b/cmd/hypersdk-cli/actions.go index eb7fb94bda..5c623e5580 100644 --- a/cmd/hypersdk-cli/actions.go +++ b/cmd/hypersdk-cli/actions.go @@ -1,12 +1,16 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + package main import ( "context" "fmt" + "github.com/spf13/cobra" + "github.com/ava-labs/hypersdk/abi" "github.com/ava-labs/hypersdk/api/jsonrpc" - "github.com/spf13/cobra" ) var actionsCmd = &cobra.Command{ diff --git a/cmd/hypersdk-cli/address.go b/cmd/hypersdk-cli/address.go index 1fac19b6a4..d152e83cbc 100644 --- a/cmd/hypersdk-cli/address.go +++ b/cmd/hypersdk-cli/address.go @@ -1,10 +1,14 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + package main import ( "fmt" - "github.com/ava-labs/hypersdk/auth" "github.com/spf13/cobra" + + "github.com/ava-labs/hypersdk/auth" ) var addressCmd = &cobra.Command{ diff --git a/cmd/hypersdk-cli/config.go b/cmd/hypersdk-cli/config.go index 12ebc97e76..24b01bd196 100644 --- a/cmd/hypersdk-cli/config.go +++ b/cmd/hypersdk-cli/config.go @@ -1,3 +1,6 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + package main import ( @@ -7,8 +10,9 @@ import ( "path/filepath" "strings" - "github.com/ava-labs/hypersdk/codec" "github.com/spf13/cobra" + + "github.com/ava-labs/hypersdk/codec" ) func isJSONOutputRequested(cmd *cobra.Command) (bool, error) { @@ -101,7 +105,7 @@ func writeConfig(config map[string]string) error { } configDir := filepath.Join(homeDir, ".hypersdk-cli") - if err := os.MkdirAll(configDir, 0755); err != nil { + if err := os.MkdirAll(configDir, 0o755); err != nil { return fmt.Errorf("failed to create config directory: %w", err) } @@ -111,7 +115,7 @@ func writeConfig(config map[string]string) error { buf.WriteString(fmt.Sprintf("%s = %s\n", key, value)) } - if err := os.WriteFile(configPath, []byte(buf.String()), 0644); err != nil { + if err := os.WriteFile(configPath, []byte(buf.String()), 0o644); err != nil { return fmt.Errorf("failed to write config file: %w", err) } diff --git a/cmd/hypersdk-cli/endpoint.go b/cmd/hypersdk-cli/endpoint.go index 0c5807cfa3..2c8308a502 100644 --- a/cmd/hypersdk-cli/endpoint.go +++ b/cmd/hypersdk-cli/endpoint.go @@ -1,3 +1,6 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + package main import ( diff --git a/cmd/hypersdk-cli/endpoint_set.go b/cmd/hypersdk-cli/endpoint_set.go index 58168cb2be..3d7803da4b 100644 --- a/cmd/hypersdk-cli/endpoint_set.go +++ b/cmd/hypersdk-cli/endpoint_set.go @@ -1,11 +1,15 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + package main import ( "context" "fmt" - "github.com/ava-labs/hypersdk/api/jsonrpc" "github.com/spf13/cobra" + + "github.com/ava-labs/hypersdk/api/jsonrpc" ) var endpointSetCmd = &cobra.Command{ diff --git a/cmd/hypersdk-cli/key.go b/cmd/hypersdk-cli/key.go index 964ac55808..d2f48c5064 100644 --- a/cmd/hypersdk-cli/key.go +++ b/cmd/hypersdk-cli/key.go @@ -1,3 +1,6 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + package main import ( diff --git a/cmd/hypersdk-cli/key_set.go b/cmd/hypersdk-cli/key_set.go index c1613d0a42..406dd0d003 100644 --- a/cmd/hypersdk-cli/key_set.go +++ b/cmd/hypersdk-cli/key_set.go @@ -1,12 +1,16 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + package main import ( "encoding/hex" "fmt" + "github.com/spf13/cobra" + "github.com/ava-labs/hypersdk/auth" "github.com/ava-labs/hypersdk/crypto/ed25519" - "github.com/spf13/cobra" ) var keyGenerateCmd = &cobra.Command{ @@ -26,7 +30,7 @@ var keySetCmd = &cobra.Command{ Use: "set", Short: "Set the private ED25519 key", RunE: func(cmd *cobra.Command, args []string) error { - //read directly from the flag instead of calling getConfigValue + // read directly from the flag instead of calling getConfigValue keyString, err := cmd.Flags().GetString("key") if err != nil { return fmt.Errorf("failed to get key: %w", err) diff --git a/cmd/hypersdk-cli/main.go b/cmd/hypersdk-cli/main.go index 71bbf5f8af..822fe820a9 100644 --- a/cmd/hypersdk-cli/main.go +++ b/cmd/hypersdk-cli/main.go @@ -1,3 +1,6 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + package main import ( diff --git a/cmd/hypersdk-cli/ping.go b/cmd/hypersdk-cli/ping.go index 86e1b1ae26..a9c9972fd4 100644 --- a/cmd/hypersdk-cli/ping.go +++ b/cmd/hypersdk-cli/ping.go @@ -1,11 +1,15 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + package main import ( "context" "fmt" - "github.com/ava-labs/hypersdk/api/jsonrpc" "github.com/spf13/cobra" + + "github.com/ava-labs/hypersdk/api/jsonrpc" ) var endpointPingCmd = &cobra.Command{ diff --git a/cmd/hypersdk-cli/read.go b/cmd/hypersdk-cli/read.go index 429c072b0d..2c2599a80a 100644 --- a/cmd/hypersdk-cli/read.go +++ b/cmd/hypersdk-cli/read.go @@ -1,3 +1,6 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + package main import ( @@ -8,20 +11,21 @@ import ( "strconv" "strings" + "github.com/spf13/cobra" + "github.com/ava-labs/hypersdk/abi" "github.com/ava-labs/hypersdk/abi/dynamic" "github.com/ava-labs/hypersdk/api/jsonrpc" "github.com/ava-labs/hypersdk/auth" "github.com/ava-labs/hypersdk/cli/prompt" "github.com/ava-labs/hypersdk/codec" - "github.com/spf13/cobra" ) var readCmd = &cobra.Command{ Use: "read [action]", Short: "Read data from the chain", RunE: func(cmd *cobra.Command, args []string) error { - //1. figure out sender address + // 1. figure out sender address senderStr, err := cmd.Flags().GetString("sender") if err != nil { return fmt.Errorf("failed to get sender: %w", err) @@ -35,7 +39,7 @@ var readCmd = &cobra.Command{ return fmt.Errorf("failed to convert sender to address: %w", err) } } else { - //ok, infer user's address from the private key + // ok, infer user's address from the private key keyString, err := getConfigValue(cmd, "key") if err != nil { return fmt.Errorf("failed to get key from config: %w", err) @@ -47,20 +51,20 @@ var readCmd = &cobra.Command{ sender = auth.NewED25519Address(key.PublicKey()) } - //2. create client + // 2. create client endpoint, err := getConfigValue(cmd, "endpoint") if err != nil { return fmt.Errorf("failed to get endpoint: %w", err) } client := jsonrpc.NewJSONRPCClient(endpoint) - //3. get abi + // 3. get abi abi, err := client.GetABI(context.Background()) if err != nil { return fmt.Errorf("failed to get abi: %w", err) } - //4. get action name from args + // 4. get action name from args if len(args) == 0 { return fmt.Errorf("action name is required") } @@ -75,7 +79,7 @@ var readCmd = &cobra.Command{ return fmt.Errorf("failed to find type: %s", actionName) } - //5. create action using kvPairs + // 5. create action using kvPairs kvPairs, err := fillAction(cmd, typ) if err != nil { return err diff --git a/cmd/hypersdk-cli/transaction.go b/cmd/hypersdk-cli/transaction.go index 3f8b7918c0..77304001a0 100644 --- a/cmd/hypersdk-cli/transaction.go +++ b/cmd/hypersdk-cli/transaction.go @@ -1,3 +1,6 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + package main import ( @@ -8,6 +11,8 @@ import ( "time" "github.com/ava-labs/avalanchego/ids" + "github.com/spf13/cobra" + "github.com/ava-labs/hypersdk/abi/dynamic" "github.com/ava-labs/hypersdk/api/jsonrpc" "github.com/ava-labs/hypersdk/api/ws" @@ -17,7 +22,6 @@ import ( "github.com/ava-labs/hypersdk/consts" "github.com/ava-labs/hypersdk/crypto/ed25519" "github.com/ava-labs/hypersdk/utils" - "github.com/spf13/cobra" ) var txCmd = &cobra.Command{ @@ -27,7 +31,7 @@ var txCmd = &cobra.Command{ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - //1. Decode key + // 1. Decode key keyString, err := getConfigValue(cmd, "key") if err != nil { return fmt.Errorf("failed to get key from config: %w", err) @@ -37,20 +41,20 @@ var txCmd = &cobra.Command{ return fmt.Errorf("failed to decode key: %w", err) } - //2. create client + // 2. create client endpoint, err := getConfigValue(cmd, "endpoint") if err != nil { return fmt.Errorf("failed to get endpoint: %w", err) } client := jsonrpc.NewJSONRPCClient(endpoint) - //3. get abi + // 3. get abi abi, err := client.GetABI(ctx) if err != nil { return fmt.Errorf("failed to get abi: %w", err) } - //4. get action name from args + // 4. get action name from args if len(args) == 0 { return fmt.Errorf("action name is required") } @@ -65,7 +69,7 @@ var txCmd = &cobra.Command{ return fmt.Errorf("failed to find type: %s", actionName) } - //5. create action using kvPairs + // 5. create action using kvPairs kvPairs, err := fillAction(cmd, typ) if err != nil { return err diff --git a/examples/morpheusvm/tests/sign_tx_test.go b/examples/morpheusvm/tests/sign_tx_test.go index 390fc087b2..9e3eab8c75 100644 --- a/examples/morpheusvm/tests/sign_tx_test.go +++ b/examples/morpheusvm/tests/sign_tx_test.go @@ -1,3 +1,6 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + package tests import ( @@ -6,18 +9,17 @@ import ( "encoding/json" "testing" - "github.com/ava-labs/hypersdk/abi/dynamic" - "github.com/ava-labs/hypersdk/crypto/ed25519" + "github.com/stretchr/testify/require" + "github.com/ava-labs/hypersdk/abi/dynamic" + "github.com/ava-labs/hypersdk/api/jsonrpc" "github.com/ava-labs/hypersdk/auth" "github.com/ava-labs/hypersdk/chain" "github.com/ava-labs/hypersdk/codec" "github.com/ava-labs/hypersdk/consts" + "github.com/ava-labs/hypersdk/crypto/ed25519" "github.com/ava-labs/hypersdk/examples/morpheusvm/actions" "github.com/ava-labs/hypersdk/examples/morpheusvm/vm" - "github.com/stretchr/testify/require" - - "github.com/ava-labs/hypersdk/api/jsonrpc" ) func TestSignTx(t *testing.T) { @@ -73,7 +75,6 @@ func TestSignTx(t *testing.T) { require.Equal(t, signedBytes, manuallySignedBytes, "signed bytes do not match") require.FailNow(t, hex.EncodeToString(manuallySignedBytes)) - } func SignTxManually(actionsTxBytes [][]byte, base *chain.Base, privateKey ed25519.PrivateKey) ([]byte, error) { From 12ccaed9c4e6eee524cf88be0fa0008082a26b46 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 05:37:27 +0000 Subject: [PATCH 32/53] lint --- abi/dynamic/reflect_marshal.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/abi/dynamic/reflect_marshal.go b/abi/dynamic/reflect_marshal.go index b8c90e383c..bd3986aea5 100644 --- a/abi/dynamic/reflect_marshal.go +++ b/abi/dynamic/reflect_marshal.go @@ -13,11 +13,12 @@ import ( "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" - "golang.org/x/text/cases" - "golang.org/x/text/language" ) var ErrTypeNotFound = errors.New("type not found in ABI") From 1a073a0ab757c8e0604f3c1e150ee2e29f8495be Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 05:37:54 +0000 Subject: [PATCH 33/53] remove extra test --- examples/morpheusvm/tests/sign_tx_test.go | 113 ---------------------- 1 file changed, 113 deletions(-) delete mode 100644 examples/morpheusvm/tests/sign_tx_test.go diff --git a/examples/morpheusvm/tests/sign_tx_test.go b/examples/morpheusvm/tests/sign_tx_test.go deleted file mode 100644 index 9e3eab8c75..0000000000 --- a/examples/morpheusvm/tests/sign_tx_test.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package tests - -import ( - "context" - "encoding/hex" - "encoding/json" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/ava-labs/hypersdk/abi/dynamic" - "github.com/ava-labs/hypersdk/api/jsonrpc" - "github.com/ava-labs/hypersdk/auth" - "github.com/ava-labs/hypersdk/chain" - "github.com/ava-labs/hypersdk/codec" - "github.com/ava-labs/hypersdk/consts" - "github.com/ava-labs/hypersdk/crypto/ed25519" - "github.com/ava-labs/hypersdk/examples/morpheusvm/actions" - "github.com/ava-labs/hypersdk/examples/morpheusvm/vm" -) - -func TestSignTx(t *testing.T) { - keyBytes, err := codec.LoadHex("0x323b1d8f4eed5f0da9da93071b034f2dce9d2d22692c172f3cb252a64ddfafd01b057de320297c29ad0c1f589ea216869cf1938d88c9fbd70d6748323dbf2fa7", -1) - require.NoError(t, err) - - key := ed25519.PrivateKey(keyBytes) - - factory := auth.NewED25519Factory(key) - - action1 := actions.Transfer{ - To: [33]byte{1, 2, 3, 4}, - Value: 1000000000, - Memo: []byte("test memo 1"), - } - - action2 := actions.Transfer{ - To: [33]byte{5, 6, 7, 8}, - Value: 2000000000, - Memo: []byte("test memo 2"), - } - - hyperVMRPC := vm.NewJSONRPCClient("http://localhost:9650/ext/bc/morpheusvm/") - hyperSDKRPC := jsonrpc.NewJSONRPCClient("http://localhost:9650/ext/bc/morpheusvm/") - - parser, err := hyperVMRPC.Parser(context.Background()) - require.NoError(t, err) - - _, tx, _, err := hyperSDKRPC.GenerateTransaction(context.Background(), parser, []chain.Action{&action1, &action2}, factory) - require.NoError(t, err) - - signedBytes := tx.Bytes() - - abi, err := hyperSDKRPC.GetABI(context.Background()) - require.NoError(t, err) - - // Marshal actions to bytes - actionBytes := make([][]byte, 0) - for _, action := range []chain.Action{&action1, &action2} { - jsonPayload, err := json.Marshal(action) - require.NoError(t, err) - - bytes, err := dynamic.Marshal(abi, "Transfer", string(jsonPayload)) - require.NoError(t, err) - actionBytes = append(actionBytes, bytes) - } - - // Use the manual signing function - manuallySignedBytes, err := SignTxManually(actionBytes, tx.Base, key) - require.NoError(t, err) - - // Compare results - require.Equal(t, signedBytes, manuallySignedBytes, "signed bytes do not match") - - require.FailNow(t, hex.EncodeToString(manuallySignedBytes)) -} - -func SignTxManually(actionsTxBytes [][]byte, base *chain.Base, privateKey ed25519.PrivateKey) ([]byte, error) { - // Create auth factory - factory := auth.NewED25519Factory(privateKey) - - // Marshal base - p := codec.NewWriter(base.Size(), consts.NetworkSizeLimit) - base.Marshal(p) - baseBytes := p.Bytes() - - // Build unsigned bytes starting with base and number of actions - unsignedBytes := make([]byte, 0) - unsignedBytes = append(unsignedBytes, baseBytes...) - unsignedBytes = append(unsignedBytes, byte(len(actionsTxBytes))) // Number of actions - - // Append each action's bytes - for _, actionBytes := range actionsTxBytes { - unsignedBytes = append(unsignedBytes, actionBytes...) - } - - // Sign the transaction - auth, err := factory.Sign(unsignedBytes) - if err != nil { - return nil, err - } - - // Marshal auth - p = codec.NewWriter(auth.Size(), consts.NetworkSizeLimit) - auth.Marshal(p) - authBytes := append([]byte{auth.GetTypeID()}, p.Bytes()...) - - // Combine everything into final signed transaction - signedBytes := append(unsignedBytes, authBytes...) - return signedBytes, nil -} From ad6420ee5e0156e4282f7426fc3884d183fd71b7 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 05:47:35 +0000 Subject: [PATCH 34/53] lint --- abi/dynamic/reflect_marshal.go | 2 +- cli/spam.go | 10 +++++----- cmd/hypersdk-cli/README.md | 8 ++++++-- cmd/hypersdk-cli/actions.go | 3 +-- cmd/hypersdk-cli/address.go | 2 +- cmd/hypersdk-cli/config.go | 5 +++-- cmd/hypersdk-cli/endpoint.go | 2 +- cmd/hypersdk-cli/endpoint_set.go | 12 +++++++++--- cmd/hypersdk-cli/key_set.go | 9 +++++---- cmd/hypersdk-cli/ping.go | 2 +- cmd/hypersdk-cli/read.go | 3 ++- cmd/hypersdk-cli/transaction.go | 9 +++++---- 12 files changed, 40 insertions(+), 27 deletions(-) diff --git a/abi/dynamic/reflect_marshal.go b/abi/dynamic/reflect_marshal.go index bd3986aea5..c0ecb2ef6a 100644 --- a/abi/dynamic/reflect_marshal.go +++ b/abi/dynamic/reflect_marshal.go @@ -45,7 +45,7 @@ func Marshal(inputABI abi.ABI, typeName string, jsonData string) ([]byte, error) found := false for _, action := range inputABI.Actions { if action.Name == typeName { - typeID = byte(action.ID) + typeID = action.ID found = true break } diff --git a/cli/spam.go b/cli/spam.go index 53e05d7931..7391394b69 100644 --- a/cli/spam.go +++ b/cli/spam.go @@ -86,11 +86,11 @@ func (h *Handler) BuildSpammer(sh throughput.SpamHelper, defaults bool) (*throug key, sZipf, vZipf, - int(txsPerSecond), - int(minTxsPerSecond), - int(txsPerSecondStep), - int(numClients), - int(numAccounts), + txsPerSecond, + minTxsPerSecond, + txsPerSecondStep, + numClients, + numAccounts, ) return throughput.NewSpammer(sc, sh) diff --git a/cmd/hypersdk-cli/README.md b/cmd/hypersdk-cli/README.md index ad9f5f04b2..e05fe6b43a 100644 --- a/cmd/hypersdk-cli/README.md +++ b/cmd/hypersdk-cli/README.md @@ -121,8 +121,12 @@ hypersdk-cli tx Transfer ## Notes -- The `balance` command is not currently implemented due to the lack of a standardized balance RPC method at the HyperSDK level. -- The `maxFee` for transactions is currently hardcoded to 1,000,000. - Only flat actions are supported. Arrays, slices, embedded structs, maps, and struct fields are not supported. - The CLI supports ED25519 keys only. - If `--data` is supplied or JSON output is selected, the CLI will not ask for action arguments interactively. + +## Known Issues + +- The `balance` command is not currently implemented due to the lack of a standardized balance RPC method at the HyperSDK level. +- The `maxFee` for transactions is currently hardcoded to 1,000,000. +- The `key set` and `endpoint set` commands use a nested command structure which adds unnecessary complexity for a small CLI tool. A flatter command structure would be more appropriate. diff --git a/cmd/hypersdk-cli/actions.go b/cmd/hypersdk-cli/actions.go index 5c623e5580..be270ebee1 100644 --- a/cmd/hypersdk-cli/actions.go +++ b/cmd/hypersdk-cli/actions.go @@ -16,7 +16,7 @@ import ( var actionsCmd = &cobra.Command{ Use: "actions", Short: "Print the list of actions available in the ABI", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { endpoint, err := getConfigValue(cmd, "endpoint") if err != nil { return fmt.Errorf("failed to get endpoint: %w", err) @@ -66,7 +66,6 @@ func (a abiWrapper) String() string { for _, field := range typ.Fields { result += fmt.Sprintf(" %s: %s\n", field.Name, field.Type) } - } return result } diff --git a/cmd/hypersdk-cli/address.go b/cmd/hypersdk-cli/address.go index d152e83cbc..f6b9203756 100644 --- a/cmd/hypersdk-cli/address.go +++ b/cmd/hypersdk-cli/address.go @@ -14,7 +14,7 @@ import ( var addressCmd = &cobra.Command{ Use: "address", Short: "Print current key address", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { keyString, err := getConfigValue(cmd, "key") if err != nil { return fmt.Errorf("failed to get key: %w", err) diff --git a/cmd/hypersdk-cli/config.go b/cmd/hypersdk-cli/config.go index 24b01bd196..bd248497ae 100644 --- a/cmd/hypersdk-cli/config.go +++ b/cmd/hypersdk-cli/config.go @@ -5,6 +5,7 @@ package main import ( "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -115,7 +116,7 @@ func writeConfig(config map[string]string) error { buf.WriteString(fmt.Sprintf("%s = %s\n", key, value)) } - if err := os.WriteFile(configPath, []byte(buf.String()), 0o644); err != nil { + if err := os.WriteFile(configPath, []byte(buf.String()), 0o600); err != nil { return fmt.Errorf("failed to write config file: %w", err) } @@ -131,5 +132,5 @@ func decodeFileOrHex(whatever string) ([]byte, error) { return fileContents, nil } - return nil, fmt.Errorf("unable to decode input as hex, or read as file path") + return nil, errors.New("unable to decode input as hex, or read as file path") } diff --git a/cmd/hypersdk-cli/endpoint.go b/cmd/hypersdk-cli/endpoint.go index 2c8308a502..a461ae9da5 100644 --- a/cmd/hypersdk-cli/endpoint.go +++ b/cmd/hypersdk-cli/endpoint.go @@ -12,7 +12,7 @@ import ( var endpointCmd = &cobra.Command{ Use: "endpoint", Short: "Manage endpoint", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { endpoint, err := getConfigValue(cmd, "endpoint") if err != nil { return fmt.Errorf("failed to get endpoint: %w", err) diff --git a/cmd/hypersdk-cli/endpoint_set.go b/cmd/hypersdk-cli/endpoint_set.go index 3d7803da4b..a4e23cf169 100644 --- a/cmd/hypersdk-cli/endpoint_set.go +++ b/cmd/hypersdk-cli/endpoint_set.go @@ -5,7 +5,9 @@ package main import ( "context" + "errors" "fmt" + "log" "github.com/spf13/cobra" @@ -15,14 +17,14 @@ import ( var endpointSetCmd = &cobra.Command{ Use: "set", Short: "Set the endpoint URL", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { endpoint, err := cmd.Flags().GetString("endpoint") if err != nil { return fmt.Errorf("failed to get endpoint flag: %w", err) } if endpoint == "" { - return fmt.Errorf("endpoint is required") + return errors.New("endpoint is required") } if err := updateConfig("endpoint", endpoint); err != nil { @@ -59,5 +61,9 @@ func (r endpointSetCmdResponse) String() string { func init() { endpointCmd.AddCommand(endpointSetCmd) endpointSetCmd.Flags().String("endpoint", "", "Endpoint URL to set") - endpointSetCmd.MarkFlagRequired("endpoint") + + err := endpointSetCmd.MarkFlagRequired("endpoint") + if err != nil { + log.Fatalf("failed to mark endpoint flag as required: %s", err) + } } diff --git a/cmd/hypersdk-cli/key_set.go b/cmd/hypersdk-cli/key_set.go index 406dd0d003..0e8c90898f 100644 --- a/cmd/hypersdk-cli/key_set.go +++ b/cmd/hypersdk-cli/key_set.go @@ -5,6 +5,7 @@ package main import ( "encoding/hex" + "errors" "fmt" "github.com/spf13/cobra" @@ -16,7 +17,7 @@ import ( var keyGenerateCmd = &cobra.Command{ Use: "generate", Short: "Generate a new key", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { newKey, err := ed25519.GeneratePrivateKey() if err != nil { return fmt.Errorf("failed to generate key: %w", err) @@ -29,14 +30,14 @@ var keyGenerateCmd = &cobra.Command{ var keySetCmd = &cobra.Command{ Use: "set", Short: "Set the private ED25519 key", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { // read directly from the flag instead of calling getConfigValue keyString, err := cmd.Flags().GetString("key") if err != nil { return fmt.Errorf("failed to get key: %w", err) } if keyString == "" { - return fmt.Errorf("--key is required") + return errors.New("--key is required") } return checkAndSavePrivateKey(keyString, cmd) @@ -82,7 +83,7 @@ type keySetCmdResponse struct { } func (r keySetCmdResponse) String() string { - return fmt.Sprintf("✅ Key added successfully!\nAddress: %s", r.Address) + return "✅ Key added successfully!\nAddress: " + r.Address } func init() { diff --git a/cmd/hypersdk-cli/ping.go b/cmd/hypersdk-cli/ping.go index a9c9972fd4..612d82f8a6 100644 --- a/cmd/hypersdk-cli/ping.go +++ b/cmd/hypersdk-cli/ping.go @@ -15,7 +15,7 @@ import ( var endpointPingCmd = &cobra.Command{ Use: "ping", Short: "Ping the endpoint", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { endpoint, err := getConfigValue(cmd, "endpoint") if err != nil { return fmt.Errorf("failed to get endpoint: %w", err) diff --git a/cmd/hypersdk-cli/read.go b/cmd/hypersdk-cli/read.go index 2c2599a80a..7edd4a8b0e 100644 --- a/cmd/hypersdk-cli/read.go +++ b/cmd/hypersdk-cli/read.go @@ -6,6 +6,7 @@ package main import ( "context" "encoding/json" + "errors" "fmt" "math" "strconv" @@ -66,7 +67,7 @@ var readCmd = &cobra.Command{ // 4. get action name from args if len(args) == 0 { - return fmt.Errorf("action name is required") + return errors.New("action name is required") } actionName := args[0] _, found := abi.FindActionByName(actionName) diff --git a/cmd/hypersdk-cli/transaction.go b/cmd/hypersdk-cli/transaction.go index 77304001a0..312f627280 100644 --- a/cmd/hypersdk-cli/transaction.go +++ b/cmd/hypersdk-cli/transaction.go @@ -6,6 +6,7 @@ package main import ( "context" "encoding/json" + "errors" "fmt" "strings" "time" @@ -53,10 +54,9 @@ var txCmd = &cobra.Command{ if err != nil { return fmt.Errorf("failed to get abi: %w", err) } - // 4. get action name from args if len(args) == 0 { - return fmt.Errorf("action name is required") + return errors.New("action name is required") } actionName := args[0] _, found := abi.FindActionByName(actionName) @@ -211,13 +211,14 @@ func SignTxManually(actionsTxBytes [][]byte, base *chain.Base, privateKey ed2551 if err != nil { return nil, err } - // Marshal auth p = codec.NewWriter(auth.Size(), consts.NetworkSizeLimit) auth.Marshal(p) - authBytes := append([]byte{auth.GetTypeID()}, p.Bytes()...) + authBytes := []byte{auth.GetTypeID()} + authBytes = append(authBytes, p.Bytes()...) // Combine everything into final signed transaction + //nolint:gocritic //append is fine here signedBytes := append(unsignedBytes, authBytes...) return signedBytes, nil } From f8cd8e4d31702c83d36dcf4aa2a02dcf57e4a33a Mon Sep 17 00:00:00 2001 From: containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:48:03 +0900 Subject: [PATCH 35/53] Fix code scanning alert no. 114: Incorrect conversion between integer types Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: containerman17 <8990432+containerman17@users.noreply.github.com> --- cli/prompt/prompt.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cli/prompt/prompt.go b/cli/prompt/prompt.go index a18e1468f2..244e46fb0c 100644 --- a/cli/prompt/prompt.go +++ b/cli/prompt/prompt.go @@ -8,6 +8,7 @@ import ( "fmt" "strconv" "strings" + "math" "github.com/ava-labs/avalanchego/ids" "github.com/manifoldco/promptui" @@ -193,6 +194,9 @@ func Uint( if err != nil { return err } + if amount64 > math.MaxUint { + return fmt.Errorf("%d exceeds the maximum value for uint", amount64) + } amount := uint(amount64) if amount > max { return fmt.Errorf("%d must be <= %d", amount, max) From 135bb9e7f493dea3d2213f3c23afd7888f053d15 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 05:48:36 +0000 Subject: [PATCH 36/53] lint --- cli/prompt/prompt.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/prompt/prompt.go b/cli/prompt/prompt.go index 244e46fb0c..9435ddba33 100644 --- a/cli/prompt/prompt.go +++ b/cli/prompt/prompt.go @@ -6,9 +6,9 @@ package prompt import ( "errors" "fmt" + "math" "strconv" "strings" - "math" "github.com/ava-labs/avalanchego/ids" "github.com/manifoldco/promptui" From 2cdcdb8ead9de056216a3c2a85f1e8da1d68d187 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 05:51:33 +0000 Subject: [PATCH 37/53] lint --- cli/prompt/prompt.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/cli/prompt/prompt.go b/cli/prompt/prompt.go index 9435ddba33..3685d7bff8 100644 --- a/cli/prompt/prompt.go +++ b/cli/prompt/prompt.go @@ -6,7 +6,6 @@ package prompt import ( "errors" "fmt" - "math" "strconv" "strings" @@ -190,15 +189,11 @@ func Uint( if len(input) == 0 { return ErrInputEmpty } - amount64, err := strconv.ParseUint(input, 10, 64) + amount, err := strconv.ParseUint(input, 10, 0) if err != nil { return err } - if amount64 > math.MaxUint { - return fmt.Errorf("%d exceeds the maximum value for uint", amount64) - } - amount := uint(amount64) - if amount > max { + if uint(amount) > max { return fmt.Errorf("%d must be <= %d", amount, max) } return nil @@ -209,11 +204,11 @@ func Uint( return 0, err } rawAmount = strings.TrimSpace(rawAmount) - amount64, err := strconv.ParseUint(rawAmount, 10, 64) + amount, err := strconv.ParseUint(rawAmount, 10, 0) if err != nil { return 0, err } - return uint(amount64), nil + return uint(amount), nil } func Float( From 4656625be87bb0be7f54cb209bed0751293d70d6 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 05:57:07 +0000 Subject: [PATCH 38/53] simplify Uint function --- cli/prompt/prompt.go | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/cli/prompt/prompt.go b/cli/prompt/prompt.go index 3685d7bff8..25e3e2d8cf 100644 --- a/cli/prompt/prompt.go +++ b/cli/prompt/prompt.go @@ -6,6 +6,7 @@ package prompt import ( "errors" "fmt" + "math" "strconv" "strings" @@ -183,32 +184,37 @@ func Uint( label string, max uint, ) (uint, error) { + stringToUint := func(input string, max uint) (uint, error) { + input = strings.TrimSpace(input) + + if len(input) == 0 { + return 0, ErrInputEmpty + } + amount, err := strconv.ParseUint(input, 10, 0) + if err != nil { + return 0, err + } + if amount > math.MaxUint { + return 0, fmt.Errorf("%d exceeds the maximum value for uint", amount) + } + if uint(amount) > max { + return 0, fmt.Errorf("%d must be <= %d", amount, max) + } + return uint(amount), nil + } + promptText := promptui.Prompt{ Label: label, Validate: func(input string) error { - if len(input) == 0 { - return ErrInputEmpty - } - amount, err := strconv.ParseUint(input, 10, 0) - if err != nil { - return err - } - if uint(amount) > max { - return fmt.Errorf("%d must be <= %d", amount, max) - } - return nil + _, err := stringToUint(input, max) + return err }, } rawAmount, err := promptText.Run() if err != nil { return 0, err } - rawAmount = strings.TrimSpace(rawAmount) - amount, err := strconv.ParseUint(rawAmount, 10, 0) - if err != nil { - return 0, err - } - return uint(amount), nil + return stringToUint(rawAmount, max) } func Float( From 4510f51720d2e0fdecfd7fa08350e7c3eab3cf53 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 05:58:32 +0000 Subject: [PATCH 39/53] simplify Int --- cli/prompt/prompt.go | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/cli/prompt/prompt.go b/cli/prompt/prompt.go index 25e3e2d8cf..3b3f556e41 100644 --- a/cli/prompt/prompt.go +++ b/cli/prompt/prompt.go @@ -153,31 +153,37 @@ func Int( label string, max int, ) (int, error) { + stringToInt := func(input string, max int) (int, error) { + input = strings.TrimSpace(input) + + if len(input) == 0 { + return 0, ErrInputEmpty + } + amount, err := strconv.Atoi(input) + if err != nil { + return 0, err + } + if amount <= 0 { + return 0, fmt.Errorf("%d must be > 0", amount) + } + if amount > max { + return 0, fmt.Errorf("%d must be <= %d", amount, max) + } + return amount, nil + } + promptText := promptui.Prompt{ Label: label, Validate: func(input string) error { - if len(input) == 0 { - return ErrInputEmpty - } - amount, err := strconv.Atoi(input) - if err != nil { - return err - } - if amount <= 0 { - return fmt.Errorf("%d must be > 0", amount) - } - if amount > max { - return fmt.Errorf("%d must be <= %d", amount, max) - } - return nil + _, err := stringToInt(input, max) + return err }, } rawAmount, err := promptText.Run() if err != nil { return 0, err } - rawAmount = strings.TrimSpace(rawAmount) - return strconv.Atoi(rawAmount) + return stringToInt(rawAmount, max) } func Uint( From 59dcdaa9bef16e5d60f555032b711b7d02f76e02 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 06:03:32 +0000 Subject: [PATCH 40/53] readme update --- cmd/hypersdk-cli/README.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/cmd/hypersdk-cli/README.md b/cmd/hypersdk-cli/README.md index e05fe6b43a..d31321b02c 100644 --- a/cmd/hypersdk-cli/README.md +++ b/cmd/hypersdk-cli/README.md @@ -5,10 +5,10 @@ A command-line interface for interacting with HyperSDK-based chains. ## Installation ```bash -go install github.com/ava-labs/hypersdk/cmd/hypersdk-cli@0d9e79c1c38c9e0611d29144040a8def8f2f60e3 +go install github.com/ava-labs/hypersdk/cmd/hypersdk-cli@4510f51720d2e0fdecfd7fa08350e7c3eab3cf53 ``` -TODO: Has to be @latest +FIXME: Has to point to the commit with the latest update from main, or just `@main` later on. ## Configuration @@ -23,13 +23,6 @@ The CLI stores configuration in `~/.hypersdk-cli/config.cfg`. This includes: ## Commands -### address - -Print the current key address. - -```bash -hypersdk-cli address -``` ### key @@ -77,6 +70,14 @@ Check connectivity with the current endpoint. hypersdk-cli ping ``` +### address + +Print the current key address. + +```bash +hypersdk-cli address +``` + ### actions Print the list of actions available in the ABI. @@ -110,7 +111,7 @@ hypersdk-cli read Transfer Send a transaction with a single action. ```bash -hypersdk-cli tx Transfer --data to=0x000000000000000000000000000000000000000000000000000000000000000000,value=12,memo= +hypersdk-cli tx Transfer --data to=0x000000000000000000000000000000000000000000000000000000000000000000,value=12,memo=0x001234 ``` For interactive input: @@ -130,3 +131,4 @@ hypersdk-cli tx Transfer - The `balance` command is not currently implemented due to the lack of a standardized balance RPC method at the HyperSDK level. - The `maxFee` for transactions is currently hardcoded to 1,000,000. - The `key set` and `endpoint set` commands use a nested command structure which adds unnecessary complexity for a small CLI tool. A flatter command structure would be more appropriate. +- Currency values are represented as uint64 without decimal point support in the ABI. The CLI cannot automatically parse decimal inputs (e.g. "12.0") since there is no currency type annotation. Users must enter the raw uint64 value including all decimal places (e.g. "12000000000" for 12 coins with 9 decimal places). From 20ca2a645891dd9bc70edee189d334d8e5ebe825 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 06:21:05 +0000 Subject: [PATCH 41/53] reverse consts --- consts/consts.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/consts/consts.go b/consts/consts.go index 00afcfeab9..96d6419d37 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -21,13 +21,14 @@ const ( // no more than 50 KiB of overhead but is likely much less) NetworkSizeLimit = 2_044_723 // 1.95 MiB - MaxUint8 = math.MaxUint8 - MaxUint16 = math.MaxUint16 + // FIXME: should use the standard math.MaxUint8, etc. + MaxUint8 = ^uint8(0) + MaxUint16 = ^uint16(0) MaxUint8Offset = 7 - MaxUint = math.MaxUint - MaxInt = int(math.MaxUint >> 1) + MaxUint = ^uint(0) + MaxInt = int(MaxUint >> 1) MaxUint64Offset = 63 - MaxUint64 = math.MaxUint64 + MaxUint64 = ^uint64(0) MaxFloat64 = math.MaxFloat64 MillisecondsPerSecond = 1000 Decimals = 9 From 45266e338385fe0f0eadc3c9bb3657483a993a3c Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 06:22:22 +0000 Subject: [PATCH 42/53] lint --- internal/window/window_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/window/window_test.go b/internal/window/window_test.go index 550a69105f..d0ddade09e 100644 --- a/internal/window/window_test.go +++ b/internal/window/window_test.go @@ -116,12 +116,12 @@ func TestUint64WindowOverflow(t *testing.T) { } sum := Sum(uint64Window) - require.Equal(uint64(consts.MaxUint64), sum) + require.Equal(consts.MaxUint64, sum) for i := 0; i < 10; i++ { Update(&uint64Window, i*8, uint64(i)) sum = Sum(uint64Window) - require.Equal(uint64(consts.MaxUint64), sum) + require.Equal(consts.MaxUint64, sum) } } From fcea8957561424effe0cf64ecf3d4574fffcb085 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 06:30:55 +0000 Subject: [PATCH 43/53] remove InvalidAddress --- examples/morpheusvm/actions/transfer_test.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/examples/morpheusvm/actions/transfer_test.go b/examples/morpheusvm/actions/transfer_test.go index e232797587..972b2801a4 100644 --- a/examples/morpheusvm/actions/transfer_test.go +++ b/examples/morpheusvm/actions/transfer_test.go @@ -31,16 +31,6 @@ func TestTransferAction(t *testing.T) { }, ExpectedErr: ErrOutputValueZero, }, - { - Name: "InvalidAddress", - Actor: codec.EmptyAddress, - Action: &Transfer{ - To: codec.EmptyAddress, - Value: 1, - }, - State: chaintest.NewInMemoryStore(), - ExpectedErr: storage.ErrInvalidAddress, - }, { Name: "NotEnoughBalance", Actor: codec.EmptyAddress, From 4269a1bfa87cf710ef58b10721ed500d27fca4ee Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Thu, 24 Oct 2024 06:34:43 +0000 Subject: [PATCH 44/53] bring back the NonExistentAddress test --- examples/morpheusvm/actions/transfer_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/examples/morpheusvm/actions/transfer_test.go b/examples/morpheusvm/actions/transfer_test.go index 972b2801a4..7625ee7bf4 100644 --- a/examples/morpheusvm/actions/transfer_test.go +++ b/examples/morpheusvm/actions/transfer_test.go @@ -31,6 +31,16 @@ func TestTransferAction(t *testing.T) { }, ExpectedErr: ErrOutputValueZero, }, + { + Name: "NonExistentAddress", + Actor: codec.EmptyAddress, + Action: &Transfer{ + To: codec.EmptyAddress, + Value: 1, + }, + State: chaintest.NewInMemoryStore(), + ExpectedErr: storage.ErrInvalidBalance, + }, { Name: "NotEnoughBalance", Actor: codec.EmptyAddress, From dfc4900bb9211b7c65d32bb8e71c754c37f9c1ab Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Fri, 25 Oct 2024 06:23:21 +0000 Subject: [PATCH 45/53] (optional nit) maybe text instead of human? --- cmd/hypersdk-cli/README.md | 2 +- cmd/hypersdk-cli/main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/hypersdk-cli/README.md b/cmd/hypersdk-cli/README.md index d31321b02c..8cf9796a6b 100644 --- a/cmd/hypersdk-cli/README.md +++ b/cmd/hypersdk-cli/README.md @@ -19,7 +19,7 @@ The CLI stores configuration in `~/.hypersdk-cli/config.cfg`. This includes: ## Global Flags - `--endpoint`: Override the default endpoint for a single command -- `-o, --output`: Set output format (`human` or `json`) +- `-o, --output`: Set output format (`text` or `json`) ## Commands diff --git a/cmd/hypersdk-cli/main.go b/cmd/hypersdk-cli/main.go index 822fe820a9..185c8c19df 100644 --- a/cmd/hypersdk-cli/main.go +++ b/cmd/hypersdk-cli/main.go @@ -25,7 +25,7 @@ func Execute() { } func init() { - rootCmd.PersistentFlags().StringP("output", "o", "human", "Output format (human or json)") + rootCmd.PersistentFlags().StringP("output", "o", "text", "Output format (text or json)") rootCmd.PersistentFlags().String("endpoint", "", "Override the default endpoint") rootCmd.PersistentFlags().String("key", "", "Private ED25519 key as hex string") } From 5170175786b50de05002e2d2dbebeefeadb49ee1 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Fri, 25 Oct 2024 06:34:19 +0000 Subject: [PATCH 46/53] switch from reading the config manually to using cobra/viper to read in a config file at the default location --- cmd/hypersdk-cli/README.md | 8 ++- cmd/hypersdk-cli/actions.go | 2 +- cmd/hypersdk-cli/address.go | 2 +- cmd/hypersdk-cli/config.go | 118 +++++++++++++------------------ cmd/hypersdk-cli/endpoint.go | 2 +- cmd/hypersdk-cli/endpoint_set.go | 20 +----- cmd/hypersdk-cli/key_set.go | 14 ++-- cmd/hypersdk-cli/ping.go | 2 +- cmd/hypersdk-cli/read.go | 4 +- cmd/hypersdk-cli/transaction.go | 4 +- 10 files changed, 76 insertions(+), 100 deletions(-) diff --git a/cmd/hypersdk-cli/README.md b/cmd/hypersdk-cli/README.md index 8cf9796a6b..93d790a951 100644 --- a/cmd/hypersdk-cli/README.md +++ b/cmd/hypersdk-cli/README.md @@ -12,10 +12,16 @@ FIXME: Has to point to the commit with the latest update from main, or just `@ma ## Configuration -The CLI stores configuration in `~/.hypersdk-cli/config.cfg`. This includes: +The CLI stores configuration in `~/.hypersdk-cli/config.yaml`. This includes: - Private key - Endpoint URL +Example setup for a local HyperSDK VM: +```bash +hypersdk-cli endpoint set --endpoint=http://localhost:9650/ext/bc/morpheusvm/ +hypersdk-cli key set --key=0x323b1d8f4eed5f0da9da93071b034f2dce9d2d22692c172f3cb252a64ddfafd01b057de320297c29ad0c1f589ea216869cf1938d88c9fbd70d6748323dbf2fa7 +``` + ## Global Flags - `--endpoint`: Override the default endpoint for a single command diff --git a/cmd/hypersdk-cli/actions.go b/cmd/hypersdk-cli/actions.go index be270ebee1..d82d08fbae 100644 --- a/cmd/hypersdk-cli/actions.go +++ b/cmd/hypersdk-cli/actions.go @@ -17,7 +17,7 @@ var actionsCmd = &cobra.Command{ Use: "actions", Short: "Print the list of actions available in the ABI", RunE: func(cmd *cobra.Command, _ []string) error { - endpoint, err := getConfigValue(cmd, "endpoint") + endpoint, err := getConfigValue(cmd, "endpoint", true) if err != nil { return fmt.Errorf("failed to get endpoint: %w", err) } diff --git a/cmd/hypersdk-cli/address.go b/cmd/hypersdk-cli/address.go index f6b9203756..e3651186a8 100644 --- a/cmd/hypersdk-cli/address.go +++ b/cmd/hypersdk-cli/address.go @@ -15,7 +15,7 @@ var addressCmd = &cobra.Command{ Use: "address", Short: "Print current key address", RunE: func(cmd *cobra.Command, _ []string) error { - keyString, err := getConfigValue(cmd, "key") + keyString, err := getConfigValue(cmd, "key", true) if err != nil { return fmt.Errorf("failed to get key: %w", err) } diff --git a/cmd/hypersdk-cli/config.go b/cmd/hypersdk-cli/config.go index bd248497ae..1d803b4108 100644 --- a/cmd/hypersdk-cli/config.go +++ b/cmd/hypersdk-cli/config.go @@ -12,12 +12,49 @@ import ( "strings" "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/ava-labs/hypersdk/codec" ) +func init() { + homeDir, err := os.UserHomeDir() + if err != nil { + fmt.Fprintln(os.Stderr, "Error getting home directory:", err) + os.Exit(1) + } + + configDir := filepath.Join(homeDir, ".hypersdk-cli") + if err := os.MkdirAll(configDir, 0755); err != nil { + fmt.Fprintln(os.Stderr, "Error creating config directory:", err) + os.Exit(1) + } + + configFile := filepath.Join(configDir, "config.yaml") + if _, err := os.Stat(configFile); os.IsNotExist(err) { + if _, err := os.Create(configFile); err != nil { + fmt.Fprintln(os.Stderr, "Error creating config file:", err) + os.Exit(1) + } + } + + // Set config name and paths + viper.SetConfigName("config") + viper.SetConfigType("yaml") + viper.AddConfigPath(configDir) + + // Read config + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + fmt.Fprintln(os.Stderr, "Error reading config:", err) + os.Exit(1) + } + // Config file not found; will be created when needed + } +} + func isJSONOutputRequested(cmd *cobra.Command) (bool, error) { - output, err := getConfigValue(cmd, "output") + output, err := getConfigValue(cmd, "output", false) if err != nil { return false, fmt.Errorf("failed to get output format: %w", err) } @@ -43,84 +80,27 @@ func printValue(cmd *cobra.Command, v fmt.Stringer) error { } } -func getConfigValue(cmd *cobra.Command, name string) (string, error) { - // Check if the value is among flags - if value, err := cmd.Flags().GetString(name); err == nil && value != "" { +func getConfigValue(cmd *cobra.Command, key string, required bool) (string, error) { + // Check flags first + if value, err := cmd.Flags().GetString(key); err == nil && value != "" { return value, nil } - // If not in flags, check the config file - config, err := readConfig() - if err != nil { - return "", fmt.Errorf("failed to read config: %w", err) - } - - if value, ok := config[name]; ok { + // Then check viper + if value := viper.GetString(key); value != "" { return value, nil } - return "", fmt.Errorf("value for %s not found", name) -} - -func updateConfig(name, value string) error { - config, err := readConfig() - if err != nil { - return fmt.Errorf("failed to read config: %w", err) + if required { + return "", fmt.Errorf("required value for %s not found", key) } - config[name] = value - return writeConfig(config) + return "", nil } -func readConfig() (map[string]string, error) { - homeDir, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("failed to get home directory: %w", err) - } - - configPath := filepath.Join(homeDir, ".hypersdk-cli", "config.cfg") - data, err := os.ReadFile(configPath) - if err != nil { - if os.IsNotExist(err) { - return make(map[string]string), nil - } - return nil, fmt.Errorf("failed to read config file: %w", err) - } - - config := make(map[string]string) - lines := strings.Split(string(data), "\n") - for _, line := range lines { - parts := strings.SplitN(line, "=", 2) - if len(parts) == 2 { - config[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) - } - } - - return config, nil -} - -func writeConfig(config map[string]string) error { - homeDir, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get home directory: %w", err) - } - - configDir := filepath.Join(homeDir, ".hypersdk-cli") - if err := os.MkdirAll(configDir, 0o755); err != nil { - return fmt.Errorf("failed to create config directory: %w", err) - } - - configPath := filepath.Join(configDir, "config.cfg") - var buf strings.Builder - for key, value := range config { - buf.WriteString(fmt.Sprintf("%s = %s\n", key, value)) - } - - if err := os.WriteFile(configPath, []byte(buf.String()), 0o600); err != nil { - return fmt.Errorf("failed to write config file: %w", err) - } - - return nil +func setConfigValue(key, value string) error { + viper.Set(key, value) + return viper.WriteConfig() } func decodeFileOrHex(whatever string) ([]byte, error) { diff --git a/cmd/hypersdk-cli/endpoint.go b/cmd/hypersdk-cli/endpoint.go index a461ae9da5..745bdf5100 100644 --- a/cmd/hypersdk-cli/endpoint.go +++ b/cmd/hypersdk-cli/endpoint.go @@ -13,7 +13,7 @@ var endpointCmd = &cobra.Command{ Use: "endpoint", Short: "Manage endpoint", RunE: func(cmd *cobra.Command, _ []string) error { - endpoint, err := getConfigValue(cmd, "endpoint") + endpoint, err := getConfigValue(cmd, "endpoint", true) if err != nil { return fmt.Errorf("failed to get endpoint: %w", err) } diff --git a/cmd/hypersdk-cli/endpoint_set.go b/cmd/hypersdk-cli/endpoint_set.go index a4e23cf169..4832595129 100644 --- a/cmd/hypersdk-cli/endpoint_set.go +++ b/cmd/hypersdk-cli/endpoint_set.go @@ -4,14 +4,11 @@ package main import ( - "context" "errors" "fmt" "log" "github.com/spf13/cobra" - - "github.com/ava-labs/hypersdk/api/jsonrpc" ) var endpointSetCmd = &cobra.Command{ @@ -27,35 +24,22 @@ var endpointSetCmd = &cobra.Command{ return errors.New("endpoint is required") } - if err := updateConfig("endpoint", endpoint); err != nil { + if err := setConfigValue("endpoint", endpoint); err != nil { return fmt.Errorf("failed to update config: %w", err) } - client := jsonrpc.NewJSONRPCClient(endpoint) - success, err := client.Ping(context.Background()) - pingErr := "" - if err != nil { - pingErr = err.Error() - } - return printValue(cmd, endpointSetCmdResponse{ Endpoint: endpoint, - pingResponse: pingResponse{ - PingSucceed: success, - PingError: pingErr, - }, }) }, } type endpointSetCmdResponse struct { - pingResponse Endpoint string `json:"endpoint"` } func (r endpointSetCmdResponse) String() string { - pingStatus := r.pingResponse.String() - return fmt.Sprintf("Endpoint set to: %s\n%s", r.Endpoint, pingStatus) + return fmt.Sprintf("Endpoint set to: %s", r.Endpoint) } func init() { diff --git a/cmd/hypersdk-cli/key_set.go b/cmd/hypersdk-cli/key_set.go index 0e8c90898f..b095432d62 100644 --- a/cmd/hypersdk-cli/key_set.go +++ b/cmd/hypersdk-cli/key_set.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "errors" "fmt" + "log" "github.com/spf13/cobra" @@ -31,10 +32,9 @@ var keySetCmd = &cobra.Command{ Use: "set", Short: "Set the private ED25519 key", RunE: func(cmd *cobra.Command, _ []string) error { - // read directly from the flag instead of calling getConfigValue keyString, err := cmd.Flags().GetString("key") if err != nil { - return fmt.Errorf("failed to get key: %w", err) + return fmt.Errorf("failed to get key flag: %w", err) } if keyString == "" { return errors.New("--key is required") @@ -50,8 +50,8 @@ func checkAndSavePrivateKey(keyString string, cmd *cobra.Command) error { return fmt.Errorf("failed to decode key: %w", err) } - err = updateConfig("key", hex.EncodeToString(key[:])) - if err != nil { + // Use Viper to save the key + if err := setConfigValue("key", hex.EncodeToString(key[:])); err != nil { return fmt.Errorf("failed to update config: %w", err) } @@ -88,4 +88,10 @@ func (r keySetCmdResponse) String() string { func init() { keyCmd.AddCommand(keySetCmd, keyGenerateCmd) + keySetCmd.Flags().String("key", "", "Private key in hex format or path to file containing the key") + + err := keySetCmd.MarkFlagRequired("key") + if err != nil { + log.Fatalf("failed to mark key flag as required: %s", err) + } } diff --git a/cmd/hypersdk-cli/ping.go b/cmd/hypersdk-cli/ping.go index 612d82f8a6..a4a66e071e 100644 --- a/cmd/hypersdk-cli/ping.go +++ b/cmd/hypersdk-cli/ping.go @@ -16,7 +16,7 @@ var endpointPingCmd = &cobra.Command{ Use: "ping", Short: "Ping the endpoint", RunE: func(cmd *cobra.Command, _ []string) error { - endpoint, err := getConfigValue(cmd, "endpoint") + endpoint, err := getConfigValue(cmd, "endpoint", true) if err != nil { return fmt.Errorf("failed to get endpoint: %w", err) } diff --git a/cmd/hypersdk-cli/read.go b/cmd/hypersdk-cli/read.go index 7edd4a8b0e..1b2b1c8596 100644 --- a/cmd/hypersdk-cli/read.go +++ b/cmd/hypersdk-cli/read.go @@ -41,7 +41,7 @@ var readCmd = &cobra.Command{ } } else { // ok, infer user's address from the private key - keyString, err := getConfigValue(cmd, "key") + keyString, err := getConfigValue(cmd, "key", true) if err != nil { return fmt.Errorf("failed to get key from config: %w", err) } @@ -53,7 +53,7 @@ var readCmd = &cobra.Command{ } // 2. create client - endpoint, err := getConfigValue(cmd, "endpoint") + endpoint, err := getConfigValue(cmd, "endpoint", true) if err != nil { return fmt.Errorf("failed to get endpoint: %w", err) } diff --git a/cmd/hypersdk-cli/transaction.go b/cmd/hypersdk-cli/transaction.go index 312f627280..4cb7694bc5 100644 --- a/cmd/hypersdk-cli/transaction.go +++ b/cmd/hypersdk-cli/transaction.go @@ -33,7 +33,7 @@ var txCmd = &cobra.Command{ defer cancel() // 1. Decode key - keyString, err := getConfigValue(cmd, "key") + keyString, err := getConfigValue(cmd, "key", true) if err != nil { return fmt.Errorf("failed to get key from config: %w", err) } @@ -43,7 +43,7 @@ var txCmd = &cobra.Command{ } // 2. create client - endpoint, err := getConfigValue(cmd, "endpoint") + endpoint, err := getConfigValue(cmd, "endpoint", true) if err != nil { return fmt.Errorf("failed to get endpoint: %w", err) } From 8ad54d548431225a62034575aa30ce245901cdd3 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Fri, 25 Oct 2024 06:35:17 +0000 Subject: [PATCH 47/53] descriptive variable name --- cmd/hypersdk-cli/config.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/hypersdk-cli/config.go b/cmd/hypersdk-cli/config.go index 1d803b4108..e4404e6945 100644 --- a/cmd/hypersdk-cli/config.go +++ b/cmd/hypersdk-cli/config.go @@ -103,12 +103,12 @@ func setConfigValue(key, value string) error { return viper.WriteConfig() } -func decodeFileOrHex(whatever string) ([]byte, error) { - if decoded, err := codec.LoadHex(whatever, -1); err == nil { +func decodeFileOrHex(fileNameOrHex string) ([]byte, error) { + if decoded, err := codec.LoadHex(fileNameOrHex, -1); err == nil { return decoded, nil } - if fileContents, err := os.ReadFile(whatever); err == nil { + if fileContents, err := os.ReadFile(fileNameOrHex); err == nil { return fileContents, nil } From 40611e0b572c9281e3a9bf914cc2c571e6a9c24a Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Fri, 25 Oct 2024 06:38:29 +0000 Subject: [PATCH 48/53] unexpected field provided error --- cmd/hypersdk-cli/read.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/cmd/hypersdk-cli/read.go b/cmd/hypersdk-cli/read.go index 1b2b1c8596..0d0e985c6e 100644 --- a/cmd/hypersdk-cli/read.go +++ b/cmd/hypersdk-cli/read.go @@ -153,8 +153,19 @@ func fillAction(cmd *cobra.Command, typ abi.Type) (map[string]interface{}, error return kvPairs, nil } - func fillFromInputData(typ abi.Type, kvData map[string]string) (map[string]interface{}, error) { + // check if any provided fields don't exist in the type definition + validFields := make(map[string]bool) + for _, field := range typ.Fields { + validFields[field.Name] = true + } + + for inputField := range kvData { + if !validFields[inputField] { + return nil, fmt.Errorf("unexpected field provided: %s", inputField) + } + } + kvPairs := make(map[string]interface{}) for _, field := range typ.Fields { value, ok := kvData[field.Name] From c648b25a98c5d66e6a4671dbe7491bf12eb026ac Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Fri, 25 Oct 2024 06:40:57 +0000 Subject: [PATCH 49/53] add TODO for timestamp and max fee --- cmd/hypersdk-cli/transaction.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/hypersdk-cli/transaction.go b/cmd/hypersdk-cli/transaction.go index 4cb7694bc5..3945f618fd 100644 --- a/cmd/hypersdk-cli/transaction.go +++ b/cmd/hypersdk-cli/transaction.go @@ -92,8 +92,8 @@ var txCmd = &cobra.Command{ base := &chain.Base{ ChainID: chainID, - Timestamp: time.Now().Unix()*1000 + 60*1000, - MaxFee: 1_000_000, + Timestamp: time.Now().Unix()*1000 + 60*1000, // TODO: use utils.UnixRMilli(now, rules.GetValidityWindow()) + MaxFee: 1_000_000, // TODO: use chain.EstimateUnits(parser.Rules(time.Now().UnixMilli()), actions, authFactory) } signedBytes, err := SignTxManually([][]byte{actionBytes}, base, key) From d80ed67fb48c4a468e9770acfc7027d3948ec06f Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Fri, 25 Oct 2024 07:09:02 +0000 Subject: [PATCH 50/53] change ws to indexer --- api/indexer/indexer.go | 16 +++++++---- api/indexer/server.go | 4 ++- cmd/hypersdk-cli/config.go | 2 +- cmd/hypersdk-cli/read.go | 1 + cmd/hypersdk-cli/transaction.go | 51 ++++++++++++++------------------- 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/api/indexer/indexer.go b/api/indexer/indexer.go index 6f70b8928e..809960115a 100644 --- a/api/indexer/indexer.go +++ b/api/indexer/indexer.go @@ -174,6 +174,7 @@ func (i *Indexer) storeTransactions(blk *chain.ExecutedBlock) error { result.Units, result.Fee, result.Outputs, + string(result.Error), ); err != nil { return err } @@ -190,6 +191,7 @@ func (*Indexer) storeTransaction( units fees.Dimensions, fee uint64, outputs [][]byte, + errorStr string, ) error { outputLength := consts.ByteLen // Single byte containing number of outputs for _, output := range outputs { @@ -206,19 +208,20 @@ func (*Indexer) storeTransaction( for _, output := range outputs { writer.PackBytes(output) } + writer.PackString(errorStr) if err := writer.Err(); err != nil { return err } return batch.Put(txID[:], writer.Bytes()) } -func (i *Indexer) GetTransaction(txID ids.ID) (bool, int64, bool, fees.Dimensions, uint64, [][]byte, error) { +func (i *Indexer) GetTransaction(txID ids.ID) (bool, int64, bool, fees.Dimensions, uint64, [][]byte, string, error) { v, err := i.txDB.Get(txID[:]) if errors.Is(err, database.ErrNotFound) { - return false, 0, false, fees.Dimensions{}, 0, nil, nil + return false, 0, false, fees.Dimensions{}, 0, nil, "", nil } if err != nil { - return false, 0, false, fees.Dimensions{}, 0, nil, err + return false, 0, false, fees.Dimensions{}, 0, nil, "", err } reader := codec.NewReader(v, consts.NetworkSizeLimit) timestamp := reader.UnpackUint64(true) @@ -231,14 +234,15 @@ func (i *Indexer) GetTransaction(txID ids.ID) (bool, int64, bool, fees.Dimension for i := range outputs { outputs[i] = reader.UnpackLimitedBytes(consts.NetworkSizeLimit) } + errorStr := reader.UnpackString(false) if err := reader.Err(); err != nil { - return false, 0, false, fees.Dimensions{}, 0, nil, err + return false, 0, false, fees.Dimensions{}, 0, nil, "", err } dimensions, err := fees.UnpackDimensions(dimensionsBytes) if err != nil { - return false, 0, false, fees.Dimensions{}, 0, nil, err + return false, 0, false, fees.Dimensions{}, 0, nil, "", err } - return true, int64(timestamp), success, dimensions, fee, outputs, nil + return true, int64(timestamp), success, dimensions, fee, outputs, errorStr, nil } func (i *Indexer) Close() error { diff --git a/api/indexer/server.go b/api/indexer/server.go index f555bdc6c9..e8cec42f03 100644 --- a/api/indexer/server.go +++ b/api/indexer/server.go @@ -111,6 +111,7 @@ type GetTxResponse struct { Units fees.Dimensions `json:"units"` Fee uint64 `json:"fee"` Outputs []codec.Bytes `json:"result"` + ErrorStr string `json:"errorStr"` } type Server struct { @@ -122,7 +123,7 @@ func (s *Server) GetTx(req *http.Request, args *GetTxRequest, reply *GetTxRespon _, span := s.tracer.Start(req.Context(), "Indexer.GetTx") defer span.End() - found, t, success, units, fee, outputs, err := s.indexer.GetTransaction(args.TxID) + found, t, success, units, fee, outputs, errorStr, err := s.indexer.GetTransaction(args.TxID) if err != nil { return err } @@ -139,5 +140,6 @@ func (s *Server) GetTx(req *http.Request, args *GetTxRequest, reply *GetTxRespon wrappedOutputs[i] = codec.Bytes(output) } reply.Outputs = wrappedOutputs + reply.ErrorStr = errorStr return nil } diff --git a/cmd/hypersdk-cli/config.go b/cmd/hypersdk-cli/config.go index e4404e6945..cfb51bb61c 100644 --- a/cmd/hypersdk-cli/config.go +++ b/cmd/hypersdk-cli/config.go @@ -25,7 +25,7 @@ func init() { } configDir := filepath.Join(homeDir, ".hypersdk-cli") - if err := os.MkdirAll(configDir, 0755); err != nil { + if err := os.MkdirAll(configDir, 0o755); err != nil { fmt.Fprintln(os.Stderr, "Error creating config directory:", err) os.Exit(1) } diff --git a/cmd/hypersdk-cli/read.go b/cmd/hypersdk-cli/read.go index 0d0e985c6e..01c41bc646 100644 --- a/cmd/hypersdk-cli/read.go +++ b/cmd/hypersdk-cli/read.go @@ -153,6 +153,7 @@ func fillAction(cmd *cobra.Command, typ abi.Type) (map[string]interface{}, error return kvPairs, nil } + func fillFromInputData(typ abi.Type, kvData map[string]string) (map[string]interface{}, error) { // check if any provided fields don't exist in the type definition validFields := make(map[string]bool) diff --git a/cmd/hypersdk-cli/transaction.go b/cmd/hypersdk-cli/transaction.go index 3945f618fd..214a274e2e 100644 --- a/cmd/hypersdk-cli/transaction.go +++ b/cmd/hypersdk-cli/transaction.go @@ -15,14 +15,13 @@ import ( "github.com/spf13/cobra" "github.com/ava-labs/hypersdk/abi/dynamic" + "github.com/ava-labs/hypersdk/api/indexer" "github.com/ava-labs/hypersdk/api/jsonrpc" - "github.com/ava-labs/hypersdk/api/ws" "github.com/ava-labs/hypersdk/auth" "github.com/ava-labs/hypersdk/chain" "github.com/ava-labs/hypersdk/codec" "github.com/ava-labs/hypersdk/consts" "github.com/ava-labs/hypersdk/crypto/ed25519" - "github.com/ava-labs/hypersdk/utils" ) var txCmd = &cobra.Command{ @@ -101,39 +100,33 @@ var txCmd = &cobra.Command{ return fmt.Errorf("failed to sign tx: %w", err) } - wsClient, err := ws.NewWebSocketClient(endpoint, 10*time.Second, 100, 1000000) - if err != nil { - return fmt.Errorf("failed to create ws client: %w", err) - } + indexerClient := indexer.NewClient(endpoint) - if err := wsClient.RegisterRawTx(signedBytes); err != nil { - return fmt.Errorf("failed to register tx: %w", err) + expectedTxID, err := client.SubmitTx(ctx, signedBytes) + if err != nil { + return fmt.Errorf("failed to send tx: %w", err) } - expectedTxID := utils.ToID(signedBytes) - // Listen for the transaction result - var result *chain.Result + var getTxResponse indexer.GetTxResponse for { - txID, txErr, txResult, err := wsClient.ListenTx(ctx) - if err != nil { - if ctx.Err() == context.DeadlineExceeded { - return fmt.Errorf("failed to listen for transaction: %w", ctx.Err()) - } - return fmt.Errorf("failed to listen for transaction: %w", err) + if err := ctx.Err(); err != nil { + return fmt.Errorf("context expired while waiting for tx: %w", err) } - if txErr != nil { - return txErr + + getTxResponse, found, err = indexerClient.GetTx(ctx, expectedTxID) + if err != nil { + return fmt.Errorf("failed to get tx: %w", err) } - if txID == expectedTxID { - result = txResult + if found { break } + time.Sleep(500 * time.Millisecond) } var resultStruct map[string]interface{} - if result.Success { - if len(result.Outputs) == 1 { - resultJSON, err := dynamic.UnmarshalOutput(abi, result.Outputs[0]) + if getTxResponse.Success { + if len(getTxResponse.Outputs) == 1 { + resultJSON, err := dynamic.UnmarshalOutput(abi, getTxResponse.Outputs[0]) if err != nil { return fmt.Errorf("failed to unmarshal result: %w", err) } @@ -142,16 +135,16 @@ var txCmd = &cobra.Command{ if err != nil { return fmt.Errorf("failed to unmarshal result JSON: %w", err) } - } else if len(result.Outputs) > 1 { - return fmt.Errorf("expected 1 output, got %d", len(result.Outputs)) + } else if len(getTxResponse.Outputs) > 1 { + return fmt.Errorf("expected 1 output, got %d", len(getTxResponse.Outputs)) } } return printValue(cmd, txResponse{ Result: resultStruct, - Success: result.Success, + Success: getTxResponse.Success, TxID: expectedTxID, - Error: string(result.Error), + Error: getTxResponse.ErrorStr, }) }, } @@ -166,7 +159,7 @@ type txResponse struct { func (r txResponse) String() string { var result strings.Builder if r.Success { - result.WriteString(fmt.Sprintf("✅ Transaction successful (txID: %s)\n\n", r.TxID)) + result.WriteString(fmt.Sprintf("✅ Transaction successful (txID: %s)\n", r.TxID)) if r.Result != nil { for key, value := range r.Result { jsonValue, err := json.Marshal(value) From 1a47159b432d84ab46f69bcf4e2d6eee3876805d Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Fri, 25 Oct 2024 07:09:40 +0000 Subject: [PATCH 51/53] lint --- cmd/hypersdk-cli/endpoint_set.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/hypersdk-cli/endpoint_set.go b/cmd/hypersdk-cli/endpoint_set.go index 4832595129..e84dffa739 100644 --- a/cmd/hypersdk-cli/endpoint_set.go +++ b/cmd/hypersdk-cli/endpoint_set.go @@ -39,7 +39,7 @@ type endpointSetCmdResponse struct { } func (r endpointSetCmdResponse) String() string { - return fmt.Sprintf("Endpoint set to: %s", r.Endpoint) + return "Endpoint set to: " + r.Endpoint } func init() { From 0ae0c9ab525a86c61a8014ffde3e22fb4e0ef519 Mon Sep 17 00:00:00 2001 From: Containerman17 <8990432+containerman17@users.noreply.github.com> Date: Fri, 25 Oct 2024 07:13:13 +0000 Subject: [PATCH 52/53] go mod tidy (viper) --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index a225904a11..2ef0060796 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/prometheus/client_golang v1.16.0 github.com/spf13/cobra v1.5.0 + github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.8.4 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/exporters/zipkin v1.11.2 @@ -123,7 +124,6 @@ require ( github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.12.0 // indirect github.com/status-im/keycard-go v0.2.0 // indirect github.com/subosito/gotenv v1.3.0 // indirect github.com/supranational/blst v0.3.11 // indirect From 83bd857b8571bd3f7cc8ee99c723a2eb23580a77 Mon Sep 17 00:00:00 2001 From: Aaron Buchwald Date: Fri, 25 Oct 2024 10:52:26 -0400 Subject: [PATCH 53/53] move manual tx signing to chain package + nits --- chain/transaction.go | 22 ++++++++++-- chain/transaction_test.go | 61 +++++++++++++++++++++++++-------- cmd/hypersdk-cli/key_set.go | 8 ++--- cmd/hypersdk-cli/read.go | 19 ++++------ cmd/hypersdk-cli/transaction.go | 41 +--------------------- 5 files changed, 78 insertions(+), 73 deletions(-) diff --git a/chain/transaction.go b/chain/transaction.go index a0908d4bd6..4903b7b9b4 100644 --- a/chain/transaction.go +++ b/chain/transaction.go @@ -102,6 +102,24 @@ func (t *TransactionData) Sign( return UnmarshalTx(p, actionCodec, authCodec) } +func SignRawActionBytesTx( + base *Base, + rawActionsBytes []byte, + authFactory AuthFactory, +) ([]byte, error) { + p := codec.NewWriter(base.Size(), consts.NetworkSizeLimit) + base.Marshal(p) + p.PackFixedBytes(rawActionsBytes) + + auth, err := authFactory.Sign(p.Bytes()) + if err != nil { + return nil, err + } + p.PackByte(auth.GetTypeID()) + auth.Marshal(p) + return p.Bytes(), p.Err() +} + func (t *TransactionData) Expiry() int64 { return t.Base.Timestamp } func (t *TransactionData) MaxFee() uint64 { return t.Base.MaxFee } @@ -116,7 +134,7 @@ func (t *TransactionData) Marshal(p *codec.Packer) error { func (t *TransactionData) marshal(p *codec.Packer) error { t.Base.Marshal(p) - return t.Actions.marshalInto(p) + return t.Actions.MarshalInto(p) } type Actions []Action @@ -133,7 +151,7 @@ func (a Actions) Size() (int, error) { return size, nil } -func (a Actions) marshalInto(p *codec.Packer) error { +func (a Actions) MarshalInto(p *codec.Packer) error { p.PackByte(uint8(len(a))) for _, action := range a { p.PackByte(action.GetTypeID()) diff --git a/chain/transaction_test.go b/chain/transaction_test.go index d00ed512c4..999c906110 100644 --- a/chain/transaction_test.go +++ b/chain/transaction_test.go @@ -13,6 +13,7 @@ import ( "github.com/ava-labs/hypersdk/auth" "github.com/ava-labs/hypersdk/chain" "github.com/ava-labs/hypersdk/codec" + "github.com/ava-labs/hypersdk/consts" "github.com/ava-labs/hypersdk/crypto/ed25519" "github.com/ava-labs/hypersdk/state" ) @@ -115,7 +116,33 @@ func TestMarshalUnmarshal(t *testing.T) { err = actionCodec.Register(&action2{}, unmarshalAction2) require.NoError(err) - txBeforeSign := chain.TransactionData{ + // call UnsignedBytes so that the "unsignedBytes" field would get populated. + txBeforeSignBytes, err := tx.UnsignedBytes() + require.NoError(err) + + signedTx, err := tx.Sign(factory, actionCodec, authCodec) + require.NoError(err) + unsignedTxAfterSignBytes, err := signedTx.TransactionData.UnsignedBytes() + require.NoError(err) + require.Equal(txBeforeSignBytes, unsignedTxAfterSignBytes) + require.NotNil(signedTx.Auth) + require.Equal(len(signedTx.Actions), len(tx.Actions)) + for i, action := range signedTx.Actions { + require.Equal(tx.Actions[i], action) + } + + unsignedTxBytes, err := signedTx.UnsignedBytes() + require.NoError(err) + originalUnsignedTxBytes, err := tx.UnsignedBytes() + require.NoError(err) + + require.Equal(unsignedTxBytes, originalUnsignedTxBytes) + require.Len(unsignedTxBytes, 168) +} + +func TestSignRawActionBytesTx(t *testing.T) { + require := require.New(t) + tx := chain.TransactionData{ Base: &chain.Base{ Timestamp: 1724315246000, ChainID: [32]byte{1, 2, 3, 4, 5, 6, 7}, @@ -138,24 +165,28 @@ func TestMarshalUnmarshal(t *testing.T) { }, }, } - // call UnsignedBytes so that the "unsignedBytes" field would get populated. - _, err = txBeforeSign.UnsignedBytes() - require.NoError(err) - signedTx, err := tx.Sign(factory, actionCodec, authCodec) + priv, err := ed25519.GeneratePrivateKey() require.NoError(err) - require.Equal(txBeforeSign, tx) - require.NotNil(signedTx.Auth) - require.Equal(len(signedTx.Actions), len(tx.Actions)) - for i, action := range signedTx.Actions { - require.Equal(tx.Actions[i], action) - } + factory := auth.NewED25519Factory(priv) - unsignedTxBytes, err := signedTx.UnsignedBytes() + actionCodec := codec.NewTypeParser[chain.Action]() + authCodec := codec.NewTypeParser[chain.Auth]() + + err = authCodec.Register(&auth.ED25519{}, auth.UnmarshalED25519) require.NoError(err) - originalUnsignedTxBytes, err := tx.UnsignedBytes() + err = actionCodec.Register(&mockTransferAction{}, unmarshalTransfer) + require.NoError(err) + err = actionCodec.Register(&action2{}, unmarshalAction2) require.NoError(err) - require.Equal(unsignedTxBytes, originalUnsignedTxBytes) - require.Len(unsignedTxBytes, 168) + signedTx, err := tx.Sign(factory, actionCodec, authCodec) + require.NoError(err) + + p := codec.NewWriter(0, consts.NetworkSizeLimit) + require.NoError(signedTx.Actions.MarshalInto(p)) + actionsBytes := p.Bytes() + rawSignedTxBytes, err := chain.SignRawActionBytesTx(tx.Base, actionsBytes, factory) + require.NoError(err) + require.Equal(signedTx.Bytes(), rawSignedTxBytes) } diff --git a/cmd/hypersdk-cli/key_set.go b/cmd/hypersdk-cli/key_set.go index b095432d62..22eef68779 100644 --- a/cmd/hypersdk-cli/key_set.go +++ b/cmd/hypersdk-cli/key_set.go @@ -24,7 +24,7 @@ var keyGenerateCmd = &cobra.Command{ return fmt.Errorf("failed to generate key: %w", err) } - return checkAndSavePrivateKey(hex.EncodeToString(newKey[:]), cmd) + return checkAndSavePrivateKey(cmd, hex.EncodeToString(newKey[:])) }, } @@ -40,12 +40,12 @@ var keySetCmd = &cobra.Command{ return errors.New("--key is required") } - return checkAndSavePrivateKey(keyString, cmd) + return checkAndSavePrivateKey(cmd, keyString) }, } -func checkAndSavePrivateKey(keyString string, cmd *cobra.Command) error { - key, err := privateKeyFromString(keyString) +func checkAndSavePrivateKey(cmd *cobra.Command, keyStr string) error { + key, err := privateKeyFromString(keyStr) if err != nil { return fmt.Errorf("failed to decode key: %w", err) } diff --git a/cmd/hypersdk-cli/read.go b/cmd/hypersdk-cli/read.go index 01c41bc646..4ce610dd50 100644 --- a/cmd/hypersdk-cli/read.go +++ b/cmd/hypersdk-cli/read.go @@ -155,24 +155,19 @@ func fillAction(cmd *cobra.Command, typ abi.Type) (map[string]interface{}, error } func fillFromInputData(typ abi.Type, kvData map[string]string) (map[string]interface{}, error) { - // check if any provided fields don't exist in the type definition - validFields := make(map[string]bool) - for _, field := range typ.Fields { - validFields[field.Name] = true + // Require exact match in required fields to supplied arguments + if len(kvData) != len(typ.Fields) { + return nil, fmt.Errorf("type has %d fields, got %d arguments", len(typ.Fields), len(kvData)) } - - for inputField := range kvData { - if !validFields[inputField] { - return nil, fmt.Errorf("unexpected field provided: %s", inputField) + for _, field := range typ.Fields { + if _, ok := kvData[field.Name]; !ok { + return nil, fmt.Errorf("missing argument: %s", field.Name) } } kvPairs := make(map[string]interface{}) for _, field := range typ.Fields { - value, ok := kvData[field.Name] - if !ok { - continue - } + value := kvData[field.Name] var parsedValue interface{} var err error switch field.Type { diff --git a/cmd/hypersdk-cli/transaction.go b/cmd/hypersdk-cli/transaction.go index 214a274e2e..d8e6f7d097 100644 --- a/cmd/hypersdk-cli/transaction.go +++ b/cmd/hypersdk-cli/transaction.go @@ -19,9 +19,6 @@ import ( "github.com/ava-labs/hypersdk/api/jsonrpc" "github.com/ava-labs/hypersdk/auth" "github.com/ava-labs/hypersdk/chain" - "github.com/ava-labs/hypersdk/codec" - "github.com/ava-labs/hypersdk/consts" - "github.com/ava-labs/hypersdk/crypto/ed25519" ) var txCmd = &cobra.Command{ @@ -95,7 +92,7 @@ var txCmd = &cobra.Command{ MaxFee: 1_000_000, // TODO: use chain.EstimateUnits(parser.Rules(time.Now().UnixMilli()), actions, authFactory) } - signedBytes, err := SignTxManually([][]byte{actionBytes}, base, key) + signedBytes, err := chain.SignRawActionBytesTx(base, append([]byte{1}, actionBytes...), auth.NewED25519Factory(key)) if err != nil { return fmt.Errorf("failed to sign tx: %w", err) } @@ -179,39 +176,3 @@ func init() { txCmd.Flags().StringToString("data", nil, "Key-value pairs for the action data (e.g., key1=value1,key2=value2)") rootCmd.AddCommand(txCmd) } - -func SignTxManually(actionsTxBytes [][]byte, base *chain.Base, privateKey ed25519.PrivateKey) ([]byte, error) { - // Create auth factory - factory := auth.NewED25519Factory(privateKey) - - // Marshal base - p := codec.NewWriter(base.Size(), consts.NetworkSizeLimit) - base.Marshal(p) - baseBytes := p.Bytes() - - // Build unsigned bytes starting with base and number of actions - unsignedBytes := make([]byte, 0) - unsignedBytes = append(unsignedBytes, baseBytes...) - unsignedBytes = append(unsignedBytes, byte(len(actionsTxBytes))) // Number of actions - - // Append each action's bytes - for _, actionBytes := range actionsTxBytes { - unsignedBytes = append(unsignedBytes, actionBytes...) - } - - // Sign the transaction - auth, err := factory.Sign(unsignedBytes) - if err != nil { - return nil, err - } - // Marshal auth - p = codec.NewWriter(auth.Size(), consts.NetworkSizeLimit) - auth.Marshal(p) - authBytes := []byte{auth.GetTypeID()} - authBytes = append(authBytes, p.Bytes()...) - - // Combine everything into final signed transaction - //nolint:gocritic //append is fine here - signedBytes := append(unsignedBytes, authBytes...) - return signedBytes, nil -}