From c32886e89749e1ac34a0338bce786eb840cc8353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 10 Apr 2024 17:53:08 +0300 Subject: [PATCH 01/24] Initial commit. --- abi/codec.go | 271 ++++++++ abi/codecForCompositeTypes.go | 76 +++ abi/codecForCustomTypes_enum.go | 61 ++ abi/codecForCustomTypes_struct.go | 38 ++ abi/codecForSimpleValues_address.go | 48 ++ abi/codecForSimpleValues_boolean.go | 70 ++ abi/codecForSimpleValues_bytes.go | 30 + abi/codecForSimpleValues_numerical.go | 138 ++++ abi/codecForSimpleValues_string.go | 31 + abi/codec_test.go | 907 ++++++++++++++++++++++++++ abi/constants.go | 4 + abi/parts.go | 99 +++ abi/serializer.go | 246 +++++++ abi/serializer_test.go | 327 ++++++++++ abi/shared.go | 45 ++ abi/values.go | 122 ++++ go.mod | 14 + go.sum | 13 + 18 files changed, 2540 insertions(+) create mode 100644 abi/codec.go create mode 100644 abi/codecForCompositeTypes.go create mode 100644 abi/codecForCustomTypes_enum.go create mode 100644 abi/codecForCustomTypes_struct.go create mode 100644 abi/codecForSimpleValues_address.go create mode 100644 abi/codecForSimpleValues_boolean.go create mode 100644 abi/codecForSimpleValues_bytes.go create mode 100644 abi/codecForSimpleValues_numerical.go create mode 100644 abi/codecForSimpleValues_string.go create mode 100644 abi/codec_test.go create mode 100644 abi/constants.go create mode 100644 abi/parts.go create mode 100644 abi/serializer.go create mode 100644 abi/serializer_test.go create mode 100644 abi/shared.go create mode 100644 abi/values.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/abi/codec.go b/abi/codec.go new file mode 100644 index 0000000..2dc8d55 --- /dev/null +++ b/abi/codec.go @@ -0,0 +1,271 @@ +package abi + +import ( + "bytes" + "errors" + "fmt" + "io" + "math" +) + +type codec struct { + pubKeyLength int +} + +// argsNewCodec defines the arguments needed for a new codec +type argsNewCodec struct { + pubKeyLength int +} + +// newCodec creates a new default codec which follows the rules of the MultiversX Serialization format: +// https://docs.multiversx.com/developers/data/serialization-overview +func newCodec(args argsNewCodec) (*codec, error) { + if args.pubKeyLength <= 0 { + return nil, errors.New("cannot create codec: bad public key length") + } + + return &codec{ + pubKeyLength: args.pubKeyLength, + }, nil +} + +// EncodeNested encodes the given value following the nested encoding rules +func (c *codec) EncodeNested(value any) ([]byte, error) { + buffer := bytes.NewBuffer(nil) + err := c.doEncodeNested(buffer, value) + if err != nil { + return nil, err + } + + return buffer.Bytes(), nil +} + +func (c *codec) doEncodeNested(writer io.Writer, value any) error { + switch value := value.(type) { + case BoolValue: + return c.encodeNestedBool(writer, value) + case U8Value: + return c.encodeNestedNumber(writer, value.Value, 1) + case U16Value: + return c.encodeNestedNumber(writer, value.Value, 2) + case U32Value: + return c.encodeNestedNumber(writer, value.Value, 4) + case U64Value: + return c.encodeNestedNumber(writer, value.Value, 8) + case I8Value: + return c.encodeNestedNumber(writer, value.Value, 1) + case I16Value: + return c.encodeNestedNumber(writer, value.Value, 2) + case I32Value: + return c.encodeNestedNumber(writer, value.Value, 4) + case I64Value: + return c.encodeNestedNumber(writer, value.Value, 8) + case BigIntValue: + return c.encodeNestedBigNumber(writer, value.Value) + case AddressValue: + return c.encodeNestedAddress(writer, value) + case StringValue: + return c.encodeNestedString(writer, value) + case BytesValue: + return c.encodeNestedBytes(writer, value) + case StructValue: + return c.encodeNestedStruct(writer, value) + case EnumValue: + return c.encodeNestedEnum(writer, value) + case OptionValue: + return c.encodeNestedOption(writer, value) + case InputListValue: + return c.encodeNestedList(writer, value) + default: + return fmt.Errorf("unsupported type for nested encoding: %T", value) + } +} + +// EncodeTopLevel encodes the given value following the top-level encoding rules +func (c *codec) EncodeTopLevel(value any) ([]byte, error) { + buffer := bytes.NewBuffer(nil) + err := c.doEncodeTopLevel(buffer, value) + if err != nil { + return nil, err + } + + return buffer.Bytes(), nil +} + +func (c *codec) doEncodeTopLevel(writer io.Writer, value any) error { + switch value := value.(type) { + case BoolValue: + return c.encodeTopLevelBool(writer, value) + case U8Value: + return c.encodeTopLevelUnsignedNumber(writer, uint64(value.Value)) + case U16Value: + return c.encodeTopLevelUnsignedNumber(writer, uint64(value.Value)) + case U32Value: + return c.encodeTopLevelUnsignedNumber(writer, uint64(value.Value)) + case U64Value: + return c.encodeTopLevelUnsignedNumber(writer, value.Value) + case I8Value: + return c.encodeTopLevelSignedNumber(writer, int64(value.Value)) + case I16Value: + return c.encodeTopLevelSignedNumber(writer, int64(value.Value)) + case I32Value: + return c.encodeTopLevelSignedNumber(writer, int64(value.Value)) + case I64Value: + return c.encodeTopLevelSignedNumber(writer, value.Value) + case BigIntValue: + return c.encodeTopLevelBigNumber(writer, value.Value) + case AddressValue: + return c.encodeTopLevelAddress(writer, value) + case StructValue: + return c.encodeTopLevelStruct(writer, value) + case EnumValue: + return c.encodeTopLevelEnum(writer, value) + default: + return fmt.Errorf("unsupported type for top-level encoding: %T", value) + } +} + +// DecodeNested decodes the given data into the provided object following the nested decoding rules +func (c *codec) DecodeNested(data []byte, value any) error { + reader := bytes.NewReader(data) + err := c.doDecodeNested(reader, value) + if err != nil { + return fmt.Errorf("cannot decode (nested) %T, because of: %w", value, err) + } + + return nil +} + +func (c *codec) doDecodeNested(reader io.Reader, value any) error { + switch value := value.(type) { + case *BoolValue: + return c.decodeNestedBool(reader, value) + case *U8Value: + return c.decodeNestedNumber(reader, &value.Value, 1) + case *U16Value: + return c.decodeNestedNumber(reader, &value.Value, 2) + case *U32Value: + return c.decodeNestedNumber(reader, &value.Value, 4) + case *U64Value: + return c.decodeNestedNumber(reader, &value.Value, 8) + case *I8Value: + return c.decodeNestedNumber(reader, &value.Value, 1) + case *I16Value: + return c.decodeNestedNumber(reader, &value.Value, 2) + case *I32Value: + return c.decodeNestedNumber(reader, &value.Value, 4) + case *I64Value: + return c.decodeNestedNumber(reader, &value.Value, 8) + case *BigIntValue: + n, err := c.decodeNestedBigNumber(reader) + if err != nil { + return err + } + + value.Value = n + return nil + case *AddressValue: + return c.decodeNestedAddress(reader, value) + case *StringValue: + return c.decodeNestedString(reader, value) + case *BytesValue: + return c.decodeNestedBytes(reader, value) + case *StructValue: + return c.decodeNestedStruct(reader, value) + case *EnumValue: + return c.decodeNestedEnum(reader, value) + case *OptionValue: + return c.decodeNestedOption(reader, value) + case *OutputListValue: + return c.decodeNestedList(reader, value) + default: + return fmt.Errorf("unsupported type for nested decoding: %T", value) + } +} + +// DecodeTopLevel decodes the given data into the provided object following the top-level decoding rules +func (c *codec) DecodeTopLevel(data []byte, value any) error { + err := c.doDecodeTopLevel(data, value) + if err != nil { + return fmt.Errorf("cannot decode (top-level) %T, because of: %w", value, err) + } + + return nil +} + +func (c *codec) doDecodeTopLevel(data []byte, value any) error { + switch value := value.(type) { + case *BoolValue: + return c.decodeTopLevelBool(data, value) + case *U8Value: + n, err := c.decodeTopLevelUnsignedNumber(data, math.MaxUint8) + if err != nil { + return err + } + + value.Value = uint8(n) + case *U16Value: + n, err := c.decodeTopLevelUnsignedNumber(data, math.MaxUint16) + if err != nil { + return err + } + + value.Value = uint16(n) + case *U32Value: + n, err := c.decodeTopLevelUnsignedNumber(data, math.MaxUint32) + if err != nil { + return err + } + + value.Value = uint32(n) + case *U64Value: + n, err := c.decodeTopLevelUnsignedNumber(data, math.MaxUint64) + if err != nil { + return err + } + + value.Value = uint64(n) + case *I8Value: + n, err := c.decodeTopLevelSignedNumber(data, math.MaxInt8) + if err != nil { + return err + } + + value.Value = int8(n) + case *I16Value: + n, err := c.decodeTopLevelSignedNumber(data, math.MaxInt16) + if err != nil { + return err + } + + value.Value = int16(n) + case *I32Value: + n, err := c.decodeTopLevelSignedNumber(data, math.MaxInt32) + if err != nil { + return err + } + + value.Value = int32(n) + + case *I64Value: + n, err := c.decodeTopLevelSignedNumber(data, math.MaxInt64) + if err != nil { + return err + } + + value.Value = int64(n) + case *BigIntValue: + n := c.decodeTopLevelBigNumber(data) + value.Value = n + case *AddressValue: + return c.decodeTopLevelAddress(data, value) + case *StructValue: + return c.decodeTopLevelStruct(data, value) + case *EnumValue: + return c.decodeTopLevelEnum(data, value) + default: + return fmt.Errorf("unsupported type for top-level decoding: %T", value) + } + + return nil +} diff --git a/abi/codecForCompositeTypes.go b/abi/codecForCompositeTypes.go new file mode 100644 index 0000000..df1ca72 --- /dev/null +++ b/abi/codecForCompositeTypes.go @@ -0,0 +1,76 @@ +package abi + +import ( + "errors" + "io" +) + +func (c *codec) encodeNestedOption(writer io.Writer, value OptionValue) error { + if value.Value == nil { + _, err := writer.Write([]byte{0}) + return err + } + + _, err := writer.Write([]byte{1}) + if err != nil { + return err + } + + return c.doEncodeNested(writer, value.Value) +} + +func (c *codec) decodeNestedOption(reader io.Reader, value *OptionValue) error { + bytes, err := readBytesExactly(reader, 1) + if err != nil { + return err + } + + if bytes[0] == 0 { + value.Value = nil + return nil + } + + return c.doDecodeNested(reader, value.Value) +} + +func (c *codec) encodeNestedList(writer io.Writer, value InputListValue) error { + err := encodeLength(writer, uint32(len(value.Items))) + if err != nil { + return err + } + + for _, item := range value.Items { + err := c.doEncodeNested(writer, item) + if err != nil { + return err + } + } + + return nil +} + +func (c *codec) decodeNestedList(reader io.Reader, value *OutputListValue) error { + if value.ItemCreator == nil { + return errors.New("cannot deserialize list: item creator is nil") + } + + length, err := decodeLength(reader) + if err != nil { + return err + } + + value.Items = make([]any, 0, length) + + for i := uint32(0); i < length; i++ { + newItem := value.ItemCreator() + + err := c.doDecodeNested(reader, newItem) + if err != nil { + return err + } + + value.Items = append(value.Items, newItem) + } + + return nil +} diff --git a/abi/codecForCustomTypes_enum.go b/abi/codecForCustomTypes_enum.go new file mode 100644 index 0000000..7a215d1 --- /dev/null +++ b/abi/codecForCustomTypes_enum.go @@ -0,0 +1,61 @@ +package abi + +import ( + "bytes" + "fmt" + "io" +) + +func (c *codec) encodeNestedEnum(writer io.Writer, value EnumValue) error { + err := c.doEncodeNested(writer, U8Value{Value: value.Discriminant}) + if err != nil { + return err + } + + for _, field := range value.Fields { + err := c.doEncodeNested(writer, field.Value) + if err != nil { + return fmt.Errorf("cannot encode field '%s' of enum, because of: %w", field.Name, err) + } + } + + return nil +} + +func (c *codec) encodeTopLevelEnum(writer io.Writer, value EnumValue) error { + if value.Discriminant == 0 && len(value.Fields) == 0 { + // Write nothing + return nil + } + + return c.encodeNestedEnum(writer, value) +} + +func (c *codec) decodeNestedEnum(reader io.Reader, value *EnumValue) error { + discriminant := &U8Value{} + err := c.doDecodeNested(reader, discriminant) + if err != nil { + return err + } + + value.Discriminant = discriminant.Value + + for _, field := range value.Fields { + err := c.doDecodeNested(reader, field.Value) + if err != nil { + return fmt.Errorf("cannot decode field '%s' of enum, because of: %w", field.Name, err) + } + } + + return nil +} + +func (c *codec) decodeTopLevelEnum(data []byte, value *EnumValue) error { + if len(data) == 0 { + value.Discriminant = 0 + return nil + } + + reader := bytes.NewReader(data) + return c.decodeNestedEnum(reader, value) +} diff --git a/abi/codecForCustomTypes_struct.go b/abi/codecForCustomTypes_struct.go new file mode 100644 index 0000000..ea7e48d --- /dev/null +++ b/abi/codecForCustomTypes_struct.go @@ -0,0 +1,38 @@ +package abi + +import ( + "bytes" + "fmt" + "io" +) + +func (c *codec) encodeNestedStruct(writer io.Writer, value StructValue) error { + for _, field := range value.Fields { + err := c.doEncodeNested(writer, field.Value) + if err != nil { + return fmt.Errorf("cannot encode field '%s' of struct, because of: %w", field.Name, err) + } + } + + return nil +} + +func (c *codec) encodeTopLevelStruct(writer io.Writer, value StructValue) error { + return c.encodeNestedStruct(writer, value) +} + +func (c *codec) decodeNestedStruct(reader io.Reader, value *StructValue) error { + for _, field := range value.Fields { + err := c.doDecodeNested(reader, field.Value) + if err != nil { + return fmt.Errorf("cannot decode field '%s' of struct, because of: %w", field.Name, err) + } + } + + return nil +} + +func (c *codec) decodeTopLevelStruct(data []byte, value *StructValue) error { + reader := bytes.NewReader(data) + return c.decodeNestedStruct(reader, value) +} diff --git a/abi/codecForSimpleValues_address.go b/abi/codecForSimpleValues_address.go new file mode 100644 index 0000000..3600259 --- /dev/null +++ b/abi/codecForSimpleValues_address.go @@ -0,0 +1,48 @@ +package abi + +import ( + "fmt" + "io" +) + +func (c *codec) encodeNestedAddress(writer io.Writer, value AddressValue) error { + return c.encodeTopLevelAddress(writer, value) +} + +func (c *codec) encodeTopLevelAddress(writer io.Writer, value AddressValue) error { + err := c.checkPubKeyLength(value.Value) + if err != nil { + return err + } + + _, err = writer.Write(value.Value) + return err +} + +func (c *codec) decodeNestedAddress(reader io.Reader, value *AddressValue) error { + data, err := readBytesExactly(reader, c.pubKeyLength) + if err != nil { + return err + } + + value.Value = data + return nil +} + +func (c *codec) decodeTopLevelAddress(data []byte, value *AddressValue) error { + err := c.checkPubKeyLength(data) + if err != nil { + return err + } + + value.Value = data + return nil +} + +func (c *codec) checkPubKeyLength(pubkey []byte) error { + if len(pubkey) != c.pubKeyLength { + return fmt.Errorf("public key (address) has invalid length: %d", len(pubkey)) + } + + return nil +} diff --git a/abi/codecForSimpleValues_boolean.go b/abi/codecForSimpleValues_boolean.go new file mode 100644 index 0000000..98cab65 --- /dev/null +++ b/abi/codecForSimpleValues_boolean.go @@ -0,0 +1,70 @@ +package abi + +import ( + "fmt" + "io" +) + +func (c *codec) encodeNestedBool(writer io.Writer, value BoolValue) error { + if value.Value { + _, err := writer.Write([]byte{trueAsByte}) + return err + } + + _, err := writer.Write([]byte{falseAsByte}) + return err +} + +func (c *codec) decodeNestedBool(reader io.Reader, value *BoolValue) error { + data, err := readBytesExactly(reader, 1) + if err != nil { + return err + } + + value.Value, err = c.byteToBool(data[0]) + if err != nil { + return err + } + + return nil +} + +func (c *codec) encodeTopLevelBool(writer io.Writer, value BoolValue) error { + if !value.Value { + // For "false", write nothing. + return nil + } + + _, err := writer.Write([]byte{trueAsByte}) + return err +} + +func (c *codec) decodeTopLevelBool(data []byte, value *BoolValue) error { + if len(data) == 0 { + value.Value = false + return nil + } + + if len(data) == 1 { + boolValue, err := c.byteToBool(data[0]) + if err != nil { + return err + } + + value.Value = boolValue + return nil + } + + return fmt.Errorf("unexpected boolean value: %v", data) +} + +func (c *codec) byteToBool(data uint8) (bool, error) { + switch data { + case trueAsByte: + return true, nil + case falseAsByte: + return false, nil + default: + return false, fmt.Errorf("unexpected boolean value: %d", data) + } +} diff --git a/abi/codecForSimpleValues_bytes.go b/abi/codecForSimpleValues_bytes.go new file mode 100644 index 0000000..6c4eb5c --- /dev/null +++ b/abi/codecForSimpleValues_bytes.go @@ -0,0 +1,30 @@ +package abi + +import ( + "io" +) + +func (c *codec) encodeNestedBytes(writer io.Writer, value BytesValue) error { + err := encodeLength(writer, uint32(len(value.Value))) + if err != nil { + return err + } + + _, err = writer.Write(value.Value) + return err +} + +func (c *codec) decodeNestedBytes(reader io.Reader, value *BytesValue) error { + length, err := decodeLength(reader) + if err != nil { + return err + } + + data, err := readBytesExactly(reader, int(length)) + if err != nil { + return err + } + + value.Value = data + return nil +} diff --git a/abi/codecForSimpleValues_numerical.go b/abi/codecForSimpleValues_numerical.go new file mode 100644 index 0000000..898c46a --- /dev/null +++ b/abi/codecForSimpleValues_numerical.go @@ -0,0 +1,138 @@ +package abi + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "math/big" + + twos "github.com/multiversx/mx-components-big-int/twos-complement" +) + +func (c *codec) encodeNestedNumber(writer io.Writer, value any, numBytes int) error { + buffer := new(bytes.Buffer) + + err := binary.Write(buffer, binary.BigEndian, value) + if err != nil { + return err + } + + data := buffer.Bytes() + if len(data) != numBytes { + return fmt.Errorf("unexpected number of bytes: %d != %d", len(data), numBytes) + } + + _, err = writer.Write(data) + if err != nil { + return err + } + + return nil +} + +func (c *codec) decodeNestedNumber(reader io.Reader, value any, numBytes int) error { + data, err := readBytesExactly(reader, numBytes) + if err != nil { + return err + } + + buffer := bytes.NewReader(data) + err = binary.Read(buffer, binary.BigEndian, value) + if err != nil { + return err + } + + return nil +} + +func (c *codec) encodeTopLevelUnsignedNumber(writer io.Writer, value uint64) error { + b := big.NewInt(0).SetUint64(value) + data := b.Bytes() + _, err := writer.Write(data) + return err +} + +func (c *codec) encodeTopLevelSignedNumber(writer io.Writer, value int64) error { + data := twos.ToBytes(big.NewInt(value)) + _, err := writer.Write(data) + return err +} + +func (c *codec) decodeTopLevelUnsignedNumber(data []byte, maxValue uint64) (uint64, error) { + b := big.NewInt(0).SetBytes(data) + if !b.IsUint64() { + return 0, fmt.Errorf("decoded value is too large (does not fit an uint64): %s", b) + } + + n := b.Uint64() + if n > maxValue { + return 0, fmt.Errorf("decoded value is too large: %d > %d", n, maxValue) + } + + return n, nil +} + +func (c *codec) decodeTopLevelSignedNumber(data []byte, maxValue int64) (int64, error) { + b := twos.FromBytes(data) + + if !b.IsInt64() { + return 0, fmt.Errorf("decoded value is too large (does not fit an int64): %s", b) + } + + n := b.Int64() + if n > maxValue { + return 0, fmt.Errorf("decoded value is too large: %d > %d", n, maxValue) + } + + return n, nil +} + +func (c *codec) encodeNestedBigNumber(writer io.Writer, value *big.Int) error { + data := twos.ToBytes(value) + dataLength := len(data) + + // Write the length of the payload + err := encodeLength(writer, uint32(dataLength)) + if err != nil { + return err + } + + // Write the payload + _, err = writer.Write(data) + if err != nil { + return err + } + + return nil +} + +func (c *codec) encodeTopLevelBigNumber(writer io.Writer, value *big.Int) error { + data := twos.ToBytes(value) + _, err := writer.Write(data) + if err != nil { + return err + } + + return nil +} + +func (c *codec) decodeNestedBigNumber(reader io.Reader) (*big.Int, error) { + // Read the length of the payload + length, err := decodeLength(reader) + if err != nil { + return nil, err + } + + // Read the payload + data, err := readBytesExactly(reader, int(length)) + if err != nil { + return nil, err + } + + return twos.FromBytes(data), nil +} + +func (c *codec) decodeTopLevelBigNumber(data []byte) *big.Int { + return twos.FromBytes(data) +} diff --git a/abi/codecForSimpleValues_string.go b/abi/codecForSimpleValues_string.go new file mode 100644 index 0000000..a8de1cc --- /dev/null +++ b/abi/codecForSimpleValues_string.go @@ -0,0 +1,31 @@ +package abi + +import ( + "io" +) + +func (c *codec) encodeNestedString(writer io.Writer, value StringValue) error { + data := []byte(value.Value) + err := encodeLength(writer, uint32(len(data))) + if err != nil { + return err + } + + _, err = writer.Write(data) + return err +} + +func (c *codec) decodeNestedString(reader io.Reader, value *StringValue) error { + length, err := decodeLength(reader) + if err != nil { + return err + } + + data, err := readBytesExactly(reader, int(length)) + if err != nil { + return err + } + + value.Value = string(data) + return nil +} diff --git a/abi/codec_test.go b/abi/codec_test.go new file mode 100644 index 0000000..3f4bb84 --- /dev/null +++ b/abi/codec_test.go @@ -0,0 +1,907 @@ +package abi + +import ( + "encoding/hex" + "math/big" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewCodec(t *testing.T) { + t.Run("should work", func(t *testing.T) { + codec, err := newCodec(argsNewCodec{ + pubKeyLength: 32, + }) + + require.NoError(t, err) + require.NotNil(t, codec) + }) + + t.Run("should err if bad public key length", func(t *testing.T) { + _, err := newCodec(argsNewCodec{ + pubKeyLength: 0, + }) + + require.ErrorContains(t, err, "bad public key length") + }) +} + +func TestCodec_EncodeNested(t *testing.T) { + codec, _ := newCodec(argsNewCodec{ + pubKeyLength: 32, + }) + + doTest := func(t *testing.T, value any, expected string) { + encoded, err := codec.EncodeNested(value) + require.NoError(t, err) + require.Equal(t, expected, hex.EncodeToString(encoded)) + } + + t.Run("bool", func(t *testing.T) { + doTest(t, BoolValue{Value: false}, "00") + doTest(t, BoolValue{Value: true}, "01") + }) + + t.Run("u8, i8", func(t *testing.T) { + doTest(t, U8Value{Value: 0x00}, "00") + doTest(t, U8Value{Value: 0x01}, "01") + doTest(t, U8Value{Value: 0x42}, "42") + doTest(t, U8Value{Value: 0xff}, "ff") + + doTest(t, I8Value{Value: 0x00}, "00") + doTest(t, I8Value{Value: 0x01}, "01") + doTest(t, I8Value{Value: -1}, "ff") + doTest(t, I8Value{Value: -128}, "80") + doTest(t, I8Value{Value: 127}, "7f") + }) + + t.Run("u16, i16", func(t *testing.T) { + doTest(t, U16Value{Value: 0x00}, "0000") + doTest(t, U16Value{Value: 0x11}, "0011") + doTest(t, U16Value{Value: 0x1234}, "1234") + doTest(t, U16Value{Value: 0xffff}, "ffff") + + doTest(t, I16Value{Value: 0x0000}, "0000") + doTest(t, I16Value{Value: 0x0011}, "0011") + doTest(t, I16Value{Value: -1}, "ffff") + doTest(t, I16Value{Value: -32768}, "8000") + }) + + t.Run("u32, i32", func(t *testing.T) { + doTest(t, U32Value{Value: 0x00000000}, "00000000") + doTest(t, U32Value{Value: 0x00000011}, "00000011") + doTest(t, U32Value{Value: 0x00001122}, "00001122") + doTest(t, U32Value{Value: 0x00112233}, "00112233") + doTest(t, U32Value{Value: 0x11223344}, "11223344") + doTest(t, U32Value{Value: 0xffffffff}, "ffffffff") + + doTest(t, I32Value{Value: 0x00000000}, "00000000") + doTest(t, I32Value{Value: 0x00000011}, "00000011") + doTest(t, I32Value{Value: -1}, "ffffffff") + doTest(t, I32Value{Value: -2147483648}, "80000000") + }) + + t.Run("u64, i64", func(t *testing.T) { + doTest(t, U64Value{Value: 0x0000000000000000}, "0000000000000000") + doTest(t, U64Value{Value: 0x0000000000000011}, "0000000000000011") + doTest(t, U64Value{Value: 0x0000000000001122}, "0000000000001122") + doTest(t, U64Value{Value: 0x0000000000112233}, "0000000000112233") + doTest(t, U64Value{Value: 0x0000000011223344}, "0000000011223344") + doTest(t, U64Value{Value: 0x0000001122334455}, "0000001122334455") + doTest(t, U64Value{Value: 0x0000112233445566}, "0000112233445566") + doTest(t, U64Value{Value: 0x0011223344556677}, "0011223344556677") + doTest(t, U64Value{Value: 0x1122334455667788}, "1122334455667788") + doTest(t, U64Value{Value: 0xffffffffffffffff}, "ffffffffffffffff") + + doTest(t, I64Value{Value: 0x0000000000000000}, "0000000000000000") + doTest(t, I64Value{Value: 0x0000000000000011}, "0000000000000011") + doTest(t, I64Value{Value: -1}, "ffffffffffffffff") + }) + + t.Run("bigInt", func(t *testing.T) { + doTest(t, BigIntValue{Value: big.NewInt(0)}, "00000000") + doTest(t, BigIntValue{Value: big.NewInt(1)}, "0000000101") + doTest(t, BigIntValue{Value: big.NewInt(-1)}, "00000001ff") + }) + + t.Run("address", func(t *testing.T) { + data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") + doTest(t, AddressValue{Value: data}, "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") + }) + + t.Run("address (bad)", func(t *testing.T) { + data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d42") + _, err := codec.EncodeNested(AddressValue{Value: data}) + require.ErrorContains(t, err, "public key (address) has invalid length") + }) + + t.Run("string", func(t *testing.T) { + doTest(t, StringValue{Value: ""}, "00000000") + doTest(t, StringValue{Value: "abc"}, "00000003616263") + }) + + t.Run("bytes", func(t *testing.T) { + doTest(t, BytesValue{Value: []byte{}}, "00000000") + doTest(t, BytesValue{Value: []byte{'a', 'b', 'c'}}, "00000003616263") + }) + + t.Run("struct", func(t *testing.T) { + fooStruct := StructValue{ + Fields: []Field{ + { + Value: U8Value{Value: 0x01}, + }, + { + Value: U16Value{Value: 0x4142}, + }, + }, + } + + doTest(t, fooStruct, "014142") + }) + + t.Run("enum (discriminant == 0)", func(t *testing.T) { + fooEnum := EnumValue{ + Discriminant: 0, + } + + doTest(t, fooEnum, "00") + }) + + t.Run("enum (discriminant != 0)", func(t *testing.T) { + fooEnum := EnumValue{ + Discriminant: 42, + } + + doTest(t, fooEnum, "2a") + }) + + t.Run("enum with Fields", func(t *testing.T) { + fooEnum := EnumValue{ + Discriminant: 42, + Fields: []Field{ + { + Value: U8Value{Value: 0x01}, + }, + { + Value: U16Value{Value: 0x4142}, + }, + }, + } + + doTest(t, fooEnum, "2a014142") + }) + + t.Run("option with value", func(t *testing.T) { + fooOption := OptionValue{ + Value: U16Value{Value: 0x08}, + } + + doTest(t, fooOption, "010008") + }) + + t.Run("option without value", func(t *testing.T) { + fooOption := OptionValue{ + Value: nil, + } + + doTest(t, fooOption, "00") + }) + + t.Run("list", func(t *testing.T) { + fooList := InputListValue{ + Items: []any{ + U16Value{Value: 1}, + U16Value{Value: 2}, + U16Value{Value: 3}, + }, + } + + doTest(t, fooList, "00000003000100020003") + }) + + t.Run("should err when unknown type", func(t *testing.T) { + type dummy struct { + foobar string + } + + encoded, err := codec.EncodeNested(&dummy{foobar: "hello"}) + require.ErrorContains(t, err, "unsupported type for nested encoding: *abi.dummy") + require.Nil(t, encoded) + }) +} + +func TestCodec_EncodeTopLevel(t *testing.T) { + codec, _ := newCodec(argsNewCodec{ + pubKeyLength: 32, + }) + + doTest := func(t *testing.T, value any, expected string) { + encoded, err := codec.EncodeTopLevel(value) + require.NoError(t, err) + require.Equal(t, expected, hex.EncodeToString(encoded)) + } + + t.Run("bool", func(t *testing.T) { + doTest(t, BoolValue{Value: false}, "") + doTest(t, BoolValue{Value: true}, "01") + }) + + t.Run("u8, i8", func(t *testing.T) { + doTest(t, U8Value{Value: 0x00}, "") + doTest(t, U8Value{Value: 0x01}, "01") + + doTest(t, I8Value{Value: 0x00}, "") + doTest(t, I8Value{Value: 0x01}, "01") + doTest(t, I8Value{Value: -1}, "ff") + }) + + t.Run("u16, i16", func(t *testing.T) { + doTest(t, U16Value{Value: 0x0042}, "42") + + doTest(t, I16Value{Value: 0x0000}, "") + doTest(t, I16Value{Value: 0x0011}, "11") + doTest(t, I16Value{Value: -1}, "ff") + }) + + t.Run("u32, i32", func(t *testing.T) { + doTest(t, U32Value{Value: 0x00004242}, "4242") + + doTest(t, I32Value{Value: 0x00000000}, "") + doTest(t, I32Value{Value: 0x00000011}, "11") + doTest(t, I32Value{Value: -1}, "ff") + }) + + t.Run("u64, i64", func(t *testing.T) { + doTest(t, U64Value{Value: 0x0042434445464748}, "42434445464748") + + doTest(t, I64Value{Value: 0x0000000000000000}, "") + doTest(t, I64Value{Value: 0x0000000000000011}, "11") + doTest(t, I64Value{Value: -1}, "ff") + }) + + t.Run("bigInt", func(t *testing.T) { + doTest(t, BigIntValue{Value: big.NewInt(0)}, "") + doTest(t, BigIntValue{Value: big.NewInt(1)}, "01") + doTest(t, BigIntValue{Value: big.NewInt(-1)}, "ff") + }) + + t.Run("address", func(t *testing.T) { + data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") + doTest(t, AddressValue{Value: data}, "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") + }) + + t.Run("address (bad)", func(t *testing.T) { + data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d42") + _, err := codec.EncodeTopLevel(AddressValue{Value: data}) + require.ErrorContains(t, err, "public key (address) has invalid length") + }) + + t.Run("struct", func(t *testing.T) { + fooStruct := StructValue{ + Fields: []Field{ + { + Value: U8Value{Value: 0x01}, + }, + { + Value: U16Value{Value: 0x4142}, + }, + }, + } + + doTest(t, fooStruct, "014142") + }) + + t.Run("enum (discriminant == 0)", func(t *testing.T) { + fooEnum := EnumValue{ + Discriminant: 0, + } + + doTest(t, fooEnum, "") + }) + + t.Run("enum (discriminant != 0)", func(t *testing.T) { + fooEnum := EnumValue{ + Discriminant: 42, + } + + doTest(t, fooEnum, "2a") + }) + + t.Run("enum with Fields", func(t *testing.T) { + fooEnum := EnumValue{ + Discriminant: 42, + Fields: []Field{ + { + Value: U8Value{Value: 0x01}, + }, + { + Value: U16Value{Value: 0x4142}, + }, + }, + } + + doTest(t, fooEnum, "2a014142") + }) + + t.Run("should err when unknown type", func(t *testing.T) { + type dummy struct { + foobar string + } + + encoded, err := codec.EncodeTopLevel(&dummy{foobar: "hello"}) + require.ErrorContains(t, err, "unsupported type for top-level encoding: *abi.dummy") + require.Nil(t, encoded) + }) +} + +func TestCodec_DecodeNested(t *testing.T) { + codec, _ := newCodec(argsNewCodec{ + pubKeyLength: 32, + }) + + t.Run("bool (true)", func(t *testing.T) { + data, _ := hex.DecodeString("01") + destination := &BoolValue{} + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &BoolValue{Value: true}, destination) + }) + + t.Run("bool (false)", func(t *testing.T) { + data, _ := hex.DecodeString("00") + destination := &BoolValue{} + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &BoolValue{Value: false}, destination) + }) + + t.Run("u8", func(t *testing.T) { + data, _ := hex.DecodeString("01") + destination := &U8Value{} + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &U8Value{Value: 0x01}, destination) + }) + + t.Run("i8", func(t *testing.T) { + data, _ := hex.DecodeString("ff") + destination := &I8Value{} + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &I8Value{Value: -1}, destination) + }) + + t.Run("u16", func(t *testing.T) { + data, _ := hex.DecodeString("4142") + destination := &U16Value{} + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &U16Value{Value: 0x4142}, destination) + }) + + t.Run("i16", func(t *testing.T) { + data, _ := hex.DecodeString("ffff") + destination := &I16Value{} + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &I16Value{Value: -1}, destination) + }) + + t.Run("u32", func(t *testing.T) { + data, _ := hex.DecodeString("41424344") + destination := &U32Value{} + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &U32Value{Value: 0x41424344}, destination) + }) + + t.Run("i32", func(t *testing.T) { + data, _ := hex.DecodeString("ffffffff") + destination := &I32Value{} + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &I32Value{Value: -1}, destination) + }) + + t.Run("u64", func(t *testing.T) { + data, _ := hex.DecodeString("4142434445464748") + destination := &U64Value{} + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &U64Value{Value: 0x4142434445464748}, destination) + }) + + t.Run("i64", func(t *testing.T) { + data, _ := hex.DecodeString("ffffffffffffffff") + destination := &I64Value{} + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &I64Value{Value: -1}, destination) + }) + + t.Run("u16, should err because it cannot read 2 bytes", func(t *testing.T) { + data, _ := hex.DecodeString("01") + destination := &U16Value{} + + err := codec.DecodeNested(data, destination) + require.ErrorContains(t, err, "cannot read exactly 2 bytes") + }) + + t.Run("u32, should err because it cannot read 4 bytes", func(t *testing.T) { + data, _ := hex.DecodeString("4142") + destination := &U32Value{} + + err := codec.DecodeNested(data, destination) + require.ErrorContains(t, err, "cannot read exactly 4 bytes") + }) + + t.Run("u64, should err because it cannot read 8 bytes", func(t *testing.T) { + data, _ := hex.DecodeString("41424344") + destination := &U64Value{} + + err := codec.DecodeNested(data, destination) + require.ErrorContains(t, err, "cannot read exactly 8 bytes") + }) + + t.Run("bigInt", func(t *testing.T) { + data, _ := hex.DecodeString("00000000") + destination := &BigIntValue{} + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &BigIntValue{Value: big.NewInt(0)}, destination) + + data, _ = hex.DecodeString("0000000101") + destination = &BigIntValue{} + err = codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &BigIntValue{Value: big.NewInt(1)}, destination) + + data, _ = hex.DecodeString("00000001ff") + destination = &BigIntValue{} + err = codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &BigIntValue{Value: big.NewInt(-1)}, destination) + }) + + t.Run("bigInt: should err when bad data", func(t *testing.T) { + data, _ := hex.DecodeString("0000000301") + destination := &BigIntValue{} + err := codec.DecodeNested(data, destination) + require.ErrorContains(t, err, "cannot decode (nested) *abi.BigIntValue, because of: cannot read exactly 3 bytes") + }) + + t.Run("address", func(t *testing.T) { + data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") + + destination := &AddressValue{} + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &AddressValue{Value: data}, destination) + }) + + t.Run("address (bad)", func(t *testing.T) { + data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d42") + + destination := &AddressValue{} + err := codec.DecodeNested(data, destination) + require.ErrorContains(t, err, "cannot read exactly 32 bytes") + }) + + t.Run("string", func(t *testing.T) { + data, _ := hex.DecodeString("00000000") + destination := &StringValue{} + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &StringValue{}, destination) + + data, _ = hex.DecodeString("00000003616263") + destination = &StringValue{} + err = codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &StringValue{Value: "abc"}, destination) + }) + + t.Run("bytes", func(t *testing.T) { + data, _ := hex.DecodeString("00000000") + destination := &BytesValue{} + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &BytesValue{Value: []byte{}}, destination) + + data, _ = hex.DecodeString("00000003616263") + destination = &BytesValue{} + err = codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &BytesValue{Value: []byte{'a', 'b', 'c'}}, destination) + }) + + t.Run("struct", func(t *testing.T) { + data, _ := hex.DecodeString("014142") + + destination := &StructValue{ + Fields: []Field{ + { + Value: &U8Value{}, + }, + { + Value: &U16Value{}, + }, + }, + } + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &StructValue{ + Fields: []Field{ + { + Value: &U8Value{Value: 0x01}, + }, + { + Value: &U16Value{Value: 0x4142}, + }, + }, + }, destination) + }) + + t.Run("enum (discriminant == 0)", func(t *testing.T) { + data, _ := hex.DecodeString("00") + destination := &EnumValue{} + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &EnumValue{ + Discriminant: 0x00, + }, destination) + }) + + t.Run("enum (discriminant != 0)", func(t *testing.T) { + data, _ := hex.DecodeString("01") + destination := &EnumValue{} + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &EnumValue{ + Discriminant: 0x01, + }, destination) + }) + + t.Run("enum with Fields", func(t *testing.T) { + data, _ := hex.DecodeString("01014142") + + destination := &EnumValue{ + Fields: []Field{ + { + Value: &U8Value{}, + }, + { + Value: &U16Value{}, + }, + }, + } + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &EnumValue{ + Discriminant: 0x01, + Fields: []Field{ + { + Value: &U8Value{Value: 0x01}, + }, + { + Value: &U16Value{Value: 0x4142}, + }, + }, + }, destination) + }) + + t.Run("option with value", func(t *testing.T) { + data, _ := hex.DecodeString("010008") + + destination := &OptionValue{ + Value: &U16Value{}, + } + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &OptionValue{ + Value: &U16Value{Value: 8}, + }, destination) + }) + + t.Run("option without value", func(t *testing.T) { + data, _ := hex.DecodeString("00") + + destination := &OptionValue{ + Value: &U16Value{}, + } + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, &OptionValue{ + Value: nil, + }, destination) + }) + + t.Run("list", func(t *testing.T) { + data, _ := hex.DecodeString("00000003000100020003") + + destination := &OutputListValue{ + ItemCreator: func() any { return &U16Value{} }, + Items: []any{}, + } + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, + []any{ + &U16Value{Value: 1}, + &U16Value{Value: 2}, + &U16Value{Value: 3}, + }, destination.Items) + }) + + t.Run("should err when unknown type", func(t *testing.T) { + type dummy struct { + foobar string + } + + err := codec.DecodeNested([]byte{0x00}, &dummy{foobar: "hello"}) + require.ErrorContains(t, err, "unsupported type for nested decoding: *abi.dummy") + }) +} + +func TestCodec_DecodeTopLevel(t *testing.T) { + codec, _ := newCodec(argsNewCodec{ + pubKeyLength: 32, + }) + + t.Run("bool (true)", func(t *testing.T) { + data, _ := hex.DecodeString("01") + destination := &BoolValue{} + + err := codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &BoolValue{Value: true}, destination) + }) + + t.Run("bool (false)", func(t *testing.T) { + data, _ := hex.DecodeString("") + destination := &BoolValue{} + + err := codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &BoolValue{Value: false}, destination) + }) + + t.Run("u8", func(t *testing.T) { + data, _ := hex.DecodeString("01") + destination := &U8Value{} + + err := codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &U8Value{Value: 0x01}, destination) + }) + + t.Run("i8", func(t *testing.T) { + data, _ := hex.DecodeString("ff") + destination := &I8Value{} + + err := codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &I8Value{Value: -1}, destination) + }) + + t.Run("u16", func(t *testing.T) { + data, _ := hex.DecodeString("02") + destination := &U16Value{} + + err := codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &U16Value{Value: 0x0002}, destination) + }) + + t.Run("i16", func(t *testing.T) { + data, _ := hex.DecodeString("ffff") + destination := &I16Value{} + + err := codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &I16Value{Value: -1}, destination) + }) + + t.Run("u32", func(t *testing.T) { + data, _ := hex.DecodeString("03") + destination := &U32Value{} + + err := codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &U32Value{Value: 0x00000003}, destination) + }) + + t.Run("i32", func(t *testing.T) { + data, _ := hex.DecodeString("ffffffff") + destination := &I32Value{} + + err := codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &I32Value{Value: -1}, destination) + }) + + t.Run("u64", func(t *testing.T) { + data, _ := hex.DecodeString("04") + destination := &U64Value{} + + err := codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &U64Value{Value: 0x0000000000000004}, destination) + }) + + t.Run("i64", func(t *testing.T) { + data, _ := hex.DecodeString("ffffffffffffffff") + destination := &I64Value{} + + err := codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &I64Value{Value: -1}, destination) + }) + + t.Run("u8, i8: should err because decoded value is too large", func(t *testing.T) { + data, _ := hex.DecodeString("4142") + + err := codec.DecodeTopLevel(data, &U8Value{}) + require.ErrorContains(t, err, "decoded value is too large") + + err = codec.DecodeTopLevel(data, &I8Value{}) + require.ErrorContains(t, err, "decoded value is too large") + }) + + t.Run("u16, i16: should err because decoded value is too large", func(t *testing.T) { + data, _ := hex.DecodeString("41424344") + + err := codec.DecodeTopLevel(data, &U16Value{}) + require.ErrorContains(t, err, "decoded value is too large") + + err = codec.DecodeTopLevel(data, &I16Value{}) + require.ErrorContains(t, err, "decoded value is too large") + }) + + t.Run("u32, i32: should err because decoded value is too large", func(t *testing.T) { + data, _ := hex.DecodeString("4142434445464748") + + err := codec.DecodeTopLevel(data, &U32Value{}) + require.ErrorContains(t, err, "decoded value is too large") + + err = codec.DecodeTopLevel(data, &I32Value{}) + require.ErrorContains(t, err, "decoded value is too large") + }) + + t.Run("u64, i64: should err because decoded value is too large", func(t *testing.T) { + data, _ := hex.DecodeString("41424344454647489876") + + err := codec.DecodeTopLevel(data, &U64Value{}) + require.ErrorContains(t, err, "decoded value is too large") + + err = codec.DecodeTopLevel(data, &I64Value{}) + require.ErrorContains(t, err, "decoded value is too large") + }) + + t.Run("bigInt", func(t *testing.T) { + data, _ := hex.DecodeString("") + destination := &BigIntValue{} + err := codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &BigIntValue{Value: big.NewInt(0)}, destination) + + data, _ = hex.DecodeString("01") + destination = &BigIntValue{} + err = codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &BigIntValue{Value: big.NewInt(1)}, destination) + + data, _ = hex.DecodeString("ff") + destination = &BigIntValue{} + err = codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &BigIntValue{Value: big.NewInt(-1)}, destination) + }) + + t.Run("struct", func(t *testing.T) { + data, _ := hex.DecodeString("014142") + + destination := &StructValue{ + Fields: []Field{ + { + Value: &U8Value{}, + }, + { + Value: &U16Value{}, + }, + }, + } + + err := codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &StructValue{ + Fields: []Field{ + { + Value: &U8Value{Value: 0x01}, + }, + { + Value: &U16Value{Value: 0x4142}, + }, + }, + }, destination) + }) + + t.Run("enum (discriminant == 0)", func(t *testing.T) { + data, _ := hex.DecodeString("") + destination := &EnumValue{} + + err := codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &EnumValue{ + Discriminant: 0x00, + }, destination) + }) + + t.Run("enum (discriminant != 0)", func(t *testing.T) { + data, _ := hex.DecodeString("01") + destination := &EnumValue{} + + err := codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &EnumValue{ + Discriminant: 0x01, + }, destination) + }) + + t.Run("enum with Fields", func(t *testing.T) { + data, _ := hex.DecodeString("01014142") + + destination := &EnumValue{ + Fields: []Field{ + { + Value: &U8Value{}, + }, + { + Value: &U16Value{}, + }, + }, + } + + err := codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &EnumValue{ + Discriminant: 0x01, + Fields: []Field{ + { + Value: &U8Value{Value: 0x01}, + }, + { + Value: &U16Value{Value: 0x4142}, + }, + }, + }, destination) + }) + + t.Run("should err when unknown type", func(t *testing.T) { + type dummy struct { + foobar string + } + + err := codec.DecodeTopLevel([]byte{0x00}, &dummy{foobar: "hello"}) + require.ErrorContains(t, err, "unsupported type for top-level decoding: *abi.dummy") + }) +} diff --git a/abi/constants.go b/abi/constants.go new file mode 100644 index 0000000..aa4c956 --- /dev/null +++ b/abi/constants.go @@ -0,0 +1,4 @@ +package abi + +const trueAsByte = uint8(1) +const falseAsByte = uint8(0) diff --git a/abi/parts.go b/abi/parts.go new file mode 100644 index 0000000..0523f3d --- /dev/null +++ b/abi/parts.go @@ -0,0 +1,99 @@ +package abi + +import ( + "errors" + "fmt" +) + +// partsHolder holds data parts (e.g. raw contract call arguments, raw contract return values). +// It allows one to easily construct parts (thus functioning as a builder of parts). +// It also allows one to focus on a specific part to read from (thus functioning as a reader of parts: think of a pick-up head). +// Both functionalities (building and reading) are kept within this single abstraction, for convenience. +type partsHolder struct { + parts [][]byte + focusedPartIndex uint32 +} + +// newPartsHolder creates a new partsHolder, which has the given parts. +// Focus is on the first part, if any, or "beyond the last part" otherwise. +func newPartsHolder(parts [][]byte) *partsHolder { + return &partsHolder{ + parts: parts, + focusedPartIndex: 0, + } +} + +// newEmptyPartsHolder creates a new partsHolder, which has no parts. +// Parts are created by calling appendEmptyPart(). +// Focus is "beyond the last part" (since there is no part). +func newEmptyPartsHolder() *partsHolder { + return &partsHolder{ + parts: [][]byte{}, + focusedPartIndex: 0, + } +} + +func (holder *partsHolder) getParts() [][]byte { + return holder.parts +} + +func (holder *partsHolder) getNumParts() uint32 { + return uint32(len(holder.parts)) +} + +func (holder *partsHolder) getPart(index uint32) ([]byte, error) { + if index >= holder.getNumParts() { + return nil, fmt.Errorf("part index %d is out of range", index) + } + + return holder.parts[index], nil +} + +func (holder *partsHolder) appendToLastPart(data []byte) error { + if !holder.hasAnyPart() { + return errors.New("cannot write, since there is no part to write to") + } + + holder.parts[len(holder.parts)-1] = append(holder.parts[len(holder.parts)-1], data...) + return nil +} + +func (holder *partsHolder) hasAnyPart() bool { + return len(holder.parts) > 0 +} + +func (holder *partsHolder) appendEmptyPart() { + holder.parts = append(holder.parts, []byte{}) +} + +// readWholeFocusedPart reads the whole focused part, if any. Otherwise, it returns an error. +func (holder *partsHolder) readWholeFocusedPart() ([]byte, error) { + if holder.isFocusedBeyondLastPart() { + return nil, fmt.Errorf("cannot wholly read part %d: unexpected end of data", holder.focusedPartIndex) + } + + part, err := holder.getPart(uint32(holder.focusedPartIndex)) + if err != nil { + return nil, err + } + + return part, nil +} + +// focusOnNextPart focuses on the next part, if any. Otherwise, it returns an error. +func (holder *partsHolder) focusOnNextPart() error { + if holder.isFocusedBeyondLastPart() { + return fmt.Errorf( + "cannot focus on next part, since the focus is already beyond the last part; focused part index is %d", + holder.focusedPartIndex, + ) + } + + holder.focusedPartIndex++ + return nil +} + +// isFocusedBeyondLastPart returns true if the focus is already beyond the last part. +func (holder *partsHolder) isFocusedBeyondLastPart() bool { + return holder.focusedPartIndex >= holder.getNumParts() +} diff --git a/abi/serializer.go b/abi/serializer.go new file mode 100644 index 0000000..63f34d9 --- /dev/null +++ b/abi/serializer.go @@ -0,0 +1,246 @@ +package abi + +import ( + "encoding/hex" + "errors" + "strings" +) + +type serializer struct { + codec *codec + partsSeparator string +} + +// ArgsNewSerializer defines the arguments needed for a new serializer +type ArgsNewSerializer struct { + PartsSeparator string + PubKeyLength int +} + +// NewSerializer creates a new serializer. +// The serializer follows the rules of the MultiversX Serialization format: +// https://docs.multiversx.com/developers/data/serialization-overview +func NewSerializer(args ArgsNewSerializer) (*serializer, error) { + if args.PartsSeparator == "" { + return nil, errors.New("cannot create serializer: parts separator must not be empty") + } + + codec, err := newCodec(argsNewCodec{ + pubKeyLength: args.PubKeyLength, + }) + if err != nil { + return nil, err + } + + return &serializer{ + codec: codec, + partsSeparator: args.PartsSeparator, + }, nil +} + +// Serialize serializes the given input values into a string +func (s *serializer) Serialize(inputValues []any) (string, error) { + parts, err := s.serializeToParts(inputValues) + if err != nil { + return "", err + } + + return s.encodeParts(parts), nil +} + +func (s *serializer) serializeToParts(inputValues []any) ([][]byte, error) { + partsHolder := newEmptyPartsHolder() + + err := s.doSerialize(partsHolder, inputValues) + if err != nil { + return nil, err + } + + return partsHolder.getParts(), nil +} + +func (s *serializer) doSerialize(partsHolder *partsHolder, inputValues []any) error { + var err error + + for i, value := range inputValues { + if value == nil { + return errors.New("cannot serialize nil value") + } + + switch value := value.(type) { + case InputMultiValue: + err = s.serializeInputMultiValue(partsHolder, value) + case InputVariadicValues: + if i != len(inputValues)-1 { + return errors.New("variadic values must be last among input values") + } + + err = s.serializeInputVariadicValues(partsHolder, value) + default: + partsHolder.appendEmptyPart() + err = s.serializeDirectlyEncodableValue(partsHolder, value) + } + + if err != nil { + return err + } + } + + return nil +} + +// Deserialize deserializes the given data into the output values +func (s *serializer) Deserialize(data string, outputValues []any) error { + parts, err := s.decodeIntoParts(data) + if err != nil { + return err + } + + return s.deserializeParts(parts, outputValues) +} + +func (s *serializer) deserializeParts(parts [][]byte, outputValues []any) error { + partsHolder := newPartsHolder(parts) + + err := s.doDeserialize(partsHolder, outputValues) + if err != nil { + return err + } + + return nil +} + +func (s *serializer) doDeserialize(partsHolder *partsHolder, outputValues []any) error { + var err error + + for i, value := range outputValues { + if value == nil { + return errors.New("cannot deserialize into nil value") + } + + switch value := value.(type) { + case *OutputMultiValue: + err = s.deserializeOutputMultiValue(partsHolder, value) + case *OutputVariadicValues: + if i != len(outputValues)-1 { + return errors.New("variadic values must be last among output values") + } + + err = s.deserializeOutputVariadicValues(partsHolder, value) + default: + err = s.deserializeDirectlyEncodableValue(partsHolder, value) + } + + if err != nil { + return err + } + } + + return nil +} + +func (s *serializer) serializeInputMultiValue(partsHolder *partsHolder, value InputMultiValue) error { + for _, item := range value.Items { + err := s.doSerialize(partsHolder, []any{item}) + if err != nil { + return err + } + } + + return nil +} + +func (s *serializer) serializeInputVariadicValues(partsHolder *partsHolder, value InputVariadicValues) error { + for _, item := range value.Items { + err := s.doSerialize(partsHolder, []any{item}) + if err != nil { + return err + } + } + + return nil +} + +func (s *serializer) serializeDirectlyEncodableValue(partsHolder *partsHolder, value any) error { + data, err := s.codec.EncodeTopLevel(value) + if err != nil { + return err + } + + return partsHolder.appendToLastPart(data) +} + +func (s *serializer) deserializeOutputMultiValue(partsHolder *partsHolder, value *OutputMultiValue) error { + for _, item := range value.Items { + err := s.doDeserialize(partsHolder, []any{item}) + if err != nil { + return err + } + } + + return nil +} + +func (s *serializer) deserializeOutputVariadicValues(partsHolder *partsHolder, value *OutputVariadicValues) error { + if value.ItemCreator == nil { + return errors.New("cannot deserialize variadic values: item creator is nil") + } + + for !partsHolder.isFocusedBeyondLastPart() { + newItem := value.ItemCreator() + + err := s.doDeserialize(partsHolder, []any{newItem}) + if err != nil { + return err + } + + value.Items = append(value.Items, newItem) + } + + return nil +} + +func (s *serializer) deserializeDirectlyEncodableValue(partsHolder *partsHolder, value any) error { + part, err := partsHolder.readWholeFocusedPart() + if err != nil { + return err + } + + err = s.codec.DecodeTopLevel(part, value) + if err != nil { + return err + } + + err = partsHolder.focusOnNextPart() + if err != nil { + return err + } + + return nil +} + +func (s *serializer) encodeParts(parts [][]byte) string { + partsHex := make([]string, len(parts)) + + for i, part := range parts { + partsHex[i] = hex.EncodeToString(part) + } + + return strings.Join(partsHex, s.partsSeparator) +} + +func (s *serializer) decodeIntoParts(encoded string) ([][]byte, error) { + partsHex := strings.Split(encoded, s.partsSeparator) + parts := make([][]byte, len(partsHex)) + + for i, partHex := range partsHex { + part, err := hex.DecodeString(partHex) + if err != nil { + return nil, err + } + + parts[i] = part + } + + return parts, nil +} diff --git a/abi/serializer_test.go b/abi/serializer_test.go new file mode 100644 index 0000000..d717af9 --- /dev/null +++ b/abi/serializer_test.go @@ -0,0 +1,327 @@ +package abi + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSerializer_Serialize(t *testing.T) { + serializer, err := NewSerializer(ArgsNewSerializer{ + PartsSeparator: "@", + PubKeyLength: 32, + }) + require.NoError(t, err) + + t.Run("u8", func(t *testing.T) { + data, err := serializer.Serialize([]any{ + U8Value{Value: 0x42}, + }) + + require.NoError(t, err) + require.Equal(t, "42", data) + }) + + t.Run("u16", func(t *testing.T) { + data, err := serializer.Serialize([]any{ + U16Value{Value: 0x4243}, + }) + + require.NoError(t, err) + require.Equal(t, "4243", data) + }) + + t.Run("u8, u16", func(t *testing.T) { + data, err := serializer.Serialize([]any{ + U8Value{Value: 0x42}, + U16Value{Value: 0x4243}, + }) + + require.NoError(t, err) + require.Equal(t, "42@4243", data) + }) + + t.Run("multi", func(t *testing.T) { + data, err := serializer.Serialize([]any{ + InputMultiValue{ + Items: []any{ + U8Value{Value: 0x42}, + U16Value{Value: 0x4243}, + U32Value{Value: 0x42434445}, + }, + }, + }) + + require.NoError(t, err) + require.Equal(t, "42@4243@42434445", data) + }) + + t.Run("u8, multi", func(t *testing.T) { + data, err := serializer.Serialize([]any{ + U8Value{Value: 0x42}, + InputMultiValue{ + Items: []any{ + U8Value{Value: 0x42}, + U16Value{Value: 0x4243}, + U32Value{Value: 0x42434445}, + }, + }, + }) + + require.NoError(t, err) + require.Equal(t, "42@42@4243@42434445", data) + }) + + t.Run("multi, multi>", func(t *testing.T) { + data, err := serializer.Serialize([]any{ + InputMultiValue{ + Items: []any{ + InputMultiValue{ + Items: []any{ + U8Value{Value: 0x42}, + U16Value{Value: 0x4243}, + }, + }, + InputMultiValue{ + Items: []any{ + U8Value{Value: 0x44}, + U16Value{Value: 0x4445}, + }, + }, + }, + }, + }) + + require.NoError(t, err) + require.Equal(t, "42@4243@44@4445", data) + }) + + t.Run("variadic, of different types", func(t *testing.T) { + data, err := serializer.Serialize([]any{ + InputVariadicValues{ + Items: []any{ + U8Value{Value: 0x42}, + U16Value{Value: 0x4243}, + }, + }, + }) + + // For now, the serializer does not perform such a strict type check. + // Although doable, it would be slightly complex and, if done, might be even dropped in the future + // (with respect to the decoder that is embedded in Rust-based smart contracts). + require.Nil(t, err) + require.Equal(t, "42@4243", data) + }) + + t.Run("variadic, u8: should err because variadic must be last", func(t *testing.T) { + _, err := serializer.Serialize([]any{ + InputVariadicValues{ + Items: []any{ + U8Value{Value: 0x42}, + U8Value{Value: 0x43}, + }, + }, + U8Value{Value: 0x44}, + }) + + require.ErrorContains(t, err, "variadic values must be last among input values") + }) + + t.Run("u8, variadic", func(t *testing.T) { + data, err := serializer.Serialize([]any{ + U8Value{Value: 0x41}, + InputVariadicValues{ + Items: []any{ + U8Value{Value: 0x42}, + U8Value{Value: 0x43}, + }, + }, + }) + + require.Nil(t, err) + require.Equal(t, "41@42@43", data) + }) +} + +func TestSerializer_Deserialize(t *testing.T) { + serializer, err := NewSerializer(ArgsNewSerializer{ + PartsSeparator: "@", + PubKeyLength: 32, + }) + require.NoError(t, err) + + t.Run("nil destination", func(t *testing.T) { + err := serializer.Deserialize("", []any{nil}) + require.ErrorContains(t, err, "cannot deserialize into nil value") + }) + + t.Run("u8", func(t *testing.T) { + outputValues := []any{ + &U8Value{}, + } + + err := serializer.Deserialize("42", outputValues) + + require.Nil(t, err) + require.Equal(t, []any{ + &U8Value{Value: 0x42}, + }, outputValues) + }) + + t.Run("u16", func(t *testing.T) { + outputValues := []any{ + &U16Value{}, + } + + err := serializer.Deserialize("4243", outputValues) + + require.Nil(t, err) + require.Equal(t, []any{ + &U16Value{Value: 0x4243}, + }, outputValues) + }) + + t.Run("u8, u16", func(t *testing.T) { + outputValues := []any{ + &U8Value{}, + &U16Value{}, + } + + err := serializer.Deserialize("42@4243", outputValues) + + require.Nil(t, err) + require.Equal(t, []any{ + &U8Value{Value: 0x42}, + &U16Value{Value: 0x4243}, + }, outputValues) + }) + + t.Run("multi", func(t *testing.T) { + outputValues := []any{ + &OutputMultiValue{ + Items: []any{ + &U8Value{}, + &U16Value{}, + &U32Value{}, + }, + }, + } + + err := serializer.Deserialize("42@4243@42434445", outputValues) + + require.Nil(t, err) + require.Equal(t, []any{ + &OutputMultiValue{ + Items: []any{ + &U8Value{Value: 0x42}, + &U16Value{Value: 0x4243}, + &U32Value{Value: 0x42434445}, + }, + }, + }, outputValues) + }) + + t.Run("u8, multi", func(t *testing.T) { + outputValues := []any{ + &U8Value{}, + &OutputMultiValue{ + Items: []any{ + &U8Value{}, + &U16Value{}, + &U32Value{}, + }, + }, + } + + err := serializer.Deserialize("42@42@4243@42434445", outputValues) + + require.Nil(t, err) + require.Equal(t, []any{ + &U8Value{Value: 0x42}, + &OutputMultiValue{ + Items: []any{ + &U8Value{Value: 0x42}, + &U16Value{Value: 0x4243}, + &U32Value{Value: 0x42434445}, + }, + }, + }, outputValues) + }) + + t.Run("variadic, should err because of nil item creator", func(t *testing.T) { + destination := &OutputVariadicValues{ + Items: []any{}, + } + + err := serializer.Deserialize("", []any{destination}) + require.ErrorContains(t, err, "cannot deserialize variadic values: item creator is nil") + }) + + t.Run("empty: u8", func(t *testing.T) { + destination := &OutputVariadicValues{ + Items: []any{}, + ItemCreator: func() any { return &U8Value{} }, + } + + err := serializer.Deserialize("", []any{destination}) + require.NoError(t, err) + require.Equal(t, []any{&U8Value{Value: 0}}, destination.Items) + }) + + t.Run("variadic", func(t *testing.T) { + destination := &OutputVariadicValues{ + Items: []any{}, + ItemCreator: func() any { return &U8Value{} }, + } + + err := serializer.Deserialize("2A@2B@2C", []any{destination}) + require.NoError(t, err) + + require.Equal(t, []any{ + &U8Value{Value: 42}, + &U8Value{Value: 43}, + &U8Value{Value: 44}, + }, destination.Items) + }) + + t.Run("varidic, with empty items", func(t *testing.T) { + destination := &OutputVariadicValues{ + Items: []any{}, + ItemCreator: func() any { return &U8Value{} }, + } + + err := serializer.Deserialize("@01@", []any{destination}) + require.NoError(t, err) + + require.Equal(t, []any{ + &U8Value{Value: 0}, + &U8Value{Value: 1}, + &U8Value{Value: 0}, + }, destination.Items) + }) + + t.Run("varidic", func(t *testing.T) { + destination := &OutputVariadicValues{ + Items: []any{}, + ItemCreator: func() any { return &U32Value{} }, + } + + err := serializer.Deserialize("AABBCCDD@DDCCBBAA", []any{destination}) + require.NoError(t, err) + + require.Equal(t, []any{ + &U32Value{Value: 0xAABBCCDD}, + &U32Value{Value: 0xDDCCBBAA}, + }, destination.Items) + }) + + t.Run("varidic, should err because decoded value is too large", func(t *testing.T) { + destination := &OutputVariadicValues{ + Items: []any{}, + ItemCreator: func() any { return &U8Value{} }, + } + + err := serializer.Deserialize("0100", []any{destination}) + require.ErrorContains(t, err, "cannot decode (top-level) *abi.U8Value, because of: decoded value is too large: 256 > 255") + }) +} diff --git a/abi/shared.go b/abi/shared.go new file mode 100644 index 0000000..ca61e0f --- /dev/null +++ b/abi/shared.go @@ -0,0 +1,45 @@ +package abi + +import ( + "encoding/binary" + "fmt" + "io" +) + +func encodeLength(writer io.Writer, length uint32) error { + bytes := make([]byte, 4) + binary.BigEndian.PutUint32(bytes, length) + + _, err := writer.Write(bytes) + if err != nil { + return err + } + + return nil +} + +func decodeLength(reader io.Reader) (uint32, error) { + bytes, err := readBytesExactly(reader, 4) + if err != nil { + return 0, err + } + + return binary.BigEndian.Uint32(bytes), nil +} + +func readBytesExactly(reader io.Reader, numBytes int) ([]byte, error) { + if numBytes == 0 { + return []byte{}, nil + } + + data := make([]byte, numBytes) + n, err := reader.Read(data) + if err != nil { + return nil, err + } + if n != numBytes { + return nil, fmt.Errorf("cannot read exactly %d bytes", numBytes) + } + + return data, err +} diff --git a/abi/values.go b/abi/values.go new file mode 100644 index 0000000..58f4095 --- /dev/null +++ b/abi/values.go @@ -0,0 +1,122 @@ +package abi + +import "math/big" + +// U8Value is a wrapper for uint8 +type U8Value struct { + Value uint8 +} + +// U16Value is a wrapper for uint16 +type U16Value struct { + Value uint16 +} + +// U32Value is a wrapper for uint32 +type U32Value struct { + Value uint32 +} + +// U64Value is a wrapper for uint64 +type U64Value struct { + Value uint64 +} + +// I8Value is a wrapper for int8 +type I8Value struct { + Value int8 +} + +// I16Value is a wrapper for int16 +type I16Value struct { + Value int16 +} + +// I32Value is a wrapper for int32 +type I32Value struct { + Value int32 +} + +// I64Value is a wrapper for int64 +type I64Value struct { + Value int64 +} + +// BigIntValue is a wrapper for a big integer +type BigIntValue struct { + Value *big.Int +} + +// AddressValue is a wrapper for an address +type AddressValue struct { + Value []byte +} + +// BytesValue is a wrapper for a byte slice +type BytesValue struct { + Value []byte +} + +// StringValue is a wrapper for a string +type StringValue struct { + Value string +} + +// BoolValue is a wrapper for a boolean +type BoolValue struct { + Value bool +} + +// OptionValue is a wrapper for an optional value +type OptionValue struct { + Value any +} + +// Field is a field in a struct, enum etc. +type Field struct { + Name string + Value any +} + +// StructValue is a struct (collection of fields) +type StructValue struct { + Fields []Field +} + +// EnumValue is an enum (discriminant and fields) +type EnumValue struct { + Discriminant uint8 + Fields []Field +} + +// InputListValue is a list of values (used for encoding) +type InputListValue struct { + Items []any +} + +// OutputListValue is a list of values (used for decoding) +type OutputListValue struct { + Items []any + ItemCreator func() any +} + +// InputMultiValue is a multi-value (used for encoding) +type InputMultiValue struct { + Items []any +} + +// OutputMultiValue is a multi-value (used for decoding) +type OutputMultiValue struct { + Items []any +} + +// InputVariadicValues holds variadic values (used for encoding) +type InputVariadicValues struct { + Items []any +} + +// OutputVariadicValues holds variadic values (used for decoding) +type OutputVariadicValues struct { + Items []any + ItemCreator func() any +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5c3912f --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/multiversx/mx-sdk-abi-go + +go 1.20 + +require ( + github.com/multiversx/mx-components-big-int v1.0.0 + github.com/stretchr/testify v1.7.1 +) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..682b503 --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/multiversx/mx-components-big-int v1.0.0 h1:Wkr8lSzK2nDqixOrrBa47VNuqdhV1m/aJhaP1EMaiS8= +github.com/multiversx/mx-components-big-int v1.0.0/go.mod h1:maIEMgHlNE2u78JaDD0oLzri+ShgU4okHfzP3LWGdQM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 7827c592320266df5dfe1c2f8454e056f1f6acb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 10 Apr 2024 18:16:29 +0300 Subject: [PATCH 02/24] Add workflows. --- .github/workflows/run-linter.yml | 27 +++++++++++++++++++++++++++ .github/workflows/run-tests.yml | 25 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 .github/workflows/run-linter.yml create mode 100644 .github/workflows/run-tests.yml diff --git a/.github/workflows/run-linter.yml b/.github/workflows/run-linter.yml new file mode 100644 index 0000000..09a9e8b --- /dev/null +++ b/.github/workflows/run-linter.yml @@ -0,0 +1,27 @@ +name: Run linter + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + workflow_dispatch: + +permissions: + contents: read + +jobs: + golangci: + name: golangci linter + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.20.7 + - uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.53.2 + args: --timeout 10m0s --max-issues-per-linter 0 --max-same-issues 0 --print-issued-lines + only-new-issues: true diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..e5b85d5 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,25 @@ +name: Run tests + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.20.7 + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... From 177323d91926429dde5fe49972810999428fdff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 10 Apr 2024 18:21:53 +0300 Subject: [PATCH 03/24] Add workflow for code coverage. --- .github/workflows/check-code-coverage.yml | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/check-code-coverage.yml diff --git a/.github/workflows/check-code-coverage.yml b/.github/workflows/check-code-coverage.yml new file mode 100644 index 0000000..f32a5d4 --- /dev/null +++ b/.github/workflows/check-code-coverage.yml @@ -0,0 +1,28 @@ +name: Check code coverage + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + workflow_dispatch: + +jobs: + build: + strategy: + matrix: + runs-on: [ubuntu-latest] + runs-on: ${{ matrix.runs-on }} + name: Build + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.20.7 + - uses: actions/checkout@v3 + + - name: Run tests + run: | + go test -cover -coverprofile=coverage.txt -covermode=atomic -v ./... + + - name: Upload coverage + run: bash <(curl -s https://codecov.io/bash) -f coverage.txt -y codecov.yml From 59108253e437f5a220f2d9a0a706ebb5dd39e2e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 10 Apr 2024 18:29:19 +0300 Subject: [PATCH 04/24] Workflow - update actions, make them consistent etc. --- .github/workflows/check-code-coverage.yml | 5 +++-- .github/workflows/run-linter.yml | 6 ++++-- .github/workflows/run-tests.yml | 7 +++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/check-code-coverage.yml b/.github/workflows/check-code-coverage.yml index f32a5d4..e41e3dd 100644 --- a/.github/workflows/check-code-coverage.yml +++ b/.github/workflows/check-code-coverage.yml @@ -15,10 +15,11 @@ jobs: runs-on: ${{ matrix.runs-on }} name: Build steps: - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v5 with: go-version: 1.20.7 - - uses: actions/checkout@v3 + + - uses: actions/checkout@v4 - name: Run tests run: | diff --git a/.github/workflows/run-linter.yml b/.github/workflows/run-linter.yml index 09a9e8b..faa2734 100644 --- a/.github/workflows/run-linter.yml +++ b/.github/workflows/run-linter.yml @@ -15,10 +15,12 @@ jobs: name: golangci linter runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v5 with: go-version: 1.20.7 - - uses: actions/checkout@v3 + + - uses: actions/checkout@v4 + - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index e5b85d5..e034c73 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -11,13 +11,12 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - - name: Set up Go - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: 1.20.7 + - uses: actions/checkout@v4 + - name: Build run: go build -v ./... From d0b966cc4ac71d2f5097ede43df6e2aa36a79723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 10 Apr 2024 18:52:59 +0300 Subject: [PATCH 05/24] Additional tests. --- abi/codec_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/abi/codec_test.go b/abi/codec_test.go index 3f4bb84..217f9c0 100644 --- a/abi/codec_test.go +++ b/abi/codec_test.go @@ -817,6 +817,23 @@ func TestCodec_DecodeTopLevel(t *testing.T) { require.Equal(t, &BigIntValue{Value: big.NewInt(-1)}, destination) }) + t.Run("address", func(t *testing.T) { + data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") + + destination := &AddressValue{} + err := codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, &AddressValue{Value: data}, destination) + }) + + t.Run("address (bad)", func(t *testing.T) { + data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d42") + + destination := &AddressValue{} + err := codec.DecodeTopLevel(data, destination) + require.ErrorContains(t, err, "public key (address) has invalid length") + }) + t.Run("struct", func(t *testing.T) { data, _ := hex.DecodeString("014142") From 47abbe832838f5b85cbf639e3744acd0d6c20401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Mon, 15 Apr 2024 19:08:49 +0300 Subject: [PATCH 06/24] Add missing implementations. --- abi/codec.go | 8 ++++++++ abi/codecForSimpleValues_bytes.go | 10 ++++++++++ abi/codecForSimpleValues_string.go | 10 ++++++++++ 3 files changed, 28 insertions(+) diff --git a/abi/codec.go b/abi/codec.go index 2dc8d55..60d04ac 100644 --- a/abi/codec.go +++ b/abi/codec.go @@ -116,6 +116,10 @@ func (c *codec) doEncodeTopLevel(writer io.Writer, value any) error { return c.encodeTopLevelBigNumber(writer, value.Value) case AddressValue: return c.encodeTopLevelAddress(writer, value) + case StringValue: + return c.encodeTopLevelString(writer, value) + case BytesValue: + return c.encodeTopLevelBytes(writer, value) case StructValue: return c.encodeTopLevelStruct(writer, value) case EnumValue: @@ -259,6 +263,10 @@ func (c *codec) doDecodeTopLevel(data []byte, value any) error { value.Value = n case *AddressValue: return c.decodeTopLevelAddress(data, value) + case *StringValue: + return c.decodeTopLevelString(data, value) + case *BytesValue: + return c.decodeTopLevelBytes(data, value) case *StructValue: return c.decodeTopLevelStruct(data, value) case *EnumValue: diff --git a/abi/codecForSimpleValues_bytes.go b/abi/codecForSimpleValues_bytes.go index 6c4eb5c..ab9decb 100644 --- a/abi/codecForSimpleValues_bytes.go +++ b/abi/codecForSimpleValues_bytes.go @@ -14,6 +14,11 @@ func (c *codec) encodeNestedBytes(writer io.Writer, value BytesValue) error { return err } +func (c *codec) encodeTopLevelBytes(writer io.Writer, value BytesValue) error { + _, err := writer.Write(value.Value) + return err +} + func (c *codec) decodeNestedBytes(reader io.Reader, value *BytesValue) error { length, err := decodeLength(reader) if err != nil { @@ -28,3 +33,8 @@ func (c *codec) decodeNestedBytes(reader io.Reader, value *BytesValue) error { value.Value = data return nil } + +func (c *codec) decodeTopLevelBytes(data []byte, value *BytesValue) error { + value.Value = data + return nil +} diff --git a/abi/codecForSimpleValues_string.go b/abi/codecForSimpleValues_string.go index a8de1cc..59a5320 100644 --- a/abi/codecForSimpleValues_string.go +++ b/abi/codecForSimpleValues_string.go @@ -15,6 +15,11 @@ func (c *codec) encodeNestedString(writer io.Writer, value StringValue) error { return err } +func (c *codec) encodeTopLevelString(writer io.Writer, value StringValue) error { + _, err := writer.Write([]byte(value.Value)) + return err +} + func (c *codec) decodeNestedString(reader io.Reader, value *StringValue) error { length, err := decodeLength(reader) if err != nil { @@ -29,3 +34,8 @@ func (c *codec) decodeNestedString(reader io.Reader, value *StringValue) error { value.Value = string(data) return nil } + +func (c *codec) decodeTopLevelString(data []byte, value *StringValue) error { + value.Value = string(data) + return nil +} From 39dd41bbb0362e0e14b90630bc40e0dc45d3b623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Tue, 16 Apr 2024 13:52:41 +0300 Subject: [PATCH 07/24] Add extra tests, move tests. --- abi/codecForSimpleValues_boolean_test.go | 31 +++ abi/codecForSimpleValues_bytes_test.go | 31 +++ abi/codecForSimpleValues_numerical_test.go | 99 ++++++++++ abi/codecForSimpleValues_string_test.go | 31 +++ abi/codec_test.go | 215 +++------------------ 5 files changed, 222 insertions(+), 185 deletions(-) create mode 100644 abi/codecForSimpleValues_boolean_test.go create mode 100644 abi/codecForSimpleValues_bytes_test.go create mode 100644 abi/codecForSimpleValues_numerical_test.go create mode 100644 abi/codecForSimpleValues_string_test.go diff --git a/abi/codecForSimpleValues_boolean_test.go b/abi/codecForSimpleValues_boolean_test.go new file mode 100644 index 0000000..6bc6d74 --- /dev/null +++ b/abi/codecForSimpleValues_boolean_test.go @@ -0,0 +1,31 @@ +package abi + +import ( + "testing" +) + +func TestCodec_Boolean(t *testing.T) { + codec, _ := newCodec(argsNewCodec{ + pubKeyLength: 32, + }) + + t.Run("should encode nested", func(t *testing.T) { + testEncodeNested(t, codec, BoolValue{Value: false}, "00") + testEncodeNested(t, codec, BoolValue{Value: true}, "01") + }) + + t.Run("should encode top-level", func(t *testing.T) { + testEncodeTopLevel(t, codec, BoolValue{Value: false}, "") + testEncodeTopLevel(t, codec, BoolValue{Value: true}, "01") + }) + + t.Run("should decode nested", func(t *testing.T) { + testDecodeNested(t, codec, "00", &BoolValue{}, &BoolValue{Value: false}) + testDecodeNested(t, codec, "01", &BoolValue{}, &BoolValue{Value: true}) + }) + + t.Run("should decode top-level", func(t *testing.T) { + testDecodeTopLevel(t, codec, "", &BoolValue{}, &BoolValue{Value: false}) + testDecodeTopLevel(t, codec, "01", &BoolValue{}, &BoolValue{Value: true}) + }) +} diff --git a/abi/codecForSimpleValues_bytes_test.go b/abi/codecForSimpleValues_bytes_test.go new file mode 100644 index 0000000..14cd8b9 --- /dev/null +++ b/abi/codecForSimpleValues_bytes_test.go @@ -0,0 +1,31 @@ +package abi + +import ( + "testing" +) + +func TestCodec_Bytes(t *testing.T) { + codec, _ := newCodec(argsNewCodec{ + pubKeyLength: 32, + }) + + t.Run("should encode nested", func(t *testing.T) { + testEncodeNested(t, codec, BytesValue{Value: []byte{}}, "00000000") + testEncodeNested(t, codec, BytesValue{Value: []byte{'a', 'b', 'c'}}, "00000003616263") + }) + + t.Run("should encode top-level", func(t *testing.T) { + testEncodeTopLevel(t, codec, BytesValue{Value: []byte{}}, "") + testEncodeTopLevel(t, codec, BytesValue{Value: []byte{'a', 'b', 'c'}}, "616263") + }) + + t.Run("should decode nested", func(t *testing.T) { + testDecodeNested(t, codec, "00000000", &BytesValue{}, &BytesValue{Value: []byte{}}) + testDecodeNested(t, codec, "00000003616263", &BytesValue{}, &BytesValue{Value: []byte{'a', 'b', 'c'}}) + }) + + t.Run("should decode top-level", func(t *testing.T) { + testDecodeTopLevel(t, codec, "", &BytesValue{}, &BytesValue{Value: []byte{}}) + testDecodeTopLevel(t, codec, "616263", &BytesValue{}, &BytesValue{Value: []byte{'a', 'b', 'c'}}) + }) +} diff --git a/abi/codecForSimpleValues_numerical_test.go b/abi/codecForSimpleValues_numerical_test.go new file mode 100644 index 0000000..0fc60fd --- /dev/null +++ b/abi/codecForSimpleValues_numerical_test.go @@ -0,0 +1,99 @@ +package abi + +import ( + "math/big" + "testing" +) + +func TestCodec_Numerical(t *testing.T) { + codec, _ := newCodec(argsNewCodec{ + pubKeyLength: 32, + }) + + t.Run("should encode nested", func(t *testing.T) { + testEncodeNested(t, codec, U8Value{Value: 0x00}, "00") + testEncodeNested(t, codec, U8Value{Value: 0x01}, "01") + testEncodeNested(t, codec, U8Value{Value: 0x42}, "42") + testEncodeNested(t, codec, U8Value{Value: 0xff}, "ff") + + testEncodeNested(t, codec, I8Value{Value: 0x00}, "00") + testEncodeNested(t, codec, I8Value{Value: 0x01}, "01") + testEncodeNested(t, codec, I8Value{Value: -1}, "ff") + testEncodeNested(t, codec, I8Value{Value: -128}, "80") + testEncodeNested(t, codec, I8Value{Value: 127}, "7f") + + testEncodeNested(t, codec, U16Value{Value: 0x0000}, "0000") + testEncodeNested(t, codec, U16Value{Value: 0x0011}, "0011") + testEncodeNested(t, codec, U16Value{Value: 0x1234}, "1234") + testEncodeNested(t, codec, U16Value{Value: 0xffff}, "ffff") + + testEncodeNested(t, codec, I16Value{Value: 0x0000}, "0000") + testEncodeNested(t, codec, I16Value{Value: 0x0011}, "0011") + testEncodeNested(t, codec, I16Value{Value: -1}, "ffff") + testEncodeNested(t, codec, I16Value{Value: -32768}, "8000") + + testEncodeNested(t, codec, U32Value{Value: 0x00000000}, "00000000") + testEncodeNested(t, codec, U32Value{Value: 0x00000011}, "00000011") + testEncodeNested(t, codec, U32Value{Value: 0x00001122}, "00001122") + testEncodeNested(t, codec, U32Value{Value: 0x00112233}, "00112233") + testEncodeNested(t, codec, U32Value{Value: 0x11223344}, "11223344") + testEncodeNested(t, codec, U32Value{Value: 0xffffffff}, "ffffffff") + + testEncodeNested(t, codec, I32Value{Value: 0x00000000}, "00000000") + testEncodeNested(t, codec, I32Value{Value: 0x00000011}, "00000011") + testEncodeNested(t, codec, I32Value{Value: -1}, "ffffffff") + testEncodeNested(t, codec, I32Value{Value: -2147483648}, "80000000") + + testEncodeNested(t, codec, U64Value{Value: 0x0000000000000000}, "0000000000000000") + testEncodeNested(t, codec, U64Value{Value: 0x0000000000000011}, "0000000000000011") + testEncodeNested(t, codec, U64Value{Value: 0x0011223344556677}, "0011223344556677") + testEncodeNested(t, codec, U64Value{Value: 0xffffffffffffffff}, "ffffffffffffffff") + + testEncodeNested(t, codec, I64Value{Value: 0x0000000000000000}, "0000000000000000") + testEncodeNested(t, codec, I64Value{Value: 0x0000000000000011}, "0000000000000011") + testEncodeNested(t, codec, I64Value{Value: -1}, "ffffffffffffffff") + testEncodeNested(t, codec, I64Value{Value: -9223372036854775808}, "8000000000000000") + + testEncodeNested(t, codec, BigIntValue{Value: big.NewInt(0)}, "00000000") + testEncodeNested(t, codec, BigIntValue{Value: big.NewInt(1)}, "0000000101") + testEncodeNested(t, codec, BigIntValue{Value: big.NewInt(-1)}, "00000001ff") + }) + + t.Run("should encode top-level", func(t *testing.T) { + testEncodeTopLevel(t, codec, U8Value{Value: 0x00}, "") + testEncodeTopLevel(t, codec, U8Value{Value: 0x01}, "01") + + testEncodeTopLevel(t, codec, I8Value{Value: 0x00}, "") + testEncodeTopLevel(t, codec, I8Value{Value: 0x01}, "01") + testEncodeTopLevel(t, codec, I8Value{Value: -1}, "ff") + + testEncodeTopLevel(t, codec, U16Value{Value: 0x0000}, "") + testEncodeTopLevel(t, codec, U16Value{Value: 0x0011}, "11") + + testEncodeTopLevel(t, codec, I16Value{Value: 0x0000}, "") + testEncodeTopLevel(t, codec, I16Value{Value: 0x0011}, "11") + testEncodeTopLevel(t, codec, I16Value{Value: -1}, "ff") + + testEncodeTopLevel(t, codec, U32Value{Value: 0x00004242}, "4242") + + testEncodeTopLevel(t, codec, I32Value{Value: 0x00000000}, "") + testEncodeTopLevel(t, codec, I32Value{Value: 0x00000011}, "11") + testEncodeTopLevel(t, codec, I32Value{Value: -1}, "ff") + + testEncodeTopLevel(t, codec, U64Value{Value: 0x0042434445464748}, "42434445464748") + + testEncodeTopLevel(t, codec, I64Value{Value: 0x0000000000000000}, "") + testEncodeTopLevel(t, codec, I64Value{Value: 0x0000000000000011}, "11") + testEncodeTopLevel(t, codec, I64Value{Value: -1}, "ff") + + testEncodeTopLevel(t, codec, BigIntValue{Value: big.NewInt(0)}, "") + testEncodeTopLevel(t, codec, BigIntValue{Value: big.NewInt(1)}, "01") + testEncodeTopLevel(t, codec, BigIntValue{Value: big.NewInt(-1)}, "ff") + }) + + t.Run("should decode nested", func(t *testing.T) { + }) + + t.Run("should decode top-level", func(t *testing.T) { + }) +} diff --git a/abi/codecForSimpleValues_string_test.go b/abi/codecForSimpleValues_string_test.go new file mode 100644 index 0000000..c2e2e44 --- /dev/null +++ b/abi/codecForSimpleValues_string_test.go @@ -0,0 +1,31 @@ +package abi + +import ( + "testing" +) + +func TestCodec_String(t *testing.T) { + codec, _ := newCodec(argsNewCodec{ + pubKeyLength: 32, + }) + + t.Run("should encode nested", func(t *testing.T) { + testEncodeNested(t, codec, StringValue{Value: ""}, "00000000") + testEncodeNested(t, codec, StringValue{Value: "abc"}, "00000003616263") + }) + + t.Run("should encode top-level", func(t *testing.T) { + testEncodeTopLevel(t, codec, StringValue{Value: ""}, "") + testEncodeTopLevel(t, codec, StringValue{Value: "abc"}, "616263") + }) + + t.Run("should decode nested", func(t *testing.T) { + testDecodeNested(t, codec, "00000000", &StringValue{}, &StringValue{}) + testDecodeNested(t, codec, "00000003616263", &StringValue{}, &StringValue{Value: "abc"}) + }) + + t.Run("should decode top-level", func(t *testing.T) { + testDecodeTopLevel(t, codec, "", &StringValue{}, &StringValue{}) + testDecodeTopLevel(t, codec, "616263", &StringValue{}, &StringValue{Value: "abc"}) + }) +} diff --git a/abi/codec_test.go b/abi/codec_test.go index 217f9c0..696a73c 100644 --- a/abi/codec_test.go +++ b/abi/codec_test.go @@ -38,73 +38,6 @@ func TestCodec_EncodeNested(t *testing.T) { require.Equal(t, expected, hex.EncodeToString(encoded)) } - t.Run("bool", func(t *testing.T) { - doTest(t, BoolValue{Value: false}, "00") - doTest(t, BoolValue{Value: true}, "01") - }) - - t.Run("u8, i8", func(t *testing.T) { - doTest(t, U8Value{Value: 0x00}, "00") - doTest(t, U8Value{Value: 0x01}, "01") - doTest(t, U8Value{Value: 0x42}, "42") - doTest(t, U8Value{Value: 0xff}, "ff") - - doTest(t, I8Value{Value: 0x00}, "00") - doTest(t, I8Value{Value: 0x01}, "01") - doTest(t, I8Value{Value: -1}, "ff") - doTest(t, I8Value{Value: -128}, "80") - doTest(t, I8Value{Value: 127}, "7f") - }) - - t.Run("u16, i16", func(t *testing.T) { - doTest(t, U16Value{Value: 0x00}, "0000") - doTest(t, U16Value{Value: 0x11}, "0011") - doTest(t, U16Value{Value: 0x1234}, "1234") - doTest(t, U16Value{Value: 0xffff}, "ffff") - - doTest(t, I16Value{Value: 0x0000}, "0000") - doTest(t, I16Value{Value: 0x0011}, "0011") - doTest(t, I16Value{Value: -1}, "ffff") - doTest(t, I16Value{Value: -32768}, "8000") - }) - - t.Run("u32, i32", func(t *testing.T) { - doTest(t, U32Value{Value: 0x00000000}, "00000000") - doTest(t, U32Value{Value: 0x00000011}, "00000011") - doTest(t, U32Value{Value: 0x00001122}, "00001122") - doTest(t, U32Value{Value: 0x00112233}, "00112233") - doTest(t, U32Value{Value: 0x11223344}, "11223344") - doTest(t, U32Value{Value: 0xffffffff}, "ffffffff") - - doTest(t, I32Value{Value: 0x00000000}, "00000000") - doTest(t, I32Value{Value: 0x00000011}, "00000011") - doTest(t, I32Value{Value: -1}, "ffffffff") - doTest(t, I32Value{Value: -2147483648}, "80000000") - }) - - t.Run("u64, i64", func(t *testing.T) { - doTest(t, U64Value{Value: 0x0000000000000000}, "0000000000000000") - doTest(t, U64Value{Value: 0x0000000000000011}, "0000000000000011") - doTest(t, U64Value{Value: 0x0000000000001122}, "0000000000001122") - doTest(t, U64Value{Value: 0x0000000000112233}, "0000000000112233") - doTest(t, U64Value{Value: 0x0000000011223344}, "0000000011223344") - doTest(t, U64Value{Value: 0x0000001122334455}, "0000001122334455") - doTest(t, U64Value{Value: 0x0000112233445566}, "0000112233445566") - doTest(t, U64Value{Value: 0x0011223344556677}, "0011223344556677") - doTest(t, U64Value{Value: 0x1122334455667788}, "1122334455667788") - doTest(t, U64Value{Value: 0xffffffffffffffff}, "ffffffffffffffff") - - doTest(t, I64Value{Value: 0x0000000000000000}, "0000000000000000") - doTest(t, I64Value{Value: 0x0000000000000011}, "0000000000000011") - doTest(t, I64Value{Value: -1}, "ffffffffffffffff") - }) - - t.Run("bigInt", func(t *testing.T) { - doTest(t, BigIntValue{Value: big.NewInt(0)}, "00000000") - doTest(t, BigIntValue{Value: big.NewInt(1)}, "0000000101") - doTest(t, BigIntValue{Value: big.NewInt(-1)}, "00000001ff") - }) - t.Run("address", func(t *testing.T) { data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") doTest(t, AddressValue{Value: data}, "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") @@ -116,16 +49,6 @@ func TestCodec_EncodeNested(t *testing.T) { require.ErrorContains(t, err, "public key (address) has invalid length") }) - t.Run("string", func(t *testing.T) { - doTest(t, StringValue{Value: ""}, "00000000") - doTest(t, StringValue{Value: "abc"}, "00000003616263") - }) - - t.Run("bytes", func(t *testing.T) { - doTest(t, BytesValue{Value: []byte{}}, "00000000") - doTest(t, BytesValue{Value: []byte{'a', 'b', 'c'}}, "00000003616263") - }) - t.Run("struct", func(t *testing.T) { fooStruct := StructValue{ Fields: []Field{ @@ -223,50 +146,6 @@ func TestCodec_EncodeTopLevel(t *testing.T) { require.Equal(t, expected, hex.EncodeToString(encoded)) } - t.Run("bool", func(t *testing.T) { - doTest(t, BoolValue{Value: false}, "") - doTest(t, BoolValue{Value: true}, "01") - }) - - t.Run("u8, i8", func(t *testing.T) { - doTest(t, U8Value{Value: 0x00}, "") - doTest(t, U8Value{Value: 0x01}, "01") - - doTest(t, I8Value{Value: 0x00}, "") - doTest(t, I8Value{Value: 0x01}, "01") - doTest(t, I8Value{Value: -1}, "ff") - }) - - t.Run("u16, i16", func(t *testing.T) { - doTest(t, U16Value{Value: 0x0042}, "42") - - doTest(t, I16Value{Value: 0x0000}, "") - doTest(t, I16Value{Value: 0x0011}, "11") - doTest(t, I16Value{Value: -1}, "ff") - }) - - t.Run("u32, i32", func(t *testing.T) { - doTest(t, U32Value{Value: 0x00004242}, "4242") - - doTest(t, I32Value{Value: 0x00000000}, "") - doTest(t, I32Value{Value: 0x00000011}, "11") - doTest(t, I32Value{Value: -1}, "ff") - }) - - t.Run("u64, i64", func(t *testing.T) { - doTest(t, U64Value{Value: 0x0042434445464748}, "42434445464748") - - doTest(t, I64Value{Value: 0x0000000000000000}, "") - doTest(t, I64Value{Value: 0x0000000000000011}, "11") - doTest(t, I64Value{Value: -1}, "ff") - }) - - t.Run("bigInt", func(t *testing.T) { - doTest(t, BigIntValue{Value: big.NewInt(0)}, "") - doTest(t, BigIntValue{Value: big.NewInt(1)}, "01") - doTest(t, BigIntValue{Value: big.NewInt(-1)}, "ff") - }) - t.Run("address", func(t *testing.T) { data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") doTest(t, AddressValue{Value: data}, "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") @@ -341,24 +220,6 @@ func TestCodec_DecodeNested(t *testing.T) { pubKeyLength: 32, }) - t.Run("bool (true)", func(t *testing.T) { - data, _ := hex.DecodeString("01") - destination := &BoolValue{} - - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &BoolValue{Value: true}, destination) - }) - - t.Run("bool (false)", func(t *testing.T) { - data, _ := hex.DecodeString("00") - destination := &BoolValue{} - - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &BoolValue{Value: false}, destination) - }) - t.Run("u8", func(t *testing.T) { data, _ := hex.DecodeString("01") destination := &U8Value{} @@ -499,34 +360,6 @@ func TestCodec_DecodeNested(t *testing.T) { require.ErrorContains(t, err, "cannot read exactly 32 bytes") }) - t.Run("string", func(t *testing.T) { - data, _ := hex.DecodeString("00000000") - destination := &StringValue{} - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &StringValue{}, destination) - - data, _ = hex.DecodeString("00000003616263") - destination = &StringValue{} - err = codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &StringValue{Value: "abc"}, destination) - }) - - t.Run("bytes", func(t *testing.T) { - data, _ := hex.DecodeString("00000000") - destination := &BytesValue{} - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &BytesValue{Value: []byte{}}, destination) - - data, _ = hex.DecodeString("00000003616263") - destination = &BytesValue{} - err = codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &BytesValue{Value: []byte{'a', 'b', 'c'}}, destination) - }) - t.Run("struct", func(t *testing.T) { data, _ := hex.DecodeString("014142") @@ -667,24 +500,6 @@ func TestCodec_DecodeTopLevel(t *testing.T) { pubKeyLength: 32, }) - t.Run("bool (true)", func(t *testing.T) { - data, _ := hex.DecodeString("01") - destination := &BoolValue{} - - err := codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &BoolValue{Value: true}, destination) - }) - - t.Run("bool (false)", func(t *testing.T) { - data, _ := hex.DecodeString("") - destination := &BoolValue{} - - err := codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &BoolValue{Value: false}, destination) - }) - t.Run("u8", func(t *testing.T) { data, _ := hex.DecodeString("01") destination := &U8Value{} @@ -922,3 +737,33 @@ func TestCodec_DecodeTopLevel(t *testing.T) { require.ErrorContains(t, err, "unsupported type for top-level decoding: *abi.dummy") }) } + +func testEncodeNested(t *testing.T, codec *codec, value any, expected string) { + encoded, err := codec.EncodeNested(value) + + require.NoError(t, err) + require.Equal(t, expected, hex.EncodeToString(encoded)) +} + +func testEncodeTopLevel(t *testing.T, codec *codec, value any, expected string) { + encoded, err := codec.EncodeTopLevel(value) + + require.NoError(t, err) + require.Equal(t, expected, hex.EncodeToString(encoded)) +} + +func testDecodeNested(t *testing.T, codec *codec, encodedData string, destination any, expected any) { + data, _ := hex.DecodeString(encodedData) + err := codec.DecodeNested(data, destination) + + require.NoError(t, err) + require.Equal(t, expected, destination) +} + +func testDecodeTopLevel(t *testing.T, codec *codec, encodedData string, destination any, expected any) { + data, _ := hex.DecodeString(encodedData) + err := codec.DecodeTopLevel(data, destination) + + require.NoError(t, err) + require.Equal(t, expected, destination) +} From 97dd79708ab30f005308a8fddc96f7757d13d350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Tue, 16 Apr 2024 14:49:10 +0300 Subject: [PATCH 08/24] Move / add some tests. --- abi/codecForSimpleValues_address_test.go | 56 ++++++ abi/codecForSimpleValues_numerical_test.go | 50 +++++ abi/codec_test.go | 220 --------------------- 3 files changed, 106 insertions(+), 220 deletions(-) create mode 100644 abi/codecForSimpleValues_address_test.go diff --git a/abi/codecForSimpleValues_address_test.go b/abi/codecForSimpleValues_address_test.go new file mode 100644 index 0000000..62fc612 --- /dev/null +++ b/abi/codecForSimpleValues_address_test.go @@ -0,0 +1,56 @@ +package abi + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCodec_Address(t *testing.T) { + codec, _ := newCodec(argsNewCodec{ + pubKeyLength: 32, + }) + + alicePubKeyHex := "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1" + alicePubKey, _ := hex.DecodeString(alicePubKeyHex) + + shortPubKeyHex := "0139472eff6886771a982f3083da5d42" + shortPubKey, _ := hex.DecodeString(shortPubKeyHex) + + t.Run("should encode nested", func(t *testing.T) { + testEncodeNested(t, codec, AddressValue{Value: alicePubKey}, alicePubKeyHex) + }) + + t.Run("should err on encode nested (bad public key length)", func(t *testing.T) { + _, err := codec.EncodeNested(AddressValue{Value: shortPubKey}) + require.ErrorContains(t, err, "public key (address) has invalid length") + }) + + t.Run("should encode top-level", func(t *testing.T) { + testEncodeTopLevel(t, codec, AddressValue{Value: alicePubKey}, alicePubKeyHex) + }) + + t.Run("should err on encode top-level (bad public key length)", func(t *testing.T) { + _, err := codec.EncodeTopLevel(AddressValue{Value: shortPubKey}) + require.ErrorContains(t, err, "public key (address) has invalid length") + }) + + t.Run("should decode nested", func(t *testing.T) { + testDecodeNested(t, codec, alicePubKeyHex, &AddressValue{}, &AddressValue{Value: alicePubKey}) + }) + + t.Run("should err on decode nested (shorter public key)", func(t *testing.T) { + err := codec.DecodeNested(shortPubKey, &AddressValue{}) + require.ErrorContains(t, err, "cannot read exactly 32 bytes") + }) + + t.Run("should decode top-level", func(t *testing.T) { + testDecodeTopLevel(t, codec, alicePubKeyHex, &AddressValue{}, &AddressValue{Value: alicePubKey}) + }) + + t.Run("should err on decode top-level (shorter public key)", func(t *testing.T) { + err := codec.DecodeTopLevel(shortPubKey, &AddressValue{}) + require.ErrorContains(t, err, "public key (address) has invalid length") + }) +} diff --git a/abi/codecForSimpleValues_numerical_test.go b/abi/codecForSimpleValues_numerical_test.go index 0fc60fd..b227c15 100644 --- a/abi/codecForSimpleValues_numerical_test.go +++ b/abi/codecForSimpleValues_numerical_test.go @@ -92,8 +92,58 @@ func TestCodec_Numerical(t *testing.T) { }) t.Run("should decode nested", func(t *testing.T) { + testDecodeNested(t, codec, "00", &U8Value{}, &U8Value{Value: 0}) + testDecodeNested(t, codec, "01", &U8Value{}, &U8Value{Value: 1}) + testDecodeNested(t, codec, "ff", &U8Value{}, &U8Value{Value: 255}) + + testDecodeNested(t, codec, "ff", &I8Value{}, &I8Value{Value: -1}) + + testDecodeNested(t, codec, "4142", &U16Value{}, &U16Value{Value: 0x4142}) + testDecodeNested(t, codec, "ffff", &U16Value{}, &U16Value{Value: 65535}) + + testDecodeNested(t, codec, "ffff", &I16Value{}, &I16Value{Value: -1}) + testDecodeNested(t, codec, "8000", &I16Value{}, &I16Value{Value: -32768}) + + testDecodeNested(t, codec, "41424344", &U32Value{}, &U32Value{Value: 0x41424344}) + testDecodeNested(t, codec, "ffffffff", &U32Value{}, &U32Value{Value: 4294967295}) + + testDecodeNested(t, codec, "ffffffff", &I32Value{}, &I32Value{Value: -1}) + testDecodeNested(t, codec, "80000000", &I32Value{}, &I32Value{Value: -2147483648}) + + testDecodeNested(t, codec, "4142434445464748", &U64Value{}, &U64Value{Value: 0x4142434445464748}) + testDecodeNested(t, codec, "ffffffffffffffff", &U64Value{}, &U64Value{Value: 18446744073709551615}) + + testDecodeNested(t, codec, "ffffffffffffffff", &I64Value{}, &I64Value{Value: -1}) + testDecodeNested(t, codec, "8000000000000000", &I64Value{}, &I64Value{Value: -9223372036854775808}) + + testDecodeNested(t, codec, "00000000", &BigIntValue{}, &BigIntValue{Value: big.NewInt(0)}) + testDecodeNested(t, codec, "0000000101", &BigIntValue{}, &BigIntValue{Value: big.NewInt(1)}) + testDecodeNested(t, codec, "00000001ff", &BigIntValue{}, &BigIntValue{Value: big.NewInt(-1)}) }) t.Run("should decode top-level", func(t *testing.T) { + testDecodeTopLevel(t, codec, "", &U8Value{}, &U8Value{Value: 0}) + testDecodeNested(t, codec, "01", &U8Value{}, &U8Value{Value: 1}) + + testDecodeTopLevel(t, codec, "ff", &I8Value{}, &I8Value{Value: -1}) + testDecodeTopLevel(t, codec, "80", &I8Value{}, &I8Value{Value: -128}) + + testDecodeTopLevel(t, codec, "4242", &U16Value{}, &U16Value{Value: 0x4242}) + testDecodeTopLevel(t, codec, "ffff", &U16Value{}, &U16Value{Value: 65535}) + + testDecodeTopLevel(t, codec, "ffff", &I16Value{}, &I16Value{Value: -1}) + testDecodeTopLevel(t, codec, "8000", &I16Value{}, &I16Value{Value: -32768}) + + testDecodeTopLevel(t, codec, "41424344", &U32Value{}, &U32Value{Value: 0x41424344}) + testDecodeTopLevel(t, codec, "ffffffff", &U32Value{}, &U32Value{Value: 4294967295}) + + testDecodeTopLevel(t, codec, "ffffffff", &I32Value{}, &I32Value{Value: -1}) + testDecodeTopLevel(t, codec, "80000000", &I32Value{}, &I32Value{Value: -2147483648}) + + testDecodeTopLevel(t, codec, "4142434445464748", &U64Value{}, &U64Value{Value: 0x4142434445464748}) + testDecodeTopLevel(t, codec, "ffffffffffffffff", &U64Value{}, &U64Value{Value: 18446744073709551615}) + + testDecodeTopLevel(t, codec, "ffffffffffffffff", &I64Value{}, &I64Value{Value: -1}) + testDecodeTopLevel(t, codec, "8000000000000000", &I64Value{}, &I64Value{Value: -9223372036854775808}) }) } diff --git a/abi/codec_test.go b/abi/codec_test.go index 696a73c..480f631 100644 --- a/abi/codec_test.go +++ b/abi/codec_test.go @@ -38,17 +38,6 @@ func TestCodec_EncodeNested(t *testing.T) { require.Equal(t, expected, hex.EncodeToString(encoded)) } - t.Run("address", func(t *testing.T) { - data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") - doTest(t, AddressValue{Value: data}, "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") - }) - - t.Run("address (bad)", func(t *testing.T) { - data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d42") - _, err := codec.EncodeNested(AddressValue{Value: data}) - require.ErrorContains(t, err, "public key (address) has invalid length") - }) - t.Run("struct", func(t *testing.T) { fooStruct := StructValue{ Fields: []Field{ @@ -146,17 +135,6 @@ func TestCodec_EncodeTopLevel(t *testing.T) { require.Equal(t, expected, hex.EncodeToString(encoded)) } - t.Run("address", func(t *testing.T) { - data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") - doTest(t, AddressValue{Value: data}, "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") - }) - - t.Run("address (bad)", func(t *testing.T) { - data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d42") - _, err := codec.EncodeTopLevel(AddressValue{Value: data}) - require.ErrorContains(t, err, "public key (address) has invalid length") - }) - t.Run("struct", func(t *testing.T) { fooStruct := StructValue{ Fields: []Field{ @@ -220,78 +198,6 @@ func TestCodec_DecodeNested(t *testing.T) { pubKeyLength: 32, }) - t.Run("u8", func(t *testing.T) { - data, _ := hex.DecodeString("01") - destination := &U8Value{} - - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &U8Value{Value: 0x01}, destination) - }) - - t.Run("i8", func(t *testing.T) { - data, _ := hex.DecodeString("ff") - destination := &I8Value{} - - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &I8Value{Value: -1}, destination) - }) - - t.Run("u16", func(t *testing.T) { - data, _ := hex.DecodeString("4142") - destination := &U16Value{} - - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &U16Value{Value: 0x4142}, destination) - }) - - t.Run("i16", func(t *testing.T) { - data, _ := hex.DecodeString("ffff") - destination := &I16Value{} - - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &I16Value{Value: -1}, destination) - }) - - t.Run("u32", func(t *testing.T) { - data, _ := hex.DecodeString("41424344") - destination := &U32Value{} - - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &U32Value{Value: 0x41424344}, destination) - }) - - t.Run("i32", func(t *testing.T) { - data, _ := hex.DecodeString("ffffffff") - destination := &I32Value{} - - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &I32Value{Value: -1}, destination) - }) - - t.Run("u64", func(t *testing.T) { - data, _ := hex.DecodeString("4142434445464748") - destination := &U64Value{} - - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &U64Value{Value: 0x4142434445464748}, destination) - }) - - t.Run("i64", func(t *testing.T) { - data, _ := hex.DecodeString("ffffffffffffffff") - destination := &I64Value{} - - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &I64Value{Value: -1}, destination) - }) - t.Run("u16, should err because it cannot read 2 bytes", func(t *testing.T) { data, _ := hex.DecodeString("01") destination := &U16Value{} @@ -316,26 +222,6 @@ func TestCodec_DecodeNested(t *testing.T) { require.ErrorContains(t, err, "cannot read exactly 8 bytes") }) - t.Run("bigInt", func(t *testing.T) { - data, _ := hex.DecodeString("00000000") - destination := &BigIntValue{} - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &BigIntValue{Value: big.NewInt(0)}, destination) - - data, _ = hex.DecodeString("0000000101") - destination = &BigIntValue{} - err = codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &BigIntValue{Value: big.NewInt(1)}, destination) - - data, _ = hex.DecodeString("00000001ff") - destination = &BigIntValue{} - err = codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &BigIntValue{Value: big.NewInt(-1)}, destination) - }) - t.Run("bigInt: should err when bad data", func(t *testing.T) { data, _ := hex.DecodeString("0000000301") destination := &BigIntValue{} @@ -343,23 +229,6 @@ func TestCodec_DecodeNested(t *testing.T) { require.ErrorContains(t, err, "cannot decode (nested) *abi.BigIntValue, because of: cannot read exactly 3 bytes") }) - t.Run("address", func(t *testing.T) { - data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") - - destination := &AddressValue{} - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &AddressValue{Value: data}, destination) - }) - - t.Run("address (bad)", func(t *testing.T) { - data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d42") - - destination := &AddressValue{} - err := codec.DecodeNested(data, destination) - require.ErrorContains(t, err, "cannot read exactly 32 bytes") - }) - t.Run("struct", func(t *testing.T) { data, _ := hex.DecodeString("014142") @@ -500,78 +369,6 @@ func TestCodec_DecodeTopLevel(t *testing.T) { pubKeyLength: 32, }) - t.Run("u8", func(t *testing.T) { - data, _ := hex.DecodeString("01") - destination := &U8Value{} - - err := codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &U8Value{Value: 0x01}, destination) - }) - - t.Run("i8", func(t *testing.T) { - data, _ := hex.DecodeString("ff") - destination := &I8Value{} - - err := codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &I8Value{Value: -1}, destination) - }) - - t.Run("u16", func(t *testing.T) { - data, _ := hex.DecodeString("02") - destination := &U16Value{} - - err := codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &U16Value{Value: 0x0002}, destination) - }) - - t.Run("i16", func(t *testing.T) { - data, _ := hex.DecodeString("ffff") - destination := &I16Value{} - - err := codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &I16Value{Value: -1}, destination) - }) - - t.Run("u32", func(t *testing.T) { - data, _ := hex.DecodeString("03") - destination := &U32Value{} - - err := codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &U32Value{Value: 0x00000003}, destination) - }) - - t.Run("i32", func(t *testing.T) { - data, _ := hex.DecodeString("ffffffff") - destination := &I32Value{} - - err := codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &I32Value{Value: -1}, destination) - }) - - t.Run("u64", func(t *testing.T) { - data, _ := hex.DecodeString("04") - destination := &U64Value{} - - err := codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &U64Value{Value: 0x0000000000000004}, destination) - }) - - t.Run("i64", func(t *testing.T) { - data, _ := hex.DecodeString("ffffffffffffffff") - destination := &I64Value{} - - err := codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &I64Value{Value: -1}, destination) - }) - t.Run("u8, i8: should err because decoded value is too large", func(t *testing.T) { data, _ := hex.DecodeString("4142") @@ -632,23 +429,6 @@ func TestCodec_DecodeTopLevel(t *testing.T) { require.Equal(t, &BigIntValue{Value: big.NewInt(-1)}, destination) }) - t.Run("address", func(t *testing.T) { - data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") - - destination := &AddressValue{} - err := codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &AddressValue{Value: data}, destination) - }) - - t.Run("address (bad)", func(t *testing.T) { - data, _ := hex.DecodeString("0139472eff6886771a982f3083da5d42") - - destination := &AddressValue{} - err := codec.DecodeTopLevel(data, destination) - require.ErrorContains(t, err, "public key (address) has invalid length") - }) - t.Run("struct", func(t *testing.T) { data, _ := hex.DecodeString("014142") From 2647e50ba787de9e46e5ff88224e23abc626a304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 17 Apr 2024 10:36:48 +0300 Subject: [PATCH 09/24] Split files. --- abi/codecForSimpleValues_numerical.go | 49 ------------------- abi/codecForSimpleValues_numerical_big.go | 57 ++++++++++++++++++++++ abi/codecForSimpleValues_numerical_test.go | 13 ----- 3 files changed, 57 insertions(+), 62 deletions(-) create mode 100644 abi/codecForSimpleValues_numerical_big.go diff --git a/abi/codecForSimpleValues_numerical.go b/abi/codecForSimpleValues_numerical.go index 898c46a..0a0b38d 100644 --- a/abi/codecForSimpleValues_numerical.go +++ b/abi/codecForSimpleValues_numerical.go @@ -87,52 +87,3 @@ func (c *codec) decodeTopLevelSignedNumber(data []byte, maxValue int64) (int64, return n, nil } - -func (c *codec) encodeNestedBigNumber(writer io.Writer, value *big.Int) error { - data := twos.ToBytes(value) - dataLength := len(data) - - // Write the length of the payload - err := encodeLength(writer, uint32(dataLength)) - if err != nil { - return err - } - - // Write the payload - _, err = writer.Write(data) - if err != nil { - return err - } - - return nil -} - -func (c *codec) encodeTopLevelBigNumber(writer io.Writer, value *big.Int) error { - data := twos.ToBytes(value) - _, err := writer.Write(data) - if err != nil { - return err - } - - return nil -} - -func (c *codec) decodeNestedBigNumber(reader io.Reader) (*big.Int, error) { - // Read the length of the payload - length, err := decodeLength(reader) - if err != nil { - return nil, err - } - - // Read the payload - data, err := readBytesExactly(reader, int(length)) - if err != nil { - return nil, err - } - - return twos.FromBytes(data), nil -} - -func (c *codec) decodeTopLevelBigNumber(data []byte) *big.Int { - return twos.FromBytes(data) -} diff --git a/abi/codecForSimpleValues_numerical_big.go b/abi/codecForSimpleValues_numerical_big.go new file mode 100644 index 0000000..4996c11 --- /dev/null +++ b/abi/codecForSimpleValues_numerical_big.go @@ -0,0 +1,57 @@ +package abi + +import ( + "io" + "math/big" + + twos "github.com/multiversx/mx-components-big-int/twos-complement" +) + +func (c *codec) encodeNestedBigNumber(writer io.Writer, value *big.Int) error { + data := twos.ToBytes(value) + dataLength := len(data) + + // Write the length of the payload + err := encodeLength(writer, uint32(dataLength)) + if err != nil { + return err + } + + // Write the payload + _, err = writer.Write(data) + if err != nil { + return err + } + + return nil +} + +func (c *codec) encodeTopLevelBigNumber(writer io.Writer, value *big.Int) error { + data := twos.ToBytes(value) + _, err := writer.Write(data) + if err != nil { + return err + } + + return nil +} + +func (c *codec) decodeNestedBigNumber(reader io.Reader) (*big.Int, error) { + // Read the length of the payload + length, err := decodeLength(reader) + if err != nil { + return nil, err + } + + // Read the payload + data, err := readBytesExactly(reader, int(length)) + if err != nil { + return nil, err + } + + return twos.FromBytes(data), nil +} + +func (c *codec) decodeTopLevelBigNumber(data []byte) *big.Int { + return twos.FromBytes(data) +} diff --git a/abi/codecForSimpleValues_numerical_test.go b/abi/codecForSimpleValues_numerical_test.go index b227c15..203d192 100644 --- a/abi/codecForSimpleValues_numerical_test.go +++ b/abi/codecForSimpleValues_numerical_test.go @@ -1,7 +1,6 @@ package abi import ( - "math/big" "testing" ) @@ -53,10 +52,6 @@ func TestCodec_Numerical(t *testing.T) { testEncodeNested(t, codec, I64Value{Value: 0x0000000000000011}, "0000000000000011") testEncodeNested(t, codec, I64Value{Value: -1}, "ffffffffffffffff") testEncodeNested(t, codec, I64Value{Value: -9223372036854775808}, "8000000000000000") - - testEncodeNested(t, codec, BigIntValue{Value: big.NewInt(0)}, "00000000") - testEncodeNested(t, codec, BigIntValue{Value: big.NewInt(1)}, "0000000101") - testEncodeNested(t, codec, BigIntValue{Value: big.NewInt(-1)}, "00000001ff") }) t.Run("should encode top-level", func(t *testing.T) { @@ -85,10 +80,6 @@ func TestCodec_Numerical(t *testing.T) { testEncodeTopLevel(t, codec, I64Value{Value: 0x0000000000000000}, "") testEncodeTopLevel(t, codec, I64Value{Value: 0x0000000000000011}, "11") testEncodeTopLevel(t, codec, I64Value{Value: -1}, "ff") - - testEncodeTopLevel(t, codec, BigIntValue{Value: big.NewInt(0)}, "") - testEncodeTopLevel(t, codec, BigIntValue{Value: big.NewInt(1)}, "01") - testEncodeTopLevel(t, codec, BigIntValue{Value: big.NewInt(-1)}, "ff") }) t.Run("should decode nested", func(t *testing.T) { @@ -115,10 +106,6 @@ func TestCodec_Numerical(t *testing.T) { testDecodeNested(t, codec, "ffffffffffffffff", &I64Value{}, &I64Value{Value: -1}) testDecodeNested(t, codec, "8000000000000000", &I64Value{}, &I64Value{Value: -9223372036854775808}) - - testDecodeNested(t, codec, "00000000", &BigIntValue{}, &BigIntValue{Value: big.NewInt(0)}) - testDecodeNested(t, codec, "0000000101", &BigIntValue{}, &BigIntValue{Value: big.NewInt(1)}) - testDecodeNested(t, codec, "00000001ff", &BigIntValue{}, &BigIntValue{Value: big.NewInt(-1)}) }) t.Run("should decode top-level", func(t *testing.T) { From 31bcbeba8a479fbbd558e3d2792082aa92f3af7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 17 Apr 2024 11:48:46 +0300 Subject: [PATCH 10/24] BigUInt vs. BigInt. --- abi/codec.go | 23 +++++- abi/codecForSimpleValues_numerical_big.go | 32 ++++++-- ...codecForSimpleValues_numerical_big_test.go | 78 +++++++++++++++++++ abi/values.go | 7 +- 4 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 abi/codecForSimpleValues_numerical_big_test.go diff --git a/abi/codec.go b/abi/codec.go index 60d04ac..b74f1d5 100644 --- a/abi/codec.go +++ b/abi/codec.go @@ -60,8 +60,10 @@ func (c *codec) doEncodeNested(writer io.Writer, value any) error { return c.encodeNestedNumber(writer, value.Value, 4) case I64Value: return c.encodeNestedNumber(writer, value.Value, 8) + case BigUIntValue: + return c.encodeNestedBigNumber(writer, value.Value, false) case BigIntValue: - return c.encodeNestedBigNumber(writer, value.Value) + return c.encodeNestedBigNumber(writer, value.Value, true) case AddressValue: return c.encodeNestedAddress(writer, value) case StringValue: @@ -112,8 +114,10 @@ func (c *codec) doEncodeTopLevel(writer io.Writer, value any) error { return c.encodeTopLevelSignedNumber(writer, int64(value.Value)) case I64Value: return c.encodeTopLevelSignedNumber(writer, value.Value) + case BigUIntValue: + return c.encodeTopLevelBigNumber(writer, value.Value, false) case BigIntValue: - return c.encodeTopLevelBigNumber(writer, value.Value) + return c.encodeTopLevelBigNumber(writer, value.Value, true) case AddressValue: return c.encodeTopLevelAddress(writer, value) case StringValue: @@ -160,8 +164,16 @@ func (c *codec) doDecodeNested(reader io.Reader, value any) error { return c.decodeNestedNumber(reader, &value.Value, 4) case *I64Value: return c.decodeNestedNumber(reader, &value.Value, 8) + case *BigUIntValue: + n, err := c.decodeNestedBigNumber(reader, false) + if err != nil { + return err + } + + value.Value = n + return nil case *BigIntValue: - n, err := c.decodeNestedBigNumber(reader) + n, err := c.decodeNestedBigNumber(reader, true) if err != nil { return err } @@ -258,8 +270,11 @@ func (c *codec) doDecodeTopLevel(data []byte, value any) error { } value.Value = int64(n) + case *BigUIntValue: + n := c.decodeTopLevelBigNumber(data, false) + value.Value = n case *BigIntValue: - n := c.decodeTopLevelBigNumber(data) + n := c.decodeTopLevelBigNumber(data, true) value.Value = n case *AddressValue: return c.decodeTopLevelAddress(data, value) diff --git a/abi/codecForSimpleValues_numerical_big.go b/abi/codecForSimpleValues_numerical_big.go index 4996c11..926494e 100644 --- a/abi/codecForSimpleValues_numerical_big.go +++ b/abi/codecForSimpleValues_numerical_big.go @@ -7,8 +7,8 @@ import ( twos "github.com/multiversx/mx-components-big-int/twos-complement" ) -func (c *codec) encodeNestedBigNumber(writer io.Writer, value *big.Int) error { - data := twos.ToBytes(value) +func (c *codec) encodeNestedBigNumber(writer io.Writer, value *big.Int, withSign bool) error { + data := bigIntToBytes(value, withSign) dataLength := len(data) // Write the length of the payload @@ -26,8 +26,8 @@ func (c *codec) encodeNestedBigNumber(writer io.Writer, value *big.Int) error { return nil } -func (c *codec) encodeTopLevelBigNumber(writer io.Writer, value *big.Int) error { - data := twos.ToBytes(value) +func (c *codec) encodeTopLevelBigNumber(writer io.Writer, value *big.Int, withSign bool) error { + data := bigIntToBytes(value, withSign) _, err := writer.Write(data) if err != nil { return err @@ -36,7 +36,7 @@ func (c *codec) encodeTopLevelBigNumber(writer io.Writer, value *big.Int) error return nil } -func (c *codec) decodeNestedBigNumber(reader io.Reader) (*big.Int, error) { +func (c *codec) decodeNestedBigNumber(reader io.Reader, withSign bool) (*big.Int, error) { // Read the length of the payload length, err := decodeLength(reader) if err != nil { @@ -49,9 +49,25 @@ func (c *codec) decodeNestedBigNumber(reader io.Reader) (*big.Int, error) { return nil, err } - return twos.FromBytes(data), nil + return bigIntFromBytes(data, withSign), nil } -func (c *codec) decodeTopLevelBigNumber(data []byte) *big.Int { - return twos.FromBytes(data) +func (c *codec) decodeTopLevelBigNumber(data []byte, withSign bool) *big.Int { + return bigIntFromBytes(data, withSign) +} + +func bigIntToBytes(value *big.Int, withSign bool) []byte { + if withSign { + return twos.ToBytes(value) + } + + return value.Bytes() +} + +func bigIntFromBytes(data []byte, withSign bool) *big.Int { + if withSign { + return twos.FromBytes(data) + } + + return big.NewInt(0).SetBytes(data) } diff --git a/abi/codecForSimpleValues_numerical_big_test.go b/abi/codecForSimpleValues_numerical_big_test.go new file mode 100644 index 0000000..46467c7 --- /dev/null +++ b/abi/codecForSimpleValues_numerical_big_test.go @@ -0,0 +1,78 @@ +package abi + +import ( + "math/big" + "testing" +) + +func TestCodec_NumericalBig(t *testing.T) { + codec, _ := newCodec(argsNewCodec{ + pubKeyLength: 32, + }) + + t.Run("should encode nested", func(t *testing.T) { + testEncodeNested(t, codec, BigUIntValue{Value: big.NewInt(0)}, "00000000") + testEncodeNested(t, codec, BigUIntValue{Value: big.NewInt(1)}, "0000000101") + testEncodeNested(t, codec, BigUIntValue{Value: big.NewInt(127)}, "000000017f") + testEncodeNested(t, codec, BigUIntValue{Value: big.NewInt(128)}, "0000000180") + testEncodeNested(t, codec, BigUIntValue{Value: big.NewInt(255)}, "00000001ff") + testEncodeNested(t, codec, BigUIntValue{Value: big.NewInt(256)}, "000000020100") + + testEncodeNested(t, codec, BigIntValue{Value: big.NewInt(0)}, "00000000") + testEncodeNested(t, codec, BigIntValue{Value: big.NewInt(1)}, "0000000101") + testEncodeNested(t, codec, BigIntValue{Value: big.NewInt(-1)}, "00000001ff") + testEncodeNested(t, codec, BigIntValue{Value: big.NewInt(127)}, "000000017f") + testEncodeNested(t, codec, BigIntValue{Value: big.NewInt(128)}, "000000020080") + testEncodeNested(t, codec, BigIntValue{Value: big.NewInt(255)}, "0000000200ff") + testEncodeNested(t, codec, BigIntValue{Value: big.NewInt(256)}, "000000020100") + }) + + t.Run("should encode top-level", func(t *testing.T) { + testEncodeTopLevel(t, codec, BigUIntValue{Value: big.NewInt(0)}, "") + testEncodeTopLevel(t, codec, BigUIntValue{Value: big.NewInt(1)}, "01") + testEncodeTopLevel(t, codec, BigUIntValue{Value: big.NewInt(127)}, "7f") + testEncodeTopLevel(t, codec, BigUIntValue{Value: big.NewInt(128)}, "80") + testEncodeTopLevel(t, codec, BigUIntValue{Value: big.NewInt(256)}, "0100") + + testEncodeTopLevel(t, codec, BigIntValue{Value: big.NewInt(0)}, "") + testEncodeTopLevel(t, codec, BigIntValue{Value: big.NewInt(1)}, "01") + testEncodeTopLevel(t, codec, BigIntValue{Value: big.NewInt(-1)}, "ff") + testEncodeTopLevel(t, codec, BigIntValue{Value: big.NewInt(127)}, "7f") + testEncodeTopLevel(t, codec, BigIntValue{Value: big.NewInt(128)}, "0080") + testEncodeTopLevel(t, codec, BigIntValue{Value: big.NewInt(255)}, "00ff") + testEncodeTopLevel(t, codec, BigIntValue{Value: big.NewInt(256)}, "0100") + }) + + t.Run("should decode nested", func(t *testing.T) { + testDecodeNested(t, codec, "00000000", &BigUIntValue{}, &BigUIntValue{Value: big.NewInt(0)}) + testDecodeNested(t, codec, "0000000101", &BigUIntValue{}, &BigUIntValue{Value: big.NewInt(1)}) + testDecodeNested(t, codec, "000000017f", &BigUIntValue{}, &BigUIntValue{Value: big.NewInt(127)}) + testDecodeNested(t, codec, "0000000180", &BigUIntValue{}, &BigUIntValue{Value: big.NewInt(128)}) + testDecodeNested(t, codec, "00000001ff", &BigUIntValue{}, &BigUIntValue{Value: big.NewInt(255)}) + testDecodeNested(t, codec, "000000020100", &BigUIntValue{}, &BigUIntValue{Value: big.NewInt(256)}) + + testDecodeNested(t, codec, "00000000", &BigIntValue{}, &BigIntValue{Value: big.NewInt(0)}) + testDecodeNested(t, codec, "0000000101", &BigIntValue{}, &BigIntValue{Value: big.NewInt(1)}) + testDecodeNested(t, codec, "00000001ff", &BigIntValue{}, &BigIntValue{Value: big.NewInt(-1)}) + testDecodeNested(t, codec, "000000017f", &BigIntValue{}, &BigIntValue{Value: big.NewInt(127)}) + testDecodeNested(t, codec, "000000020080", &BigIntValue{}, &BigIntValue{Value: big.NewInt(128)}) + testDecodeNested(t, codec, "0000000200ff", &BigIntValue{}, &BigIntValue{Value: big.NewInt(255)}) + testDecodeNested(t, codec, "000000020100", &BigIntValue{}, &BigIntValue{Value: big.NewInt(256)}) + }) + + t.Run("should decode top-level", func(t *testing.T) { + testDecodeTopLevel(t, codec, "", &BigUIntValue{}, &BigUIntValue{Value: big.NewInt(0)}) + testDecodeTopLevel(t, codec, "01", &BigUIntValue{}, &BigUIntValue{Value: big.NewInt(1)}) + testDecodeTopLevel(t, codec, "7f", &BigUIntValue{}, &BigUIntValue{Value: big.NewInt(127)}) + testDecodeTopLevel(t, codec, "80", &BigUIntValue{}, &BigUIntValue{Value: big.NewInt(128)}) + testDecodeTopLevel(t, codec, "0100", &BigUIntValue{}, &BigUIntValue{Value: big.NewInt(256)}) + + testDecodeTopLevel(t, codec, "", &BigIntValue{}, &BigIntValue{Value: big.NewInt(0)}) + testDecodeTopLevel(t, codec, "01", &BigIntValue{}, &BigIntValue{Value: big.NewInt(1)}) + testDecodeTopLevel(t, codec, "ff", &BigIntValue{}, &BigIntValue{Value: big.NewInt(-1)}) + testDecodeTopLevel(t, codec, "7f", &BigIntValue{}, &BigIntValue{Value: big.NewInt(127)}) + testDecodeTopLevel(t, codec, "0080", &BigIntValue{}, &BigIntValue{Value: big.NewInt(128)}) + testDecodeTopLevel(t, codec, "00ff", &BigIntValue{}, &BigIntValue{Value: big.NewInt(255)}) + testDecodeTopLevel(t, codec, "0100", &BigIntValue{}, &BigIntValue{Value: big.NewInt(256)}) + }) +} diff --git a/abi/values.go b/abi/values.go index 58f4095..69584e5 100644 --- a/abi/values.go +++ b/abi/values.go @@ -42,7 +42,12 @@ type I64Value struct { Value int64 } -// BigIntValue is a wrapper for a big integer +// BigIntValue is a wrapper for a big integer (unsigned) +type BigUIntValue struct { + Value *big.Int +} + +// BigIntValue is a wrapper for a big integer (signed) type BigIntValue struct { Value *big.Int } From b14a99163e57ab92eb5d22957f7888f367dfe66d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 17 Apr 2024 13:35:09 +0300 Subject: [PATCH 11/24] Multi-values vs. single values. Sketch optionals. --- abi/serializer.go | 40 ++++++++++++++++++++++++++++++ abi/valuesMulti.go | 33 ++++++++++++++++++++++++ abi/{values.go => valuesSingle.go} | 21 ---------------- 3 files changed, 73 insertions(+), 21 deletions(-) create mode 100644 abi/valuesMulti.go rename abi/{values.go => valuesSingle.go} (79%) diff --git a/abi/serializer.go b/abi/serializer.go index 63f34d9..9f5d6ef 100644 --- a/abi/serializer.go +++ b/abi/serializer.go @@ -68,6 +68,15 @@ func (s *serializer) doSerialize(partsHolder *partsHolder, inputValues []any) er } switch value := value.(type) { + case InputOptionalValue: + if i != len(inputValues)-1 { + // Usage of multiple optional values is not recommended: + // https://docs.multiversx.com/developers/data/multi-values + // Thus, here, we disallow them. + return errors.New("an optional value must be last among input values") + } + + err = s.serializeInputOptionalValue(partsHolder, value) case InputMultiValue: err = s.serializeInputMultiValue(partsHolder, value) case InputVariadicValues: @@ -119,6 +128,15 @@ func (s *serializer) doDeserialize(partsHolder *partsHolder, outputValues []any) } switch value := value.(type) { + case *OutputOptionalValue: + if i != len(outputValues)-1 { + // Usage of multiple optional values is not recommended: + // https://docs.multiversx.com/developers/data/multi-values + // Thus, here, we disallow them. + return errors.New("an optional value must be last among output values") + } + + err = s.deserializeOutputOptionalValue(partsHolder, value) case *OutputMultiValue: err = s.deserializeOutputMultiValue(partsHolder, value) case *OutputVariadicValues: @@ -139,6 +157,14 @@ func (s *serializer) doDeserialize(partsHolder *partsHolder, outputValues []any) return nil } +func (s *serializer) serializeInputOptionalValue(partsHolder *partsHolder, value InputOptionalValue) error { + if value.Value == nil { + return nil + } + + return s.doSerialize(partsHolder, []any{value.Value}) +} + func (s *serializer) serializeInputMultiValue(partsHolder *partsHolder, value InputMultiValue) error { for _, item := range value.Items { err := s.doSerialize(partsHolder, []any{item}) @@ -170,6 +196,20 @@ func (s *serializer) serializeDirectlyEncodableValue(partsHolder *partsHolder, v return partsHolder.appendToLastPart(data) } +func (s *serializer) deserializeOutputOptionalValue(partsHolder *partsHolder, value *OutputOptionalValue) error { + for partsHolder.isFocusedBeyondLastPart() { + return nil + } + + err := s.doDeserialize(partsHolder, []any{value.Value}) + if err != nil { + return err + } + + value.HasValue = true + return nil +} + func (s *serializer) deserializeOutputMultiValue(partsHolder *partsHolder, value *OutputMultiValue) error { for _, item := range value.Items { err := s.doDeserialize(partsHolder, []any{item}) diff --git a/abi/valuesMulti.go b/abi/valuesMulti.go new file mode 100644 index 0000000..a88f6d5 --- /dev/null +++ b/abi/valuesMulti.go @@ -0,0 +1,33 @@ +package abi + +// InputMultiValue is a multi-value (used for encoding) +type InputMultiValue struct { + Items []any +} + +// OutputMultiValue is a multi-value (used for decoding) +type OutputMultiValue struct { + Items []any +} + +// InputVariadicValues holds variadic values (used for encoding) +type InputVariadicValues struct { + Items []any +} + +// OutputVariadicValues holds variadic values (used for decoding) +type OutputVariadicValues struct { + Items []any + ItemCreator func() any +} + +// InputOptionalValue holds an optional value (used for encoding) +type InputOptionalValue struct { + Value any +} + +// OutputOptionalValue holds an optional value (used for decoding) +type OutputOptionalValue struct { + Value any + HasValue bool +} diff --git a/abi/values.go b/abi/valuesSingle.go similarity index 79% rename from abi/values.go rename to abi/valuesSingle.go index 69584e5..eff201a 100644 --- a/abi/values.go +++ b/abi/valuesSingle.go @@ -104,24 +104,3 @@ type OutputListValue struct { Items []any ItemCreator func() any } - -// InputMultiValue is a multi-value (used for encoding) -type InputMultiValue struct { - Items []any -} - -// OutputMultiValue is a multi-value (used for decoding) -type OutputMultiValue struct { - Items []any -} - -// InputVariadicValues holds variadic values (used for encoding) -type InputVariadicValues struct { - Items []any -} - -// OutputVariadicValues holds variadic values (used for decoding) -type OutputVariadicValues struct { - Items []any - ItemCreator func() any -} From a0a6581936ac61be1b66c9f75a3a50eed4f10f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Thu, 18 Apr 2024 14:51:07 +0300 Subject: [PATCH 12/24] Tests for optionals. --- abi/serializer_test.go | 69 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/abi/serializer_test.go b/abi/serializer_test.go index d717af9..0036ff7 100644 --- a/abi/serializer_test.go +++ b/abi/serializer_test.go @@ -41,6 +41,35 @@ func TestSerializer_Serialize(t *testing.T) { require.Equal(t, "42@4243", data) }) + t.Run("optional (missing)", func(t *testing.T) { + data, err := serializer.Serialize([]any{ + U8Value{Value: 0x42}, + InputOptionalValue{}, + }) + + require.NoError(t, err) + require.Equal(t, "42", data) + }) + + t.Run("optional (provided)", func(t *testing.T) { + data, err := serializer.Serialize([]any{ + U8Value{Value: 0x42}, + InputOptionalValue{Value: U8Value{Value: 0x43}}, + }) + + require.NoError(t, err) + require.Equal(t, "42@43", data) + }) + + t.Run("optional: should err because optional must be last", func(t *testing.T) { + _, err := serializer.Serialize([]any{ + InputOptionalValue{Value: 0x42}, + U8Value{Value: 0x43}, + }) + + require.ErrorContains(t, err, "an optional value must be last among input values") + }) + t.Run("multi", func(t *testing.T) { data, err := serializer.Serialize([]any{ InputMultiValue{ @@ -196,6 +225,46 @@ func TestSerializer_Deserialize(t *testing.T) { }, outputValues) }) + t.Run("optional (missing)", func(t *testing.T) { + outputValues := []any{ + &U8Value{}, + &OutputOptionalValue{}, + } + + err := serializer.Deserialize("42", outputValues) + + require.Nil(t, err) + require.Equal(t, []any{ + &U8Value{Value: 0x42}, + &OutputOptionalValue{HasValue: false}, + }, outputValues) + }) + + t.Run("optional (provided)", func(t *testing.T) { + outputValues := []any{ + &U8Value{}, + &OutputOptionalValue{Value: &U8Value{}}, + } + + err := serializer.Deserialize("42@43", outputValues) + + require.Nil(t, err) + require.Equal(t, []any{ + &U8Value{Value: 0x42}, + &OutputOptionalValue{Value: &U8Value{Value: 0x43}, HasValue: true}, + }, outputValues) + }) + + t.Run("optional: should err because optional must be last", func(t *testing.T) { + outputValues := []any{ + &OutputOptionalValue{Value: &U8Value{}}, + &U8Value{}, + } + + err := serializer.Deserialize("43@42", outputValues) + require.ErrorContains(t, err, "an optional value must be last among output values") + }) + t.Run("multi", func(t *testing.T) { outputValues := []any{ &OutputMultiValue{ From f5966877e919577e4456e4af21f13622cb4b7009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Thu, 18 Apr 2024 15:43:18 +0300 Subject: [PATCH 13/24] Move tests for enum and struct. --- abi/codecForSimpleValues_enum_test.go | 159 +++++++++++++++ abi/codecForSimpleValues_struct_test.go | 95 +++++++++ abi/codec_test.go | 258 ------------------------ 3 files changed, 254 insertions(+), 258 deletions(-) create mode 100644 abi/codecForSimpleValues_enum_test.go create mode 100644 abi/codecForSimpleValues_struct_test.go diff --git a/abi/codecForSimpleValues_enum_test.go b/abi/codecForSimpleValues_enum_test.go new file mode 100644 index 0000000..220d190 --- /dev/null +++ b/abi/codecForSimpleValues_enum_test.go @@ -0,0 +1,159 @@ +package abi + +import ( + "testing" +) + +func TestCodec_Enum(t *testing.T) { + codec, _ := newCodec(argsNewCodec{ + pubKeyLength: 32, + }) + + t.Run("should encode nested", func(t *testing.T) { + testEncodeNested(t, codec, + EnumValue{ + Discriminant: 0, + }, + "00", + ) + + testEncodeNested(t, codec, + EnumValue{ + Discriminant: 42, + }, + "2a", + ) + + testEncodeNested(t, codec, + EnumValue{ + Discriminant: 42, + Fields: []Field{ + { + Value: U8Value{Value: 0x01}, + }, + { + Value: U16Value{Value: 0x4142}, + }, + }, + }, + "2a014142", + ) + }) + + t.Run("should encode top-level", func(t *testing.T) { + testEncodeTopLevel(t, codec, + EnumValue{ + Discriminant: 0, + }, + "", + ) + + testEncodeTopLevel(t, codec, + EnumValue{ + Discriminant: 42, + }, + "2a", + ) + + testEncodeTopLevel(t, codec, + EnumValue{ + Discriminant: 42, + Fields: []Field{ + { + Value: U8Value{Value: 0x01}, + }, + { + Value: U16Value{Value: 0x4142}, + }, + }, + }, + "2a014142", + ) + }) + + t.Run("should decode nested", func(t *testing.T) { + testDecodeNested(t, codec, + "00", + &EnumValue{}, + &EnumValue{ + Discriminant: 0x00, + }, + ) + + testDecodeNested(t, codec, + "2a", + &EnumValue{}, + &EnumValue{ + Discriminant: 42, + }, + ) + + testDecodeNested(t, codec, + "01014142", + &EnumValue{ + Fields: []Field{ + { + Value: &U8Value{}, + }, + { + Value: &U16Value{}, + }, + }, + }, + &EnumValue{ + Discriminant: 0x01, + Fields: []Field{ + { + Value: &U8Value{Value: 0x01}, + }, + { + Value: &U16Value{Value: 0x4142}, + }, + }, + }, + ) + }) + + t.Run("should decode top-level", func(t *testing.T) { + testDecodeTopLevel(t, codec, + "", + &EnumValue{}, + &EnumValue{ + Discriminant: 0x00, + }, + ) + + testDecodeTopLevel(t, codec, + "2a", + &EnumValue{}, + &EnumValue{ + Discriminant: 42, + }, + ) + + testDecodeTopLevel(t, codec, + "01014142", + &EnumValue{ + Fields: []Field{ + { + Value: &U8Value{}, + }, + { + Value: &U16Value{}, + }, + }, + }, + &EnumValue{ + Discriminant: 0x01, + Fields: []Field{ + { + Value: &U8Value{Value: 0x01}, + }, + { + Value: &U16Value{Value: 0x4142}, + }, + }, + }, + ) + }) +} diff --git a/abi/codecForSimpleValues_struct_test.go b/abi/codecForSimpleValues_struct_test.go new file mode 100644 index 0000000..24e7171 --- /dev/null +++ b/abi/codecForSimpleValues_struct_test.go @@ -0,0 +1,95 @@ +package abi + +import ( + "testing" +) + +func TestCodec_Struct(t *testing.T) { + codec, _ := newCodec(argsNewCodec{ + pubKeyLength: 32, + }) + + t.Run("should encode nested", func(t *testing.T) { + testEncodeNested(t, codec, + StructValue{ + Fields: []Field{ + { + Value: U8Value{Value: 0x01}, + }, + { + Value: U16Value{Value: 0x4142}, + }, + }, + }, + "014142", + ) + }) + + t.Run("should encode top-level", func(t *testing.T) { + testEncodeTopLevel(t, codec, + StructValue{ + Fields: []Field{ + { + Value: U8Value{Value: 0x01}, + }, + { + Value: U16Value{Value: 0x4142}, + }, + }, + }, + "014142", + ) + }) + + t.Run("should decode nested", func(t *testing.T) { + testDecodeNested(t, codec, + "014142", + &StructValue{ + Fields: []Field{ + { + Value: &U8Value{}, + }, + { + Value: &U16Value{}, + }, + }, + }, + &StructValue{ + Fields: []Field{ + { + Value: &U8Value{Value: 0x01}, + }, + { + Value: &U16Value{Value: 0x4142}, + }, + }, + }, + ) + }) + + t.Run("should decode top-level", func(t *testing.T) { + testDecodeTopLevel(t, codec, + "014142", + &StructValue{ + Fields: []Field{ + { + Value: &U8Value{}, + }, + { + Value: &U16Value{}, + }, + }, + }, + &StructValue{ + Fields: []Field{ + { + Value: &U8Value{Value: 0x01}, + }, + { + Value: &U16Value{Value: 0x4142}, + }, + }, + }, + ) + }) +} diff --git a/abi/codec_test.go b/abi/codec_test.go index 480f631..2780487 100644 --- a/abi/codec_test.go +++ b/abi/codec_test.go @@ -38,53 +38,6 @@ func TestCodec_EncodeNested(t *testing.T) { require.Equal(t, expected, hex.EncodeToString(encoded)) } - t.Run("struct", func(t *testing.T) { - fooStruct := StructValue{ - Fields: []Field{ - { - Value: U8Value{Value: 0x01}, - }, - { - Value: U16Value{Value: 0x4142}, - }, - }, - } - - doTest(t, fooStruct, "014142") - }) - - t.Run("enum (discriminant == 0)", func(t *testing.T) { - fooEnum := EnumValue{ - Discriminant: 0, - } - - doTest(t, fooEnum, "00") - }) - - t.Run("enum (discriminant != 0)", func(t *testing.T) { - fooEnum := EnumValue{ - Discriminant: 42, - } - - doTest(t, fooEnum, "2a") - }) - - t.Run("enum with Fields", func(t *testing.T) { - fooEnum := EnumValue{ - Discriminant: 42, - Fields: []Field{ - { - Value: U8Value{Value: 0x01}, - }, - { - Value: U16Value{Value: 0x4142}, - }, - }, - } - - doTest(t, fooEnum, "2a014142") - }) - t.Run("option with value", func(t *testing.T) { fooOption := OptionValue{ Value: U16Value{Value: 0x08}, @@ -129,59 +82,6 @@ func TestCodec_EncodeTopLevel(t *testing.T) { pubKeyLength: 32, }) - doTest := func(t *testing.T, value any, expected string) { - encoded, err := codec.EncodeTopLevel(value) - require.NoError(t, err) - require.Equal(t, expected, hex.EncodeToString(encoded)) - } - - t.Run("struct", func(t *testing.T) { - fooStruct := StructValue{ - Fields: []Field{ - { - Value: U8Value{Value: 0x01}, - }, - { - Value: U16Value{Value: 0x4142}, - }, - }, - } - - doTest(t, fooStruct, "014142") - }) - - t.Run("enum (discriminant == 0)", func(t *testing.T) { - fooEnum := EnumValue{ - Discriminant: 0, - } - - doTest(t, fooEnum, "") - }) - - t.Run("enum (discriminant != 0)", func(t *testing.T) { - fooEnum := EnumValue{ - Discriminant: 42, - } - - doTest(t, fooEnum, "2a") - }) - - t.Run("enum with Fields", func(t *testing.T) { - fooEnum := EnumValue{ - Discriminant: 42, - Fields: []Field{ - { - Value: U8Value{Value: 0x01}, - }, - { - Value: U16Value{Value: 0x4142}, - }, - }, - } - - doTest(t, fooEnum, "2a014142") - }) - t.Run("should err when unknown type", func(t *testing.T) { type dummy struct { foobar string @@ -229,85 +129,6 @@ func TestCodec_DecodeNested(t *testing.T) { require.ErrorContains(t, err, "cannot decode (nested) *abi.BigIntValue, because of: cannot read exactly 3 bytes") }) - t.Run("struct", func(t *testing.T) { - data, _ := hex.DecodeString("014142") - - destination := &StructValue{ - Fields: []Field{ - { - Value: &U8Value{}, - }, - { - Value: &U16Value{}, - }, - }, - } - - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &StructValue{ - Fields: []Field{ - { - Value: &U8Value{Value: 0x01}, - }, - { - Value: &U16Value{Value: 0x4142}, - }, - }, - }, destination) - }) - - t.Run("enum (discriminant == 0)", func(t *testing.T) { - data, _ := hex.DecodeString("00") - destination := &EnumValue{} - - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &EnumValue{ - Discriminant: 0x00, - }, destination) - }) - - t.Run("enum (discriminant != 0)", func(t *testing.T) { - data, _ := hex.DecodeString("01") - destination := &EnumValue{} - - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &EnumValue{ - Discriminant: 0x01, - }, destination) - }) - - t.Run("enum with Fields", func(t *testing.T) { - data, _ := hex.DecodeString("01014142") - - destination := &EnumValue{ - Fields: []Field{ - { - Value: &U8Value{}, - }, - { - Value: &U16Value{}, - }, - }, - } - - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &EnumValue{ - Discriminant: 0x01, - Fields: []Field{ - { - Value: &U8Value{Value: 0x01}, - }, - { - Value: &U16Value{Value: 0x4142}, - }, - }, - }, destination) - }) - t.Run("option with value", func(t *testing.T) { data, _ := hex.DecodeString("010008") @@ -429,85 +250,6 @@ func TestCodec_DecodeTopLevel(t *testing.T) { require.Equal(t, &BigIntValue{Value: big.NewInt(-1)}, destination) }) - t.Run("struct", func(t *testing.T) { - data, _ := hex.DecodeString("014142") - - destination := &StructValue{ - Fields: []Field{ - { - Value: &U8Value{}, - }, - { - Value: &U16Value{}, - }, - }, - } - - err := codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &StructValue{ - Fields: []Field{ - { - Value: &U8Value{Value: 0x01}, - }, - { - Value: &U16Value{Value: 0x4142}, - }, - }, - }, destination) - }) - - t.Run("enum (discriminant == 0)", func(t *testing.T) { - data, _ := hex.DecodeString("") - destination := &EnumValue{} - - err := codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &EnumValue{ - Discriminant: 0x00, - }, destination) - }) - - t.Run("enum (discriminant != 0)", func(t *testing.T) { - data, _ := hex.DecodeString("01") - destination := &EnumValue{} - - err := codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &EnumValue{ - Discriminant: 0x01, - }, destination) - }) - - t.Run("enum with Fields", func(t *testing.T) { - data, _ := hex.DecodeString("01014142") - - destination := &EnumValue{ - Fields: []Field{ - { - Value: &U8Value{}, - }, - { - Value: &U16Value{}, - }, - }, - } - - err := codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &EnumValue{ - Discriminant: 0x01, - Fields: []Field{ - { - Value: &U8Value{Value: 0x01}, - }, - { - Value: &U16Value{Value: 0x4142}, - }, - }, - }, destination) - }) - t.Run("should err when unknown type", func(t *testing.T) { type dummy struct { foobar string From e0df7731dfebc06477294a2792cca72c365e900b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Thu, 18 Apr 2024 15:44:08 +0300 Subject: [PATCH 14/24] Rename files. --- ...SimpleValues_enum_test.go => codecForCustomTypes_enum_test.go} | 0 ...leValues_struct_test.go => codecForCustomTypes_struct_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename abi/{codecForSimpleValues_enum_test.go => codecForCustomTypes_enum_test.go} (100%) rename abi/{codecForSimpleValues_struct_test.go => codecForCustomTypes_struct_test.go} (100%) diff --git a/abi/codecForSimpleValues_enum_test.go b/abi/codecForCustomTypes_enum_test.go similarity index 100% rename from abi/codecForSimpleValues_enum_test.go rename to abi/codecForCustomTypes_enum_test.go diff --git a/abi/codecForSimpleValues_struct_test.go b/abi/codecForCustomTypes_struct_test.go similarity index 100% rename from abi/codecForSimpleValues_struct_test.go rename to abi/codecForCustomTypes_struct_test.go From 3e90a3e9072bb56f237a139ace0a8b4b34b5b3ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Thu, 18 Apr 2024 16:32:47 +0300 Subject: [PATCH 15/24] Code & tests movement. --- ...ypes.go => codecForCompositeTypes_list.go} | 28 ------- abi/codecForCompositeTypes_list_test.go | 51 ++++++++++++ abi/codecForCompositeTypes_option.go | 33 ++++++++ abi/codecForCompositeTypes_option_test.go | 45 +++++++++++ abi/codec_test.go | 80 ------------------- 5 files changed, 129 insertions(+), 108 deletions(-) rename abi/{codecForCompositeTypes.go => codecForCompositeTypes_list.go} (61%) create mode 100644 abi/codecForCompositeTypes_list_test.go create mode 100644 abi/codecForCompositeTypes_option.go create mode 100644 abi/codecForCompositeTypes_option_test.go diff --git a/abi/codecForCompositeTypes.go b/abi/codecForCompositeTypes_list.go similarity index 61% rename from abi/codecForCompositeTypes.go rename to abi/codecForCompositeTypes_list.go index df1ca72..7d6b72a 100644 --- a/abi/codecForCompositeTypes.go +++ b/abi/codecForCompositeTypes_list.go @@ -5,34 +5,6 @@ import ( "io" ) -func (c *codec) encodeNestedOption(writer io.Writer, value OptionValue) error { - if value.Value == nil { - _, err := writer.Write([]byte{0}) - return err - } - - _, err := writer.Write([]byte{1}) - if err != nil { - return err - } - - return c.doEncodeNested(writer, value.Value) -} - -func (c *codec) decodeNestedOption(reader io.Reader, value *OptionValue) error { - bytes, err := readBytesExactly(reader, 1) - if err != nil { - return err - } - - if bytes[0] == 0 { - value.Value = nil - return nil - } - - return c.doDecodeNested(reader, value.Value) -} - func (c *codec) encodeNestedList(writer io.Writer, value InputListValue) error { err := encodeLength(writer, uint32(len(value.Items))) if err != nil { diff --git a/abi/codecForCompositeTypes_list_test.go b/abi/codecForCompositeTypes_list_test.go new file mode 100644 index 0000000..4ca8e59 --- /dev/null +++ b/abi/codecForCompositeTypes_list_test.go @@ -0,0 +1,51 @@ +package abi + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCodec_List(t *testing.T) { + codec, _ := newCodec(argsNewCodec{ + pubKeyLength: 32, + }) + + t.Run("should encode nested", func(t *testing.T) { + testEncodeNested(t, codec, + InputListValue{ + Items: []any{ + U16Value{Value: 1}, + U16Value{Value: 2}, + U16Value{Value: 3}, + }, + }, + "00000003000100020003", + ) + }) + + t.Run("should encode top-level", func(t *testing.T) { + }) + + t.Run("should decode nested", func(t *testing.T) { + data, _ := hex.DecodeString("00000003000100020003") + + destination := &OutputListValue{ + ItemCreator: func() any { return &U16Value{} }, + Items: []any{}, + } + + err := codec.DecodeNested(data, destination) + require.NoError(t, err) + require.Equal(t, + []any{ + &U16Value{Value: 1}, + &U16Value{Value: 2}, + &U16Value{Value: 3}, + }, destination.Items) + }) + + t.Run("should decode top-level", func(t *testing.T) { + }) +} diff --git a/abi/codecForCompositeTypes_option.go b/abi/codecForCompositeTypes_option.go new file mode 100644 index 0000000..3aec5c1 --- /dev/null +++ b/abi/codecForCompositeTypes_option.go @@ -0,0 +1,33 @@ +package abi + +import ( + "io" +) + +func (c *codec) encodeNestedOption(writer io.Writer, value OptionValue) error { + if value.Value == nil { + _, err := writer.Write([]byte{0}) + return err + } + + _, err := writer.Write([]byte{1}) + if err != nil { + return err + } + + return c.doEncodeNested(writer, value.Value) +} + +func (c *codec) decodeNestedOption(reader io.Reader, value *OptionValue) error { + bytes, err := readBytesExactly(reader, 1) + if err != nil { + return err + } + + if bytes[0] == 0 { + value.Value = nil + return nil + } + + return c.doDecodeNested(reader, value.Value) +} diff --git a/abi/codecForCompositeTypes_option_test.go b/abi/codecForCompositeTypes_option_test.go new file mode 100644 index 0000000..c17cff4 --- /dev/null +++ b/abi/codecForCompositeTypes_option_test.go @@ -0,0 +1,45 @@ +package abi + +import ( + "testing" +) + +func TestCodec_Option(t *testing.T) { + codec, _ := newCodec(argsNewCodec{ + pubKeyLength: 32, + }) + + t.Run("should encode nested", func(t *testing.T) { + testEncodeNested(t, codec, OptionValue{ + Value: nil, + }, "00") + + testEncodeNested(t, codec, OptionValue{ + Value: U16Value{Value: 0x08}, + }, "010008") + }) + + t.Run("should encode top-level", func(t *testing.T) { + }) + + t.Run("should decode nested", func(t *testing.T) { + testDecodeNested(t, codec, "00", + &OptionValue{}, + &OptionValue{ + Value: nil, + }, + ) + + testDecodeNested(t, codec, "010008", + &OptionValue{ + Value: &U16Value{}, + }, + &OptionValue{ + Value: &U16Value{Value: 0x08}, + }, + ) + }) + + t.Run("should decode top-level", func(t *testing.T) { + }) +} diff --git a/abi/codec_test.go b/abi/codec_test.go index 2780487..5abe586 100644 --- a/abi/codec_test.go +++ b/abi/codec_test.go @@ -32,40 +32,6 @@ func TestCodec_EncodeNested(t *testing.T) { pubKeyLength: 32, }) - doTest := func(t *testing.T, value any, expected string) { - encoded, err := codec.EncodeNested(value) - require.NoError(t, err) - require.Equal(t, expected, hex.EncodeToString(encoded)) - } - - t.Run("option with value", func(t *testing.T) { - fooOption := OptionValue{ - Value: U16Value{Value: 0x08}, - } - - doTest(t, fooOption, "010008") - }) - - t.Run("option without value", func(t *testing.T) { - fooOption := OptionValue{ - Value: nil, - } - - doTest(t, fooOption, "00") - }) - - t.Run("list", func(t *testing.T) { - fooList := InputListValue{ - Items: []any{ - U16Value{Value: 1}, - U16Value{Value: 2}, - U16Value{Value: 3}, - }, - } - - doTest(t, fooList, "00000003000100020003") - }) - t.Run("should err when unknown type", func(t *testing.T) { type dummy struct { foobar string @@ -129,52 +95,6 @@ func TestCodec_DecodeNested(t *testing.T) { require.ErrorContains(t, err, "cannot decode (nested) *abi.BigIntValue, because of: cannot read exactly 3 bytes") }) - t.Run("option with value", func(t *testing.T) { - data, _ := hex.DecodeString("010008") - - destination := &OptionValue{ - Value: &U16Value{}, - } - - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &OptionValue{ - Value: &U16Value{Value: 8}, - }, destination) - }) - - t.Run("option without value", func(t *testing.T) { - data, _ := hex.DecodeString("00") - - destination := &OptionValue{ - Value: &U16Value{}, - } - - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, &OptionValue{ - Value: nil, - }, destination) - }) - - t.Run("list", func(t *testing.T) { - data, _ := hex.DecodeString("00000003000100020003") - - destination := &OutputListValue{ - ItemCreator: func() any { return &U16Value{} }, - Items: []any{}, - } - - err := codec.DecodeNested(data, destination) - require.NoError(t, err) - require.Equal(t, - []any{ - &U16Value{Value: 1}, - &U16Value{Value: 2}, - &U16Value{Value: 3}, - }, destination.Items) - }) - t.Run("should err when unknown type", func(t *testing.T) { type dummy struct { foobar string From 2949559c382a3fd7d7e93538cacab7d9a675d1cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Fri, 19 Apr 2024 18:08:10 +0300 Subject: [PATCH 16/24] Some code movement. --- abi/codecForSimpleValues_boolean.go | 20 ++++++++++---------- abi/codecForSimpleValues_numerical.go | 26 +++++++++++++------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/abi/codecForSimpleValues_boolean.go b/abi/codecForSimpleValues_boolean.go index 98cab65..d70fcd3 100644 --- a/abi/codecForSimpleValues_boolean.go +++ b/abi/codecForSimpleValues_boolean.go @@ -15,6 +15,16 @@ func (c *codec) encodeNestedBool(writer io.Writer, value BoolValue) error { return err } +func (c *codec) encodeTopLevelBool(writer io.Writer, value BoolValue) error { + if !value.Value { + // For "false", write nothing. + return nil + } + + _, err := writer.Write([]byte{trueAsByte}) + return err +} + func (c *codec) decodeNestedBool(reader io.Reader, value *BoolValue) error { data, err := readBytesExactly(reader, 1) if err != nil { @@ -29,16 +39,6 @@ func (c *codec) decodeNestedBool(reader io.Reader, value *BoolValue) error { return nil } -func (c *codec) encodeTopLevelBool(writer io.Writer, value BoolValue) error { - if !value.Value { - // For "false", write nothing. - return nil - } - - _, err := writer.Write([]byte{trueAsByte}) - return err -} - func (c *codec) decodeTopLevelBool(data []byte, value *BoolValue) error { if len(data) == 0 { value.Value = false diff --git a/abi/codecForSimpleValues_numerical.go b/abi/codecForSimpleValues_numerical.go index 0a0b38d..2c1dd9f 100644 --- a/abi/codecForSimpleValues_numerical.go +++ b/abi/codecForSimpleValues_numerical.go @@ -31,6 +31,19 @@ func (c *codec) encodeNestedNumber(writer io.Writer, value any, numBytes int) er return nil } +func (c *codec) encodeTopLevelUnsignedNumber(writer io.Writer, value uint64) error { + b := big.NewInt(0).SetUint64(value) + data := b.Bytes() + _, err := writer.Write(data) + return err +} + +func (c *codec) encodeTopLevelSignedNumber(writer io.Writer, value int64) error { + data := twos.ToBytes(big.NewInt(value)) + _, err := writer.Write(data) + return err +} + func (c *codec) decodeNestedNumber(reader io.Reader, value any, numBytes int) error { data, err := readBytesExactly(reader, numBytes) if err != nil { @@ -46,19 +59,6 @@ func (c *codec) decodeNestedNumber(reader io.Reader, value any, numBytes int) er return nil } -func (c *codec) encodeTopLevelUnsignedNumber(writer io.Writer, value uint64) error { - b := big.NewInt(0).SetUint64(value) - data := b.Bytes() - _, err := writer.Write(data) - return err -} - -func (c *codec) encodeTopLevelSignedNumber(writer io.Writer, value int64) error { - data := twos.ToBytes(big.NewInt(value)) - _, err := writer.Write(data) - return err -} - func (c *codec) decodeTopLevelUnsignedNumber(data []byte, maxValue uint64) (uint64, error) { b := big.NewInt(0).SetBytes(data) if !b.IsUint64() { From 12bf1baa82603c0177b5643b4b45b228ae327561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Fri, 19 Apr 2024 19:28:57 +0300 Subject: [PATCH 17/24] Contiued implementation for "options". --- abi/codec.go | 4 +++ abi/codecForCompositeTypes_option.go | 32 +++++++++++++++++++++ abi/codecForCompositeTypes_option_test.go | 35 +++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/abi/codec.go b/abi/codec.go index b74f1d5..340190d 100644 --- a/abi/codec.go +++ b/abi/codec.go @@ -128,6 +128,8 @@ func (c *codec) doEncodeTopLevel(writer io.Writer, value any) error { return c.encodeTopLevelStruct(writer, value) case EnumValue: return c.encodeTopLevelEnum(writer, value) + case OptionValue: + return c.encodeTopLevelOption(writer, value) default: return fmt.Errorf("unsupported type for top-level encoding: %T", value) } @@ -286,6 +288,8 @@ func (c *codec) doDecodeTopLevel(data []byte, value any) error { return c.decodeTopLevelStruct(data, value) case *EnumValue: return c.decodeTopLevelEnum(data, value) + case *OptionValue: + return c.decodeTopLevelOption(data, value) default: return fmt.Errorf("unsupported type for top-level decoding: %T", value) } diff --git a/abi/codecForCompositeTypes_option.go b/abi/codecForCompositeTypes_option.go index 3aec5c1..103a4a2 100644 --- a/abi/codecForCompositeTypes_option.go +++ b/abi/codecForCompositeTypes_option.go @@ -1,6 +1,8 @@ package abi import ( + "bytes" + "fmt" "io" ) @@ -18,6 +20,19 @@ func (c *codec) encodeNestedOption(writer io.Writer, value OptionValue) error { return c.doEncodeNested(writer, value.Value) } +func (c *codec) encodeTopLevelOption(writer io.Writer, value OptionValue) error { + if value.Value == nil { + return nil + } + + _, err := writer.Write([]byte{1}) + if err != nil { + return err + } + + return c.doEncodeNested(writer, value.Value) +} + func (c *codec) decodeNestedOption(reader io.Reader, value *OptionValue) error { bytes, err := readBytesExactly(reader, 1) if err != nil { @@ -31,3 +46,20 @@ func (c *codec) decodeNestedOption(reader io.Reader, value *OptionValue) error { return c.doDecodeNested(reader, value.Value) } + +func (c *codec) decodeTopLevelOption(data []byte, value *OptionValue) error { + if len(data) == 0 { + value.Value = nil + return nil + } + + firstByte := data[0] + dataAfterFirstByte := data[1:] + + if firstByte != 0x01 { + return fmt.Errorf("invalid first byte for top-level encoded option: %d", firstByte) + } + + reader := bytes.NewReader(dataAfterFirstByte) + return c.doDecodeNested(reader, value.Value) +} diff --git a/abi/codecForCompositeTypes_option_test.go b/abi/codecForCompositeTypes_option_test.go index c17cff4..922acc4 100644 --- a/abi/codecForCompositeTypes_option_test.go +++ b/abi/codecForCompositeTypes_option_test.go @@ -1,7 +1,10 @@ package abi import ( + "encoding/hex" "testing" + + "github.com/stretchr/testify/require" ) func TestCodec_Option(t *testing.T) { @@ -20,6 +23,13 @@ func TestCodec_Option(t *testing.T) { }) t.Run("should encode top-level", func(t *testing.T) { + testEncodeTopLevel(t, codec, OptionValue{ + Value: nil, + }, "") + + testEncodeTopLevel(t, codec, OptionValue{ + Value: U16Value{Value: 0x08}, + }, "010008") }) t.Run("should decode nested", func(t *testing.T) { @@ -41,5 +51,30 @@ func TestCodec_Option(t *testing.T) { }) t.Run("should decode top-level", func(t *testing.T) { + testDecodeTopLevel(t, codec, "", + &OptionValue{}, + &OptionValue{ + Value: nil, + }, + ) + + testDecodeTopLevel(t, codec, "010008", + &OptionValue{ + Value: &U16Value{}, + }, + &OptionValue{ + Value: &U16Value{Value: 0x08}, + }, + ) + }) + + t.Run("should err on decode top-level (bad marker for value presence)", func(t *testing.T) { + data, _ := hex.DecodeString("002a") + err := codec.DecodeTopLevel(data, &OptionValue{}) + require.ErrorContains(t, err, "invalid first byte for top-level encoded option: 0") + + data, _ = hex.DecodeString("072a") + err = codec.DecodeTopLevel(data, &OptionValue{}) + require.ErrorContains(t, err, "invalid first byte for top-level encoded option: 7") }) } From de3eb5aed7299e13b5698854d0c25b4494dfccba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Fri, 19 Apr 2024 19:55:53 +0300 Subject: [PATCH 18/24] Lists - top-level encode / decode. --- abi/codecForCompositeTypes_list.go | 57 ++++++++++++++++++++----- abi/codecForCompositeTypes_list_test.go | 31 +++++++++++++- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/abi/codecForCompositeTypes_list.go b/abi/codecForCompositeTypes_list.go index 7d6b72a..86a894b 100644 --- a/abi/codecForCompositeTypes_list.go +++ b/abi/codecForCompositeTypes_list.go @@ -1,6 +1,7 @@ package abi import ( + "bytes" "errors" "io" ) @@ -11,19 +12,16 @@ func (c *codec) encodeNestedList(writer io.Writer, value InputListValue) error { return err } - for _, item := range value.Items { - err := c.doEncodeNested(writer, item) - if err != nil { - return err - } - } + return c.encodeListItems(writer, value) +} - return nil +func (c *codec) encodeTopLevelList(writer io.Writer, value InputListValue) error { + return c.encodeListItems(writer, value) } func (c *codec) decodeNestedList(reader io.Reader, value *OutputListValue) error { if value.ItemCreator == nil { - return errors.New("cannot deserialize list: item creator is nil") + return errors.New("cannot decode list: item creator is nil") } length, err := decodeLength(reader) @@ -34,15 +32,52 @@ func (c *codec) decodeNestedList(reader io.Reader, value *OutputListValue) error value.Items = make([]any, 0, length) for i := uint32(0); i < length; i++ { - newItem := value.ItemCreator() + err := c.decodeListItem(reader, value) + if err != nil { + return err + } + } + + return nil +} + +func (c *codec) decodeTopLevelList(data []byte, value *OutputListValue) error { + if value.ItemCreator == nil { + return errors.New("cannot decode list: item creator is nil") + } - err := c.doDecodeNested(reader, newItem) + reader := bytes.NewReader(data) + value.Items = make([]any, 0) + + for reader.Len() > 0 { + err := c.decodeListItem(reader, value) + if err != nil { + return err + } + } + + return nil +} + +func (c *codec) encodeListItems(writer io.Writer, value InputListValue) error { + for _, item := range value.Items { + err := c.doEncodeNested(writer, item) if err != nil { return err } + } - value.Items = append(value.Items, newItem) + return nil +} + +func (c *codec) decodeListItem(reader io.Reader, value *OutputListValue) error { + newItem := value.ItemCreator() + + err := c.doDecodeNested(reader, newItem) + if err != nil { + return err } + value.Items = append(value.Items, newItem) return nil } diff --git a/abi/codecForCompositeTypes_list_test.go b/abi/codecForCompositeTypes_list_test.go index 4ca8e59..70983fb 100644 --- a/abi/codecForCompositeTypes_list_test.go +++ b/abi/codecForCompositeTypes_list_test.go @@ -26,6 +26,16 @@ func TestCodec_List(t *testing.T) { }) t.Run("should encode top-level", func(t *testing.T) { + testEncodeTopLevel(t, codec, + InputListValue{ + Items: []any{ + U16Value{Value: 1}, + U16Value{Value: 2}, + U16Value{Value: 3}, + }, + }, + "000100020003", + ) }) t.Run("should decode nested", func(t *testing.T) { @@ -43,9 +53,28 @@ func TestCodec_List(t *testing.T) { &U16Value{Value: 1}, &U16Value{Value: 2}, &U16Value{Value: 3}, - }, destination.Items) + }, + destination.Items, + ) }) t.Run("should decode top-level", func(t *testing.T) { + data, _ := hex.DecodeString("000100020003") + + destination := &OutputListValue{ + ItemCreator: func() any { return &U16Value{} }, + Items: []any{}, + } + + err := codec.DecodeTopLevel(data, destination) + require.NoError(t, err) + require.Equal(t, + []any{ + &U16Value{Value: 1}, + &U16Value{Value: 2}, + &U16Value{Value: 3}, + }, + destination.Items, + ) }) } From 9387e27af86ff8c4b123f53670a9cf3052703a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Fri, 19 Apr 2024 19:56:13 +0300 Subject: [PATCH 19/24] Handle lists. --- abi/codec.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/abi/codec.go b/abi/codec.go index 340190d..d155bb1 100644 --- a/abi/codec.go +++ b/abi/codec.go @@ -130,6 +130,8 @@ func (c *codec) doEncodeTopLevel(writer io.Writer, value any) error { return c.encodeTopLevelEnum(writer, value) case OptionValue: return c.encodeTopLevelOption(writer, value) + case InputListValue: + return c.encodeTopLevelList(writer, value) default: return fmt.Errorf("unsupported type for top-level encoding: %T", value) } @@ -290,6 +292,8 @@ func (c *codec) doDecodeTopLevel(data []byte, value any) error { return c.decodeTopLevelEnum(data, value) case *OptionValue: return c.decodeTopLevelOption(data, value) + case *OutputListValue: + return c.decodeTopLevelList(data, value) default: return fmt.Errorf("unsupported type for top-level decoding: %T", value) } From e36f1746cab4c3ae91bdb7963169b6b5acb0a05d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 24 Apr 2024 15:42:12 +0300 Subject: [PATCH 20/24] Tests movement etc. --- ...codecForSimpleValues_numerical_big_test.go | 15 +++ abi/codecForSimpleValues_numerical_test.go | 6 + abi/codec_test.go | 106 +++--------------- 3 files changed, 35 insertions(+), 92 deletions(-) diff --git a/abi/codecForSimpleValues_numerical_big_test.go b/abi/codecForSimpleValues_numerical_big_test.go index 46467c7..526d899 100644 --- a/abi/codecForSimpleValues_numerical_big_test.go +++ b/abi/codecForSimpleValues_numerical_big_test.go @@ -60,6 +60,10 @@ func TestCodec_NumericalBig(t *testing.T) { testDecodeNested(t, codec, "000000020100", &BigIntValue{}, &BigIntValue{Value: big.NewInt(256)}) }) + t.Run("should err on decode nested", func(t *testing.T) { + testDecodeNestedWithError(t, codec, "0000000301", &BigIntValue{}, "cannot decode (nested) *abi.BigIntValue, because of: cannot read exactly 3 bytes") + }) + t.Run("should decode top-level", func(t *testing.T) { testDecodeTopLevel(t, codec, "", &BigUIntValue{}, &BigUIntValue{Value: big.NewInt(0)}) testDecodeTopLevel(t, codec, "01", &BigUIntValue{}, &BigUIntValue{Value: big.NewInt(1)}) @@ -75,4 +79,15 @@ func TestCodec_NumericalBig(t *testing.T) { testDecodeTopLevel(t, codec, "00ff", &BigIntValue{}, &BigIntValue{Value: big.NewInt(255)}) testDecodeTopLevel(t, codec, "0100", &BigIntValue{}, &BigIntValue{Value: big.NewInt(256)}) }) + + t.Run("should err on decode top-level", func(t *testing.T) { + testDecodeTopLevelWithError(t, codec, "4142", &U8Value{}, "decoded value is too large") + testDecodeTopLevelWithError(t, codec, "4142", &I8Value{}, "decoded value is too large") + testDecodeTopLevelWithError(t, codec, "414243", &U16Value{}, "decoded value is too large") + testDecodeTopLevelWithError(t, codec, "414243", &I16Value{}, "decoded value is too large") + testDecodeTopLevelWithError(t, codec, "4142434445", &U32Value{}, "decoded value is too large") + testDecodeTopLevelWithError(t, codec, "4142434445", &I32Value{}, "decoded value is too large") + testDecodeTopLevelWithError(t, codec, "41424344454647489876", &U64Value{}, "decoded value is too large") + testDecodeTopLevelWithError(t, codec, "41424344454647489876", &I64Value{}, "decoded value is too large") + }) } diff --git a/abi/codecForSimpleValues_numerical_test.go b/abi/codecForSimpleValues_numerical_test.go index 203d192..64b3357 100644 --- a/abi/codecForSimpleValues_numerical_test.go +++ b/abi/codecForSimpleValues_numerical_test.go @@ -108,6 +108,12 @@ func TestCodec_Numerical(t *testing.T) { testDecodeNested(t, codec, "8000000000000000", &I64Value{}, &I64Value{Value: -9223372036854775808}) }) + t.Run("should err on decode nested", func(t *testing.T) { + testDecodeNestedWithError(t, codec, "01", &U16Value{}, "cannot read exactly 2 bytes") + testDecodeNestedWithError(t, codec, "4142", &U32Value{}, "cannot read exactly 4 bytes") + testDecodeNestedWithError(t, codec, "41424344", &U64Value{}, "cannot read exactly 8 bytes") + }) + t.Run("should decode top-level", func(t *testing.T) { testDecodeTopLevel(t, codec, "", &U8Value{}, &U8Value{Value: 0}) testDecodeNested(t, codec, "01", &U8Value{}, &U8Value{Value: 1}) diff --git a/abi/codec_test.go b/abi/codec_test.go index 5abe586..df6431c 100644 --- a/abi/codec_test.go +++ b/abi/codec_test.go @@ -2,7 +2,6 @@ package abi import ( "encoding/hex" - "math/big" "testing" "github.com/stretchr/testify/require" @@ -64,37 +63,6 @@ func TestCodec_DecodeNested(t *testing.T) { pubKeyLength: 32, }) - t.Run("u16, should err because it cannot read 2 bytes", func(t *testing.T) { - data, _ := hex.DecodeString("01") - destination := &U16Value{} - - err := codec.DecodeNested(data, destination) - require.ErrorContains(t, err, "cannot read exactly 2 bytes") - }) - - t.Run("u32, should err because it cannot read 4 bytes", func(t *testing.T) { - data, _ := hex.DecodeString("4142") - destination := &U32Value{} - - err := codec.DecodeNested(data, destination) - require.ErrorContains(t, err, "cannot read exactly 4 bytes") - }) - - t.Run("u64, should err because it cannot read 8 bytes", func(t *testing.T) { - data, _ := hex.DecodeString("41424344") - destination := &U64Value{} - - err := codec.DecodeNested(data, destination) - require.ErrorContains(t, err, "cannot read exactly 8 bytes") - }) - - t.Run("bigInt: should err when bad data", func(t *testing.T) { - data, _ := hex.DecodeString("0000000301") - destination := &BigIntValue{} - err := codec.DecodeNested(data, destination) - require.ErrorContains(t, err, "cannot decode (nested) *abi.BigIntValue, because of: cannot read exactly 3 bytes") - }) - t.Run("should err when unknown type", func(t *testing.T) { type dummy struct { foobar string @@ -110,66 +78,6 @@ func TestCodec_DecodeTopLevel(t *testing.T) { pubKeyLength: 32, }) - t.Run("u8, i8: should err because decoded value is too large", func(t *testing.T) { - data, _ := hex.DecodeString("4142") - - err := codec.DecodeTopLevel(data, &U8Value{}) - require.ErrorContains(t, err, "decoded value is too large") - - err = codec.DecodeTopLevel(data, &I8Value{}) - require.ErrorContains(t, err, "decoded value is too large") - }) - - t.Run("u16, i16: should err because decoded value is too large", func(t *testing.T) { - data, _ := hex.DecodeString("41424344") - - err := codec.DecodeTopLevel(data, &U16Value{}) - require.ErrorContains(t, err, "decoded value is too large") - - err = codec.DecodeTopLevel(data, &I16Value{}) - require.ErrorContains(t, err, "decoded value is too large") - }) - - t.Run("u32, i32: should err because decoded value is too large", func(t *testing.T) { - data, _ := hex.DecodeString("4142434445464748") - - err := codec.DecodeTopLevel(data, &U32Value{}) - require.ErrorContains(t, err, "decoded value is too large") - - err = codec.DecodeTopLevel(data, &I32Value{}) - require.ErrorContains(t, err, "decoded value is too large") - }) - - t.Run("u64, i64: should err because decoded value is too large", func(t *testing.T) { - data, _ := hex.DecodeString("41424344454647489876") - - err := codec.DecodeTopLevel(data, &U64Value{}) - require.ErrorContains(t, err, "decoded value is too large") - - err = codec.DecodeTopLevel(data, &I64Value{}) - require.ErrorContains(t, err, "decoded value is too large") - }) - - t.Run("bigInt", func(t *testing.T) { - data, _ := hex.DecodeString("") - destination := &BigIntValue{} - err := codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &BigIntValue{Value: big.NewInt(0)}, destination) - - data, _ = hex.DecodeString("01") - destination = &BigIntValue{} - err = codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &BigIntValue{Value: big.NewInt(1)}, destination) - - data, _ = hex.DecodeString("ff") - destination = &BigIntValue{} - err = codec.DecodeTopLevel(data, destination) - require.NoError(t, err) - require.Equal(t, &BigIntValue{Value: big.NewInt(-1)}, destination) - }) - t.Run("should err when unknown type", func(t *testing.T) { type dummy struct { foobar string @@ -202,6 +110,13 @@ func testDecodeNested(t *testing.T, codec *codec, encodedData string, destinatio require.Equal(t, expected, destination) } +func testDecodeNestedWithError(t *testing.T, codec *codec, encodedData string, destination any, expectedError string) { + data, _ := hex.DecodeString(encodedData) + err := codec.DecodeNested(data, destination) + + require.ErrorContains(t, err, expectedError) +} + func testDecodeTopLevel(t *testing.T, codec *codec, encodedData string, destination any, expected any) { data, _ := hex.DecodeString(encodedData) err := codec.DecodeTopLevel(data, destination) @@ -209,3 +124,10 @@ func testDecodeTopLevel(t *testing.T, codec *codec, encodedData string, destinat require.NoError(t, err) require.Equal(t, expected, destination) } + +func testDecodeTopLevelWithError(t *testing.T, codec *codec, encodedData string, destination any, expectedError string) { + data, _ := hex.DecodeString(encodedData) + err := codec.DecodeTopLevel(data, destination) + + require.ErrorContains(t, err, expectedError) +} From 22d42d0658034654822911fffd8c7052c9721235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 24 Apr 2024 15:50:17 +0300 Subject: [PATCH 21/24] Tests refactoring. --- abi/codec_test.go | 68 +++++++++++++---------------------------------- 1 file changed, 18 insertions(+), 50 deletions(-) diff --git a/abi/codec_test.go b/abi/codec_test.go index df6431c..cfe3f4f 100644 --- a/abi/codec_test.go +++ b/abi/codec_test.go @@ -7,8 +7,8 @@ import ( "github.com/stretchr/testify/require" ) -func TestNewCodec(t *testing.T) { - t.Run("should work", func(t *testing.T) { +func TestCodec(t *testing.T) { + t.Run("should create new codec", func(t *testing.T) { codec, err := newCodec(argsNewCodec{ pubKeyLength: 32, }) @@ -17,77 +17,40 @@ func TestNewCodec(t *testing.T) { require.NotNil(t, codec) }) - t.Run("should err if bad public key length", func(t *testing.T) { + t.Run("should err on creating new codec when public key length is bad", func(t *testing.T) { _, err := newCodec(argsNewCodec{ pubKeyLength: 0, }) require.ErrorContains(t, err, "bad public key length") }) -} -func TestCodec_EncodeNested(t *testing.T) { - codec, _ := newCodec(argsNewCodec{ - pubKeyLength: 32, - }) + t.Run("should err when encoding or decoding an unknown type", func(t *testing.T) { + codec, _ := newCodec(argsNewCodec{ + pubKeyLength: 32, + }) - t.Run("should err when unknown type", func(t *testing.T) { - type dummy struct { + type dummyType struct { foobar string } - encoded, err := codec.EncodeNested(&dummy{foobar: "hello"}) + encoded, err := codec.EncodeNested(&dummyType{foobar: "hello"}) require.ErrorContains(t, err, "unsupported type for nested encoding: *abi.dummy") require.Nil(t, encoded) - }) -} -func TestCodec_EncodeTopLevel(t *testing.T) { - codec, _ := newCodec(argsNewCodec{ - pubKeyLength: 32, - }) - - t.Run("should err when unknown type", func(t *testing.T) { - type dummy struct { - foobar string - } - - encoded, err := codec.EncodeTopLevel(&dummy{foobar: "hello"}) + encoded, err = codec.EncodeTopLevel(&dummyType{foobar: "hello"}) require.ErrorContains(t, err, "unsupported type for top-level encoding: *abi.dummy") require.Nil(t, encoded) - }) -} -func TestCodec_DecodeNested(t *testing.T) { - codec, _ := newCodec(argsNewCodec{ - pubKeyLength: 32, - }) - - t.Run("should err when unknown type", func(t *testing.T) { - type dummy struct { - foobar string - } - - err := codec.DecodeNested([]byte{0x00}, &dummy{foobar: "hello"}) + err = codec.DecodeNested([]byte{0x00}, &dummyType{foobar: "hello"}) require.ErrorContains(t, err, "unsupported type for nested decoding: *abi.dummy") - }) -} - -func TestCodec_DecodeTopLevel(t *testing.T) { - codec, _ := newCodec(argsNewCodec{ - pubKeyLength: 32, - }) - - t.Run("should err when unknown type", func(t *testing.T) { - type dummy struct { - foobar string - } - err := codec.DecodeTopLevel([]byte{0x00}, &dummy{foobar: "hello"}) + err = codec.DecodeTopLevel([]byte{0x00}, &dummyType{foobar: "hello"}) require.ErrorContains(t, err, "unsupported type for top-level decoding: *abi.dummy") }) } +// testEncodeNested is a helper function to test nested encoding. func testEncodeNested(t *testing.T, codec *codec, value any, expected string) { encoded, err := codec.EncodeNested(value) @@ -95,6 +58,7 @@ func testEncodeNested(t *testing.T, codec *codec, value any, expected string) { require.Equal(t, expected, hex.EncodeToString(encoded)) } +// testEncodeTopLevel is a helper function to test top-level encoding. func testEncodeTopLevel(t *testing.T, codec *codec, value any, expected string) { encoded, err := codec.EncodeTopLevel(value) @@ -102,6 +66,7 @@ func testEncodeTopLevel(t *testing.T, codec *codec, value any, expected string) require.Equal(t, expected, hex.EncodeToString(encoded)) } +// testDecodeNested is a helper function to test nested decoding. func testDecodeNested(t *testing.T, codec *codec, encodedData string, destination any, expected any) { data, _ := hex.DecodeString(encodedData) err := codec.DecodeNested(data, destination) @@ -110,6 +75,7 @@ func testDecodeNested(t *testing.T, codec *codec, encodedData string, destinatio require.Equal(t, expected, destination) } +// testDecodeNestedWithError is a helper function to test nested decoding. func testDecodeNestedWithError(t *testing.T, codec *codec, encodedData string, destination any, expectedError string) { data, _ := hex.DecodeString(encodedData) err := codec.DecodeNested(data, destination) @@ -117,6 +83,7 @@ func testDecodeNestedWithError(t *testing.T, codec *codec, encodedData string, d require.ErrorContains(t, err, expectedError) } +// testDecodeTopLevel is a helper function to test top-level decoding. func testDecodeTopLevel(t *testing.T, codec *codec, encodedData string, destination any, expected any) { data, _ := hex.DecodeString(encodedData) err := codec.DecodeTopLevel(data, destination) @@ -125,6 +92,7 @@ func testDecodeTopLevel(t *testing.T, codec *codec, encodedData string, destinat require.Equal(t, expected, destination) } +// testDecodeTopLevelWithError is a helper function to test top-level decoding. func testDecodeTopLevelWithError(t *testing.T, codec *codec, encodedData string, destination any, expectedError string) { data, _ := hex.DecodeString(encodedData) err := codec.DecodeTopLevel(data, destination) From 4244d9018a5e1a4213a32de2e16d3b0c78a7e24b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Mon, 29 Apr 2024 18:33:07 +0300 Subject: [PATCH 22/24] Fix after review (part 1). --- abi/codec.go | 16 +- abi/codecForCompositeTypes_option.go | 20 +- abi/codecForCompositeTypes_option_test.go | 16 +- abi/codecForSimpleValues_address.go | 8 +- abi/codecForSimpleValues_numerical.go | 4 +- abi/codecForSimpleValues_numerical_big.go | 69 +++-- ...codecForSimpleValues_numerical_big_test.go | 11 - abi/constants.go | 2 + abi/parts.go | 2 +- abi/serializer_test.go | 240 +++++++++++++++++- abi/shared.go | 2 +- abi/valuesSingle.go | 4 +- 12 files changed, 329 insertions(+), 65 deletions(-) diff --git a/abi/codec.go b/abi/codec.go index d155bb1..f619cd2 100644 --- a/abi/codec.go +++ b/abi/codec.go @@ -61,9 +61,9 @@ func (c *codec) doEncodeNested(writer io.Writer, value any) error { case I64Value: return c.encodeNestedNumber(writer, value.Value, 8) case BigUIntValue: - return c.encodeNestedBigNumber(writer, value.Value, false) + return c.encodeNestedUnsignedBigNumber(writer, value.Value) case BigIntValue: - return c.encodeNestedBigNumber(writer, value.Value, true) + return c.encodeNestedSignedBigNumber(writer, value.Value) case AddressValue: return c.encodeNestedAddress(writer, value) case StringValue: @@ -115,9 +115,9 @@ func (c *codec) doEncodeTopLevel(writer io.Writer, value any) error { case I64Value: return c.encodeTopLevelSignedNumber(writer, value.Value) case BigUIntValue: - return c.encodeTopLevelBigNumber(writer, value.Value, false) + return c.encodeTopLevelUnsignedBigNumber(writer, value.Value) case BigIntValue: - return c.encodeTopLevelBigNumber(writer, value.Value, true) + return c.encodeTopLevelSignedBigNumber(writer, value.Value) case AddressValue: return c.encodeTopLevelAddress(writer, value) case StringValue: @@ -169,7 +169,7 @@ func (c *codec) doDecodeNested(reader io.Reader, value any) error { case *I64Value: return c.decodeNestedNumber(reader, &value.Value, 8) case *BigUIntValue: - n, err := c.decodeNestedBigNumber(reader, false) + n, err := c.decodeNestedUnsignedBigNumber(reader) if err != nil { return err } @@ -177,7 +177,7 @@ func (c *codec) doDecodeNested(reader io.Reader, value any) error { value.Value = n return nil case *BigIntValue: - n, err := c.decodeNestedBigNumber(reader, true) + n, err := c.decodeNestedSignedBigNumber(reader) if err != nil { return err } @@ -275,10 +275,10 @@ func (c *codec) doDecodeTopLevel(data []byte, value any) error { value.Value = int64(n) case *BigUIntValue: - n := c.decodeTopLevelBigNumber(data, false) + n := c.decodeTopLevelUnsignedBigNumber(data) value.Value = n case *BigIntValue: - n := c.decodeTopLevelBigNumber(data, true) + n := c.decodeTopLevelSignedBigNumber(data) value.Value = n case *AddressValue: return c.decodeTopLevelAddress(data, value) diff --git a/abi/codecForCompositeTypes_option.go b/abi/codecForCompositeTypes_option.go index 103a4a2..8babb6e 100644 --- a/abi/codecForCompositeTypes_option.go +++ b/abi/codecForCompositeTypes_option.go @@ -8,11 +8,11 @@ import ( func (c *codec) encodeNestedOption(writer io.Writer, value OptionValue) error { if value.Value == nil { - _, err := writer.Write([]byte{0}) + _, err := writer.Write([]byte{optionMarkerForAbsentValue}) return err } - _, err := writer.Write([]byte{1}) + _, err := writer.Write([]byte{optionMarkerForPresentValue}) if err != nil { return err } @@ -25,7 +25,7 @@ func (c *codec) encodeTopLevelOption(writer io.Writer, value OptionValue) error return nil } - _, err := writer.Write([]byte{1}) + _, err := writer.Write([]byte{optionMarkerForPresentValue}) if err != nil { return err } @@ -34,17 +34,23 @@ func (c *codec) encodeTopLevelOption(writer io.Writer, value OptionValue) error } func (c *codec) decodeNestedOption(reader io.Reader, value *OptionValue) error { - bytes, err := readBytesExactly(reader, 1) + data, err := readBytesExactly(reader, 1) if err != nil { return err } - if bytes[0] == 0 { + firstByte := data[0] + + if firstByte == optionMarkerForAbsentValue { value.Value = nil return nil } - return c.doDecodeNested(reader, value.Value) + if firstByte == optionMarkerForPresentValue { + return c.doDecodeNested(reader, value.Value) + } + + return fmt.Errorf("invalid first byte for nested encoded option: %d", firstByte) } func (c *codec) decodeTopLevelOption(data []byte, value *OptionValue) error { @@ -56,7 +62,7 @@ func (c *codec) decodeTopLevelOption(data []byte, value *OptionValue) error { firstByte := data[0] dataAfterFirstByte := data[1:] - if firstByte != 0x01 { + if firstByte != optionMarkerForPresentValue { return fmt.Errorf("invalid first byte for top-level encoded option: %d", firstByte) } diff --git a/abi/codecForCompositeTypes_option_test.go b/abi/codecForCompositeTypes_option_test.go index 922acc4..029a733 100644 --- a/abi/codecForCompositeTypes_option_test.go +++ b/abi/codecForCompositeTypes_option_test.go @@ -1,10 +1,7 @@ package abi import ( - "encoding/hex" "testing" - - "github.com/stretchr/testify/require" ) func TestCodec_Option(t *testing.T) { @@ -50,6 +47,10 @@ func TestCodec_Option(t *testing.T) { ) }) + t.Run("should err on decode nested (bad marker for value presence)", func(t *testing.T) { + testDecodeNestedWithError(t, codec, "072a", &OptionValue{}, "invalid first byte for nested encoded option: 7") + }) + t.Run("should decode top-level", func(t *testing.T) { testDecodeTopLevel(t, codec, "", &OptionValue{}, @@ -69,12 +70,7 @@ func TestCodec_Option(t *testing.T) { }) t.Run("should err on decode top-level (bad marker for value presence)", func(t *testing.T) { - data, _ := hex.DecodeString("002a") - err := codec.DecodeTopLevel(data, &OptionValue{}) - require.ErrorContains(t, err, "invalid first byte for top-level encoded option: 0") - - data, _ = hex.DecodeString("072a") - err = codec.DecodeTopLevel(data, &OptionValue{}) - require.ErrorContains(t, err, "invalid first byte for top-level encoded option: 7") + testDecodeTopLevelWithError(t, codec, "002a", &OptionValue{}, "invalid first byte for top-level encoded option: 0") + testDecodeTopLevelWithError(t, codec, "072a", &OptionValue{}, "invalid first byte for top-level encoded option: 7") }) } diff --git a/abi/codecForSimpleValues_address.go b/abi/codecForSimpleValues_address.go index 3600259..83f54a7 100644 --- a/abi/codecForSimpleValues_address.go +++ b/abi/codecForSimpleValues_address.go @@ -6,10 +6,6 @@ import ( ) func (c *codec) encodeNestedAddress(writer io.Writer, value AddressValue) error { - return c.encodeTopLevelAddress(writer, value) -} - -func (c *codec) encodeTopLevelAddress(writer io.Writer, value AddressValue) error { err := c.checkPubKeyLength(value.Value) if err != nil { return err @@ -19,6 +15,10 @@ func (c *codec) encodeTopLevelAddress(writer io.Writer, value AddressValue) erro return err } +func (c *codec) encodeTopLevelAddress(writer io.Writer, value AddressValue) error { + return c.encodeNestedAddress(writer, value) +} + func (c *codec) decodeNestedAddress(reader io.Reader, value *AddressValue) error { data, err := readBytesExactly(reader, c.pubKeyLength) if err != nil { diff --git a/abi/codecForSimpleValues_numerical.go b/abi/codecForSimpleValues_numerical.go index 2c1dd9f..b4eea31 100644 --- a/abi/codecForSimpleValues_numerical.go +++ b/abi/codecForSimpleValues_numerical.go @@ -62,7 +62,7 @@ func (c *codec) decodeNestedNumber(reader io.Reader, value any, numBytes int) er func (c *codec) decodeTopLevelUnsignedNumber(data []byte, maxValue uint64) (uint64, error) { b := big.NewInt(0).SetBytes(data) if !b.IsUint64() { - return 0, fmt.Errorf("decoded value is too large (does not fit an uint64): %s", b) + return 0, fmt.Errorf("decoded value is too large or invalid: %s", b) } n := b.Uint64() @@ -77,7 +77,7 @@ func (c *codec) decodeTopLevelSignedNumber(data []byte, maxValue int64) (int64, b := twos.FromBytes(data) if !b.IsInt64() { - return 0, fmt.Errorf("decoded value is too large (does not fit an int64): %s", b) + return 0, fmt.Errorf("decoded value is too large or invalid: %s", b) } n := b.Int64() diff --git a/abi/codecForSimpleValues_numerical_big.go b/abi/codecForSimpleValues_numerical_big.go index 926494e..67c90b5 100644 --- a/abi/codecForSimpleValues_numerical_big.go +++ b/abi/codecForSimpleValues_numerical_big.go @@ -7,8 +7,8 @@ import ( twos "github.com/multiversx/mx-components-big-int/twos-complement" ) -func (c *codec) encodeNestedBigNumber(writer io.Writer, value *big.Int, withSign bool) error { - data := bigIntToBytes(value, withSign) +func (c *codec) encodeNestedUnsignedBigNumber(writer io.Writer, value *big.Int) error { + data := value.Bytes() dataLength := len(data) // Write the length of the payload @@ -26,8 +26,27 @@ func (c *codec) encodeNestedBigNumber(writer io.Writer, value *big.Int, withSign return nil } -func (c *codec) encodeTopLevelBigNumber(writer io.Writer, value *big.Int, withSign bool) error { - data := bigIntToBytes(value, withSign) +func (c *codec) encodeNestedSignedBigNumber(writer io.Writer, value *big.Int) error { + data := twos.ToBytes(value) + dataLength := len(data) + + // Write the length of the payload + err := encodeLength(writer, uint32(dataLength)) + if err != nil { + return err + } + + // Write the payload + _, err = writer.Write(data) + if err != nil { + return err + } + + return nil +} + +func (c *codec) encodeTopLevelUnsignedBigNumber(writer io.Writer, value *big.Int) error { + data := value.Bytes() _, err := writer.Write(data) if err != nil { return err @@ -36,7 +55,17 @@ func (c *codec) encodeTopLevelBigNumber(writer io.Writer, value *big.Int, withSi return nil } -func (c *codec) decodeNestedBigNumber(reader io.Reader, withSign bool) (*big.Int, error) { +func (c *codec) encodeTopLevelSignedBigNumber(writer io.Writer, value *big.Int) error { + data := twos.ToBytes(value) + _, err := writer.Write(data) + if err != nil { + return err + } + + return nil +} + +func (c *codec) decodeNestedUnsignedBigNumber(reader io.Reader) (*big.Int, error) { // Read the length of the payload length, err := decodeLength(reader) if err != nil { @@ -49,25 +78,29 @@ func (c *codec) decodeNestedBigNumber(reader io.Reader, withSign bool) (*big.Int return nil, err } - return bigIntFromBytes(data, withSign), nil + return big.NewInt(0).SetBytes(data), nil } -func (c *codec) decodeTopLevelBigNumber(data []byte, withSign bool) *big.Int { - return bigIntFromBytes(data, withSign) -} +func (c *codec) decodeNestedSignedBigNumber(reader io.Reader) (*big.Int, error) { + // Read the length of the payload + length, err := decodeLength(reader) + if err != nil { + return nil, err + } -func bigIntToBytes(value *big.Int, withSign bool) []byte { - if withSign { - return twos.ToBytes(value) + // Read the payload + data, err := readBytesExactly(reader, int(length)) + if err != nil { + return nil, err } - return value.Bytes() + return twos.FromBytes(data), nil } -func bigIntFromBytes(data []byte, withSign bool) *big.Int { - if withSign { - return twos.FromBytes(data) - } - +func (c *codec) decodeTopLevelUnsignedBigNumber(data []byte) *big.Int { return big.NewInt(0).SetBytes(data) } + +func (c *codec) decodeTopLevelSignedBigNumber(data []byte) *big.Int { + return twos.FromBytes(data) +} diff --git a/abi/codecForSimpleValues_numerical_big_test.go b/abi/codecForSimpleValues_numerical_big_test.go index 526d899..830ae33 100644 --- a/abi/codecForSimpleValues_numerical_big_test.go +++ b/abi/codecForSimpleValues_numerical_big_test.go @@ -79,15 +79,4 @@ func TestCodec_NumericalBig(t *testing.T) { testDecodeTopLevel(t, codec, "00ff", &BigIntValue{}, &BigIntValue{Value: big.NewInt(255)}) testDecodeTopLevel(t, codec, "0100", &BigIntValue{}, &BigIntValue{Value: big.NewInt(256)}) }) - - t.Run("should err on decode top-level", func(t *testing.T) { - testDecodeTopLevelWithError(t, codec, "4142", &U8Value{}, "decoded value is too large") - testDecodeTopLevelWithError(t, codec, "4142", &I8Value{}, "decoded value is too large") - testDecodeTopLevelWithError(t, codec, "414243", &U16Value{}, "decoded value is too large") - testDecodeTopLevelWithError(t, codec, "414243", &I16Value{}, "decoded value is too large") - testDecodeTopLevelWithError(t, codec, "4142434445", &U32Value{}, "decoded value is too large") - testDecodeTopLevelWithError(t, codec, "4142434445", &I32Value{}, "decoded value is too large") - testDecodeTopLevelWithError(t, codec, "41424344454647489876", &U64Value{}, "decoded value is too large") - testDecodeTopLevelWithError(t, codec, "41424344454647489876", &I64Value{}, "decoded value is too large") - }) } diff --git a/abi/constants.go b/abi/constants.go index aa4c956..f07d4a2 100644 --- a/abi/constants.go +++ b/abi/constants.go @@ -2,3 +2,5 @@ package abi const trueAsByte = uint8(1) const falseAsByte = uint8(0) +const optionMarkerForAbsentValue = uint8(0) +const optionMarkerForPresentValue = uint8(1) diff --git a/abi/parts.go b/abi/parts.go index 0523f3d..7d45994 100644 --- a/abi/parts.go +++ b/abi/parts.go @@ -63,7 +63,7 @@ func (holder *partsHolder) hasAnyPart() bool { } func (holder *partsHolder) appendEmptyPart() { - holder.parts = append(holder.parts, []byte{}) + holder.parts = append(holder.parts, make([]byte, 0)) } // readWholeFocusedPart reads the whole focused part, if any. Otherwise, it returns an error. diff --git a/abi/serializer_test.go b/abi/serializer_test.go index 0036ff7..4fcf3ed 100644 --- a/abi/serializer_test.go +++ b/abi/serializer_test.go @@ -1,6 +1,9 @@ package abi import ( + "encoding/hex" + "math/big" + "strings" "testing" "github.com/stretchr/testify/require" @@ -359,13 +362,14 @@ func TestSerializer_Deserialize(t *testing.T) { ItemCreator: func() any { return &U8Value{} }, } - err := serializer.Deserialize("@01@", []any{destination}) + err := serializer.Deserialize("@01@00@", []any{destination}) require.NoError(t, err) require.Equal(t, []any{ &U8Value{Value: 0}, &U8Value{Value: 1}, &U8Value{Value: 0}, + &U8Value{Value: 0}, }, destination.Items) }) @@ -394,3 +398,237 @@ func TestSerializer_Deserialize(t *testing.T) { require.ErrorContains(t, err, "cannot decode (top-level) *abi.U8Value, because of: decoded value is too large: 256 > 255") }) } + +func TestSerializer_InRealWorldScenarios(t *testing.T) { + serializer, err := NewSerializer(ArgsNewSerializer{ + PartsSeparator: "@", + PubKeyLength: 32, + }) + require.NoError(t, err) + + alicePubKeyHex := "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1" + alicePubKey, _ := hex.DecodeString(alicePubKeyHex) + bobPubKeyHex := "8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8" + bobPubKey, _ := hex.DecodeString(bobPubKeyHex) + + oneQuintillion := big.NewInt(0).SetUint64(1_000_000_000_000_000_000) + + t.Run("real-world (1): serialize input of multisig.proposeBatch(variadic), ", func(t *testing.T) { + createEsdtTokenPayment := func(tokenIdentifier string, tokenNonce uint64, amount *big.Int) StructValue { + return StructValue{ + Fields: []Field{ + { + Name: "token_identifier", + Value: StringValue{Value: tokenIdentifier}, + }, + { + Name: "token_nonce", + Value: U64Value{Value: tokenNonce}, + }, + { + Name: "amount", + Value: BigUIntValue{Value: amount}, + }, + }, + } + } + + // First action: SendTransferExecuteEgld + firstAction := EnumValue{ + Discriminant: 5, + // CallActionData + Fields: []Field{ + { + Name: "to", + Value: AddressValue{Value: alicePubKey}, + }, + { + Name: "egld_amount", + Value: BigUIntValue{Value: oneQuintillion}, + }, + { + Name: "opt_gas_limit", + Value: OptionValue{ + Value: U64Value{Value: 15000000}, + }, + }, + { + Name: "endpoint_name", + Value: BytesValue{Value: []byte("example")}, + }, + { + Name: "arguments", + Value: InputListValue{ + Items: []any{ + BytesValue{Value: []byte{0x03, 0x42}}, + BytesValue{Value: []byte{0x07, 0x43}}, + }, + }, + }, + }, + } + + // Second action: SendTransferExecuteEsdt + secondAction := EnumValue{ + Discriminant: 6, + // EsdtTransferExecuteData + Fields: []Field{ + { + Name: "to", + Value: AddressValue{Value: alicePubKey}, + }, + { + Name: "tokens", + Value: InputListValue{ + Items: []any{ + createEsdtTokenPayment("beer", 0, oneQuintillion), + createEsdtTokenPayment("chocolate", 0, oneQuintillion), + }, + }, + }, + { + Name: "opt_gas_limit", + Value: OptionValue{ + Value: U64Value{Value: 15000000}, + }, + }, + { + Name: "endpoint_name", + Value: BytesValue{Value: []byte("example")}, + }, + { + Name: "arguments", + Value: InputListValue{ + Items: []any{ + BytesValue{Value: []byte{0x03, 0x42}}, + BytesValue{Value: []byte{0x07, 0x43}}, + }, + }, + }, + }, + } + + data, err := serializer.Serialize([]any{ + firstAction, + secondAction, + }) + + encodedExpected := strings.Join( + []string{ + "05|0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1|000000080de0b6b3a7640000|010000000000e4e1c0|000000076578616d706c65|00000002000000020342000000020743", + "06|0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1|00000002|0000000462656572|0000000000000000|000000080de0b6b3a7640000|0000000963686f636f6c617465|0000000000000000|000000080de0b6b3a7640000|010000000000e4e1c0|000000076578616d706c65|00000002000000020342000000020743", + }, + "@", + ) + + // Drop the delimiters (were added for readability) + encodedExpected = strings.Replace(encodedExpected, "|", "", -1) + + require.NoError(t, err) + require.Equal(t, encodedExpected, data) + }) + + t.Run("real-world (2): deserialize output of multisig.getPendingActionFullInfo() -> variadic, ", func(t *testing.T) { + dataHex := strings.Join([]string{ + "0000002A", + "0000002A", + "05|0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1|000000080de0b6b3a7640000|010000000000e4e1c0|000000076578616d706c65|00000002000000020342000000020743", + "00000002|0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1|8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8", + }, "") + // Drop the delimiters (were added for readability) + data := strings.Replace(dataHex, "|", "", -1) + + actionId := U32Value{} + groupId := U32Value{} + + actionTo := AddressValue{} + actionEgldAmount := BigUIntValue{} + actionGasLimit := U64Value{} + actionEndpointName := BytesValue{} + actionArguments := OutputListValue{ + ItemCreator: func() any { + return &BytesValue{} + }, + } + + action := EnumValue{ + Fields: []Field{ + { + Name: "to", + Value: &actionTo, + }, + { + Name: "egld_amount", + Value: &actionEgldAmount, + }, + { + Name: "opt_gas_limit", + Value: &OptionValue{ + Value: &actionGasLimit, + }, + }, + { + Name: "endpoint_name", + Value: &actionEndpointName, + }, + { + Name: "arguments", + Value: &actionArguments, + }, + }, + } + + signers := OutputListValue{ + ItemCreator: func() any { + return &AddressValue{} + }, + } + + destination := &OutputVariadicValues{ + ItemCreator: func() any { + return &StructValue{ + Fields: []Field{ + { + Name: "action_id", + Value: &actionId, + }, + { + Name: "group_id", + Value: &groupId, + }, + { + Name: "action_data", + Value: &action, + }, + { + Name: "signers", + Value: &signers, + }, + }, + } + }, + } + + err := serializer.Deserialize(data, []any{destination}) + require.NoError(t, err) + require.Len(t, destination.Items, 1) + + // result[0].action_id and result[0].group_id + require.Equal(t, uint32(42), actionId.Value) + require.Equal(t, uint32(42), groupId.Value) + + // result[0].action_data + require.Equal(t, uint8(5), action.Discriminant) + require.Equal(t, alicePubKey, actionTo.Value) + require.Equal(t, oneQuintillion, actionEgldAmount.Value) + require.Equal(t, uint64(15000000), actionGasLimit.Value) + require.Equal(t, []byte("example"), actionEndpointName.Value) + require.Len(t, actionArguments.Items, 2) + require.Equal(t, []byte{0x03, 0x42}, actionArguments.Items[0].(*BytesValue).Value) + require.Equal(t, []byte{0x07, 0x43}, actionArguments.Items[1].(*BytesValue).Value) + + // result[0].signers + require.Equal(t, alicePubKey, signers.Items[0].(*AddressValue).Value) + require.Equal(t, bobPubKey, signers.Items[1].(*AddressValue).Value) + }) +} diff --git a/abi/shared.go b/abi/shared.go index ca61e0f..5ba8112 100644 --- a/abi/shared.go +++ b/abi/shared.go @@ -29,7 +29,7 @@ func decodeLength(reader io.Reader) (uint32, error) { func readBytesExactly(reader io.Reader, numBytes int) ([]byte, error) { if numBytes == 0 { - return []byte{}, nil + return make([]byte, 0), nil } data := make([]byte, numBytes) diff --git a/abi/valuesSingle.go b/abi/valuesSingle.go index eff201a..cfbd070 100644 --- a/abi/valuesSingle.go +++ b/abi/valuesSingle.go @@ -42,7 +42,7 @@ type I64Value struct { Value int64 } -// BigIntValue is a wrapper for a big integer (unsigned) +// BigUIntValue is a wrapper for a big integer (unsigned) type BigUIntValue struct { Value *big.Int } @@ -72,7 +72,7 @@ type BoolValue struct { Value bool } -// OptionValue is a wrapper for an optional value +// OptionValue is a wrapper for an option value type OptionValue struct { Value any } From 6177439674f0b2dfe5835995828c95ab8050e2df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Tue, 30 Apr 2024 12:01:52 +0300 Subject: [PATCH 23/24] Fix after review (part 2). --- abi/codec.go | 186 +++++++++++------- abi/codecForCompositeTypes_list.go | 28 +-- abi/codecForCompositeTypes_list_test.go | 2 +- abi/codecForCompositeTypes_option.go | 20 +- abi/codecForCompositeTypes_option_test.go | 2 +- abi/codecForCustomTypes_enum.go | 24 ++- abi/codecForCustomTypes_enum_test.go | 2 +- abi/codecForCustomTypes_struct.go | 20 +- abi/codecForCustomTypes_struct_test.go | 2 +- abi/codecForSimpleValues_address.go | 16 +- abi/codecForSimpleValues_address_test.go | 2 +- abi/codecForSimpleValues_boolean.go | 13 +- abi/codecForSimpleValues_boolean_test.go | 2 +- abi/codecForSimpleValues_bytes.go | 11 +- abi/codecForSimpleValues_bytes_test.go | 2 +- abi/codecForSimpleValues_numerical.go | 15 +- abi/codecForSimpleValues_numerical_big.go | 19 +- ...codecForSimpleValues_numerical_big_test.go | 2 +- abi/codecForSimpleValues_numerical_test.go | 2 +- abi/codecForSimpleValues_string.go | 11 +- abi/codecForSimpleValues_string_test.go | 2 +- 21 files changed, 226 insertions(+), 157 deletions(-) diff --git a/abi/codec.go b/abi/codec.go index f619cd2..cad347d 100644 --- a/abi/codec.go +++ b/abi/codec.go @@ -9,7 +9,16 @@ import ( ) type codec struct { - pubKeyLength int + codecForBool *codecForBool + codecForSmallInt *codecForSmallInt + codecForBigInt *codecForBigInt + codecForAddress *codecForAddress + codecForString *codecForString + codecForBytes *codecForBytes + codecForStruct *codeForStruct + codecForEnum *codecForEnum + codecForOption *codecForOption + codecForList *codecForList } // argsNewCodec defines the arguments needed for a new codec @@ -24,9 +33,34 @@ func newCodec(args argsNewCodec) (*codec, error) { return nil, errors.New("cannot create codec: bad public key length") } - return &codec{ - pubKeyLength: args.pubKeyLength, - }, nil + codec := &codec{ + codecForBool: &codecForBool{}, + codecForSmallInt: &codecForSmallInt{}, + codecForBigInt: &codecForBigInt{}, + codecForAddress: &codecForAddress{ + pubKeyLength: args.pubKeyLength, + }, + codecForString: &codecForString{}, + codecForBytes: &codecForBytes{}, + } + + codec.codecForStruct = &codeForStruct{ + generalCodec: codec, + } + + codec.codecForEnum = &codecForEnum{ + generalCodec: codec, + } + + codec.codecForOption = &codecForOption{ + generalCodec: codec, + } + + codec.codecForList = &codecForList{ + generalCodec: codec, + } + + return codec, nil } // EncodeNested encodes the given value following the nested encoding rules @@ -43,41 +77,41 @@ func (c *codec) EncodeNested(value any) ([]byte, error) { func (c *codec) doEncodeNested(writer io.Writer, value any) error { switch value := value.(type) { case BoolValue: - return c.encodeNestedBool(writer, value) + return c.codecForBool.encodeNested(writer, value) case U8Value: - return c.encodeNestedNumber(writer, value.Value, 1) + return c.codecForSmallInt.encodeNested(writer, value.Value, 1) case U16Value: - return c.encodeNestedNumber(writer, value.Value, 2) + return c.codecForSmallInt.encodeNested(writer, value.Value, 2) case U32Value: - return c.encodeNestedNumber(writer, value.Value, 4) + return c.codecForSmallInt.encodeNested(writer, value.Value, 4) case U64Value: - return c.encodeNestedNumber(writer, value.Value, 8) + return c.codecForSmallInt.encodeNested(writer, value.Value, 8) case I8Value: - return c.encodeNestedNumber(writer, value.Value, 1) + return c.codecForSmallInt.encodeNested(writer, value.Value, 1) case I16Value: - return c.encodeNestedNumber(writer, value.Value, 2) + return c.codecForSmallInt.encodeNested(writer, value.Value, 2) case I32Value: - return c.encodeNestedNumber(writer, value.Value, 4) + return c.codecForSmallInt.encodeNested(writer, value.Value, 4) case I64Value: - return c.encodeNestedNumber(writer, value.Value, 8) + return c.codecForSmallInt.encodeNested(writer, value.Value, 8) case BigUIntValue: - return c.encodeNestedUnsignedBigNumber(writer, value.Value) + return c.codecForBigInt.encodeNestedUnsigned(writer, value.Value) case BigIntValue: - return c.encodeNestedSignedBigNumber(writer, value.Value) + return c.codecForBigInt.encodeNestedSigned(writer, value.Value) case AddressValue: - return c.encodeNestedAddress(writer, value) + return c.codecForAddress.encodeNested(writer, value) case StringValue: - return c.encodeNestedString(writer, value) + return c.codecForString.encodeNested(writer, value) case BytesValue: - return c.encodeNestedBytes(writer, value) + return c.codecForBytes.encodeNested(writer, value) case StructValue: - return c.encodeNestedStruct(writer, value) + return c.codecForStruct.encodeNested(writer, value) case EnumValue: - return c.encodeNestedEnum(writer, value) + return c.codecForEnum.encodeNested(writer, value) case OptionValue: - return c.encodeNestedOption(writer, value) + return c.codecForOption.encodeNested(writer, value) case InputListValue: - return c.encodeNestedList(writer, value) + return c.codecForList.encodeNested(writer, value) default: return fmt.Errorf("unsupported type for nested encoding: %T", value) } @@ -97,41 +131,41 @@ func (c *codec) EncodeTopLevel(value any) ([]byte, error) { func (c *codec) doEncodeTopLevel(writer io.Writer, value any) error { switch value := value.(type) { case BoolValue: - return c.encodeTopLevelBool(writer, value) + return c.codecForBool.encodeTopLevel(writer, value) case U8Value: - return c.encodeTopLevelUnsignedNumber(writer, uint64(value.Value)) + return c.codecForSmallInt.encodeTopLevelUnsigned(writer, uint64(value.Value)) case U16Value: - return c.encodeTopLevelUnsignedNumber(writer, uint64(value.Value)) + return c.codecForSmallInt.encodeTopLevelUnsigned(writer, uint64(value.Value)) case U32Value: - return c.encodeTopLevelUnsignedNumber(writer, uint64(value.Value)) + return c.codecForSmallInt.encodeTopLevelUnsigned(writer, uint64(value.Value)) case U64Value: - return c.encodeTopLevelUnsignedNumber(writer, value.Value) + return c.codecForSmallInt.encodeTopLevelUnsigned(writer, value.Value) case I8Value: - return c.encodeTopLevelSignedNumber(writer, int64(value.Value)) + return c.codecForSmallInt.encodeTopLevelSigned(writer, int64(value.Value)) case I16Value: - return c.encodeTopLevelSignedNumber(writer, int64(value.Value)) + return c.codecForSmallInt.encodeTopLevelSigned(writer, int64(value.Value)) case I32Value: - return c.encodeTopLevelSignedNumber(writer, int64(value.Value)) + return c.codecForSmallInt.encodeTopLevelSigned(writer, int64(value.Value)) case I64Value: - return c.encodeTopLevelSignedNumber(writer, value.Value) + return c.codecForSmallInt.encodeTopLevelSigned(writer, value.Value) case BigUIntValue: - return c.encodeTopLevelUnsignedBigNumber(writer, value.Value) + return c.codecForBigInt.encodeTopLevelUnsigned(writer, value.Value) case BigIntValue: - return c.encodeTopLevelSignedBigNumber(writer, value.Value) + return c.codecForBigInt.encodeTopLevelSigned(writer, value.Value) case AddressValue: - return c.encodeTopLevelAddress(writer, value) + return c.codecForAddress.encodeTopLevel(writer, value) case StringValue: - return c.encodeTopLevelString(writer, value) + return c.codecForString.encodeTopLevel(writer, value) case BytesValue: - return c.encodeTopLevelBytes(writer, value) + return c.codecForBytes.encodeTopLevel(writer, value) case StructValue: - return c.encodeTopLevelStruct(writer, value) + return c.codecForStruct.encodeTopLevel(writer, value) case EnumValue: - return c.encodeTopLevelEnum(writer, value) + return c.codecForEnum.encodeTopLevel(writer, value) case OptionValue: - return c.encodeTopLevelOption(writer, value) + return c.codecForOption.encodeTopLevel(writer, value) case InputListValue: - return c.encodeTopLevelList(writer, value) + return c.codecForList.encodeTopLevel(writer, value) default: return fmt.Errorf("unsupported type for top-level encoding: %T", value) } @@ -151,25 +185,25 @@ func (c *codec) DecodeNested(data []byte, value any) error { func (c *codec) doDecodeNested(reader io.Reader, value any) error { switch value := value.(type) { case *BoolValue: - return c.decodeNestedBool(reader, value) + return c.codecForBool.decodeNested(reader, value) case *U8Value: - return c.decodeNestedNumber(reader, &value.Value, 1) + return c.codecForSmallInt.decodeNested(reader, &value.Value, 1) case *U16Value: - return c.decodeNestedNumber(reader, &value.Value, 2) + return c.codecForSmallInt.decodeNested(reader, &value.Value, 2) case *U32Value: - return c.decodeNestedNumber(reader, &value.Value, 4) + return c.codecForSmallInt.decodeNested(reader, &value.Value, 4) case *U64Value: - return c.decodeNestedNumber(reader, &value.Value, 8) + return c.codecForSmallInt.decodeNested(reader, &value.Value, 8) case *I8Value: - return c.decodeNestedNumber(reader, &value.Value, 1) + return c.codecForSmallInt.decodeNested(reader, &value.Value, 1) case *I16Value: - return c.decodeNestedNumber(reader, &value.Value, 2) + return c.codecForSmallInt.decodeNested(reader, &value.Value, 2) case *I32Value: - return c.decodeNestedNumber(reader, &value.Value, 4) + return c.codecForSmallInt.decodeNested(reader, &value.Value, 4) case *I64Value: - return c.decodeNestedNumber(reader, &value.Value, 8) + return c.codecForSmallInt.decodeNested(reader, &value.Value, 8) case *BigUIntValue: - n, err := c.decodeNestedUnsignedBigNumber(reader) + n, err := c.codecForBigInt.decodeNestedUnsigned(reader) if err != nil { return err } @@ -177,7 +211,7 @@ func (c *codec) doDecodeNested(reader io.Reader, value any) error { value.Value = n return nil case *BigIntValue: - n, err := c.decodeNestedSignedBigNumber(reader) + n, err := c.codecForBigInt.decodeNestedSigned(reader) if err != nil { return err } @@ -185,19 +219,19 @@ func (c *codec) doDecodeNested(reader io.Reader, value any) error { value.Value = n return nil case *AddressValue: - return c.decodeNestedAddress(reader, value) + return c.codecForAddress.decodeNested(reader, value) case *StringValue: - return c.decodeNestedString(reader, value) + return c.codecForString.decodeNested(reader, value) case *BytesValue: - return c.decodeNestedBytes(reader, value) + return c.codecForBytes.decodeNested(reader, value) case *StructValue: - return c.decodeNestedStruct(reader, value) + return c.codecForStruct.decodeNested(reader, value) case *EnumValue: - return c.decodeNestedEnum(reader, value) + return c.codecForEnum.decodeNested(reader, value) case *OptionValue: - return c.decodeNestedOption(reader, value) + return c.codecForOption.decodeNested(reader, value) case *OutputListValue: - return c.decodeNestedList(reader, value) + return c.codecForList.decodeNested(reader, value) default: return fmt.Errorf("unsupported type for nested decoding: %T", value) } @@ -216,51 +250,51 @@ func (c *codec) DecodeTopLevel(data []byte, value any) error { func (c *codec) doDecodeTopLevel(data []byte, value any) error { switch value := value.(type) { case *BoolValue: - return c.decodeTopLevelBool(data, value) + return c.codecForBool.decodeTopLevel(data, value) case *U8Value: - n, err := c.decodeTopLevelUnsignedNumber(data, math.MaxUint8) + n, err := c.codecForSmallInt.decodeTopLevelUnsigned(data, math.MaxUint8) if err != nil { return err } value.Value = uint8(n) case *U16Value: - n, err := c.decodeTopLevelUnsignedNumber(data, math.MaxUint16) + n, err := c.codecForSmallInt.decodeTopLevelUnsigned(data, math.MaxUint16) if err != nil { return err } value.Value = uint16(n) case *U32Value: - n, err := c.decodeTopLevelUnsignedNumber(data, math.MaxUint32) + n, err := c.codecForSmallInt.decodeTopLevelUnsigned(data, math.MaxUint32) if err != nil { return err } value.Value = uint32(n) case *U64Value: - n, err := c.decodeTopLevelUnsignedNumber(data, math.MaxUint64) + n, err := c.codecForSmallInt.decodeTopLevelUnsigned(data, math.MaxUint64) if err != nil { return err } value.Value = uint64(n) case *I8Value: - n, err := c.decodeTopLevelSignedNumber(data, math.MaxInt8) + n, err := c.codecForSmallInt.decodeTopLevelSigned(data, math.MaxInt8) if err != nil { return err } value.Value = int8(n) case *I16Value: - n, err := c.decodeTopLevelSignedNumber(data, math.MaxInt16) + n, err := c.codecForSmallInt.decodeTopLevelSigned(data, math.MaxInt16) if err != nil { return err } value.Value = int16(n) case *I32Value: - n, err := c.decodeTopLevelSignedNumber(data, math.MaxInt32) + n, err := c.codecForSmallInt.decodeTopLevelSigned(data, math.MaxInt32) if err != nil { return err } @@ -268,32 +302,32 @@ func (c *codec) doDecodeTopLevel(data []byte, value any) error { value.Value = int32(n) case *I64Value: - n, err := c.decodeTopLevelSignedNumber(data, math.MaxInt64) + n, err := c.codecForSmallInt.decodeTopLevelSigned(data, math.MaxInt64) if err != nil { return err } value.Value = int64(n) case *BigUIntValue: - n := c.decodeTopLevelUnsignedBigNumber(data) + n := c.codecForBigInt.decodeTopLevelUnsigned(data) value.Value = n case *BigIntValue: - n := c.decodeTopLevelSignedBigNumber(data) + n := c.codecForBigInt.decodeTopLevelSigned(data) value.Value = n case *AddressValue: - return c.decodeTopLevelAddress(data, value) + return c.codecForAddress.decodeTopLevel(data, value) case *StringValue: - return c.decodeTopLevelString(data, value) + return c.codecForString.decodeTopLevel(data, value) case *BytesValue: - return c.decodeTopLevelBytes(data, value) + return c.codecForBytes.decodeTopLevel(data, value) case *StructValue: - return c.decodeTopLevelStruct(data, value) + return c.codecForStruct.decodeTopLevel(data, value) case *EnumValue: - return c.decodeTopLevelEnum(data, value) + return c.codecForEnum.decodeTopLevel(data, value) case *OptionValue: - return c.decodeTopLevelOption(data, value) + return c.codecForOption.decodeTopLevel(data, value) case *OutputListValue: - return c.decodeTopLevelList(data, value) + return c.codecForList.decodeTopLevel(data, value) default: return fmt.Errorf("unsupported type for top-level decoding: %T", value) } diff --git a/abi/codecForCompositeTypes_list.go b/abi/codecForCompositeTypes_list.go index 86a894b..8289c3a 100644 --- a/abi/codecForCompositeTypes_list.go +++ b/abi/codecForCompositeTypes_list.go @@ -6,20 +6,24 @@ import ( "io" ) -func (c *codec) encodeNestedList(writer io.Writer, value InputListValue) error { +type codecForList struct { + generalCodec *codec +} + +func (c *codecForList) encodeNested(writer io.Writer, value InputListValue) error { err := encodeLength(writer, uint32(len(value.Items))) if err != nil { return err } - return c.encodeListItems(writer, value) + return c.encodeItems(writer, value) } -func (c *codec) encodeTopLevelList(writer io.Writer, value InputListValue) error { - return c.encodeListItems(writer, value) +func (c *codecForList) encodeTopLevel(writer io.Writer, value InputListValue) error { + return c.encodeItems(writer, value) } -func (c *codec) decodeNestedList(reader io.Reader, value *OutputListValue) error { +func (c *codecForList) decodeNested(reader io.Reader, value *OutputListValue) error { if value.ItemCreator == nil { return errors.New("cannot decode list: item creator is nil") } @@ -32,7 +36,7 @@ func (c *codec) decodeNestedList(reader io.Reader, value *OutputListValue) error value.Items = make([]any, 0, length) for i := uint32(0); i < length; i++ { - err := c.decodeListItem(reader, value) + err := c.decodeItem(reader, value) if err != nil { return err } @@ -41,7 +45,7 @@ func (c *codec) decodeNestedList(reader io.Reader, value *OutputListValue) error return nil } -func (c *codec) decodeTopLevelList(data []byte, value *OutputListValue) error { +func (c *codecForList) decodeTopLevel(data []byte, value *OutputListValue) error { if value.ItemCreator == nil { return errors.New("cannot decode list: item creator is nil") } @@ -50,7 +54,7 @@ func (c *codec) decodeTopLevelList(data []byte, value *OutputListValue) error { value.Items = make([]any, 0) for reader.Len() > 0 { - err := c.decodeListItem(reader, value) + err := c.decodeItem(reader, value) if err != nil { return err } @@ -59,9 +63,9 @@ func (c *codec) decodeTopLevelList(data []byte, value *OutputListValue) error { return nil } -func (c *codec) encodeListItems(writer io.Writer, value InputListValue) error { +func (c *codecForList) encodeItems(writer io.Writer, value InputListValue) error { for _, item := range value.Items { - err := c.doEncodeNested(writer, item) + err := c.generalCodec.doEncodeNested(writer, item) if err != nil { return err } @@ -70,10 +74,10 @@ func (c *codec) encodeListItems(writer io.Writer, value InputListValue) error { return nil } -func (c *codec) decodeListItem(reader io.Reader, value *OutputListValue) error { +func (c *codecForList) decodeItem(reader io.Reader, value *OutputListValue) error { newItem := value.ItemCreator() - err := c.doDecodeNested(reader, newItem) + err := c.generalCodec.doDecodeNested(reader, newItem) if err != nil { return err } diff --git a/abi/codecForCompositeTypes_list_test.go b/abi/codecForCompositeTypes_list_test.go index 70983fb..40552cd 100644 --- a/abi/codecForCompositeTypes_list_test.go +++ b/abi/codecForCompositeTypes_list_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestCodec_List(t *testing.T) { +func TestCodecForList(t *testing.T) { codec, _ := newCodec(argsNewCodec{ pubKeyLength: 32, }) diff --git a/abi/codecForCompositeTypes_option.go b/abi/codecForCompositeTypes_option.go index 8babb6e..2c9ea98 100644 --- a/abi/codecForCompositeTypes_option.go +++ b/abi/codecForCompositeTypes_option.go @@ -6,7 +6,11 @@ import ( "io" ) -func (c *codec) encodeNestedOption(writer io.Writer, value OptionValue) error { +type codecForOption struct { + generalCodec *codec +} + +func (c *codecForOption) encodeNested(writer io.Writer, value OptionValue) error { if value.Value == nil { _, err := writer.Write([]byte{optionMarkerForAbsentValue}) return err @@ -17,10 +21,10 @@ func (c *codec) encodeNestedOption(writer io.Writer, value OptionValue) error { return err } - return c.doEncodeNested(writer, value.Value) + return c.generalCodec.doEncodeNested(writer, value.Value) } -func (c *codec) encodeTopLevelOption(writer io.Writer, value OptionValue) error { +func (c *codecForOption) encodeTopLevel(writer io.Writer, value OptionValue) error { if value.Value == nil { return nil } @@ -30,10 +34,10 @@ func (c *codec) encodeTopLevelOption(writer io.Writer, value OptionValue) error return err } - return c.doEncodeNested(writer, value.Value) + return c.generalCodec.doEncodeNested(writer, value.Value) } -func (c *codec) decodeNestedOption(reader io.Reader, value *OptionValue) error { +func (c *codecForOption) decodeNested(reader io.Reader, value *OptionValue) error { data, err := readBytesExactly(reader, 1) if err != nil { return err @@ -47,13 +51,13 @@ func (c *codec) decodeNestedOption(reader io.Reader, value *OptionValue) error { } if firstByte == optionMarkerForPresentValue { - return c.doDecodeNested(reader, value.Value) + return c.generalCodec.doDecodeNested(reader, value.Value) } return fmt.Errorf("invalid first byte for nested encoded option: %d", firstByte) } -func (c *codec) decodeTopLevelOption(data []byte, value *OptionValue) error { +func (c *codecForOption) decodeTopLevel(data []byte, value *OptionValue) error { if len(data) == 0 { value.Value = nil return nil @@ -67,5 +71,5 @@ func (c *codec) decodeTopLevelOption(data []byte, value *OptionValue) error { } reader := bytes.NewReader(dataAfterFirstByte) - return c.doDecodeNested(reader, value.Value) + return c.generalCodec.doDecodeNested(reader, value.Value) } diff --git a/abi/codecForCompositeTypes_option_test.go b/abi/codecForCompositeTypes_option_test.go index 029a733..f127d28 100644 --- a/abi/codecForCompositeTypes_option_test.go +++ b/abi/codecForCompositeTypes_option_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestCodec_Option(t *testing.T) { +func TestCodecForOption(t *testing.T) { codec, _ := newCodec(argsNewCodec{ pubKeyLength: 32, }) diff --git a/abi/codecForCustomTypes_enum.go b/abi/codecForCustomTypes_enum.go index 7a215d1..91c6092 100644 --- a/abi/codecForCustomTypes_enum.go +++ b/abi/codecForCustomTypes_enum.go @@ -6,14 +6,18 @@ import ( "io" ) -func (c *codec) encodeNestedEnum(writer io.Writer, value EnumValue) error { - err := c.doEncodeNested(writer, U8Value{Value: value.Discriminant}) +type codecForEnum struct { + generalCodec *codec +} + +func (c *codecForEnum) encodeNested(writer io.Writer, value EnumValue) error { + err := c.generalCodec.doEncodeNested(writer, U8Value{Value: value.Discriminant}) if err != nil { return err } for _, field := range value.Fields { - err := c.doEncodeNested(writer, field.Value) + err := c.generalCodec.doEncodeNested(writer, field.Value) if err != nil { return fmt.Errorf("cannot encode field '%s' of enum, because of: %w", field.Name, err) } @@ -22,18 +26,18 @@ func (c *codec) encodeNestedEnum(writer io.Writer, value EnumValue) error { return nil } -func (c *codec) encodeTopLevelEnum(writer io.Writer, value EnumValue) error { +func (c *codecForEnum) encodeTopLevel(writer io.Writer, value EnumValue) error { if value.Discriminant == 0 && len(value.Fields) == 0 { // Write nothing return nil } - return c.encodeNestedEnum(writer, value) + return c.encodeNested(writer, value) } -func (c *codec) decodeNestedEnum(reader io.Reader, value *EnumValue) error { +func (c *codecForEnum) decodeNested(reader io.Reader, value *EnumValue) error { discriminant := &U8Value{} - err := c.doDecodeNested(reader, discriminant) + err := c.generalCodec.doDecodeNested(reader, discriminant) if err != nil { return err } @@ -41,7 +45,7 @@ func (c *codec) decodeNestedEnum(reader io.Reader, value *EnumValue) error { value.Discriminant = discriminant.Value for _, field := range value.Fields { - err := c.doDecodeNested(reader, field.Value) + err := c.generalCodec.doDecodeNested(reader, field.Value) if err != nil { return fmt.Errorf("cannot decode field '%s' of enum, because of: %w", field.Name, err) } @@ -50,12 +54,12 @@ func (c *codec) decodeNestedEnum(reader io.Reader, value *EnumValue) error { return nil } -func (c *codec) decodeTopLevelEnum(data []byte, value *EnumValue) error { +func (c *codecForEnum) decodeTopLevel(data []byte, value *EnumValue) error { if len(data) == 0 { value.Discriminant = 0 return nil } reader := bytes.NewReader(data) - return c.decodeNestedEnum(reader, value) + return c.decodeNested(reader, value) } diff --git a/abi/codecForCustomTypes_enum_test.go b/abi/codecForCustomTypes_enum_test.go index 220d190..f4a3dea 100644 --- a/abi/codecForCustomTypes_enum_test.go +++ b/abi/codecForCustomTypes_enum_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestCodec_Enum(t *testing.T) { +func TestCodecForEnum(t *testing.T) { codec, _ := newCodec(argsNewCodec{ pubKeyLength: 32, }) diff --git a/abi/codecForCustomTypes_struct.go b/abi/codecForCustomTypes_struct.go index ea7e48d..974f5b6 100644 --- a/abi/codecForCustomTypes_struct.go +++ b/abi/codecForCustomTypes_struct.go @@ -6,9 +6,13 @@ import ( "io" ) -func (c *codec) encodeNestedStruct(writer io.Writer, value StructValue) error { +type codeForStruct struct { + generalCodec *codec +} + +func (c *codeForStruct) encodeNested(writer io.Writer, value StructValue) error { for _, field := range value.Fields { - err := c.doEncodeNested(writer, field.Value) + err := c.generalCodec.doEncodeNested(writer, field.Value) if err != nil { return fmt.Errorf("cannot encode field '%s' of struct, because of: %w", field.Name, err) } @@ -17,13 +21,13 @@ func (c *codec) encodeNestedStruct(writer io.Writer, value StructValue) error { return nil } -func (c *codec) encodeTopLevelStruct(writer io.Writer, value StructValue) error { - return c.encodeNestedStruct(writer, value) +func (c *codeForStruct) encodeTopLevel(writer io.Writer, value StructValue) error { + return c.encodeNested(writer, value) } -func (c *codec) decodeNestedStruct(reader io.Reader, value *StructValue) error { +func (c *codeForStruct) decodeNested(reader io.Reader, value *StructValue) error { for _, field := range value.Fields { - err := c.doDecodeNested(reader, field.Value) + err := c.generalCodec.doDecodeNested(reader, field.Value) if err != nil { return fmt.Errorf("cannot decode field '%s' of struct, because of: %w", field.Name, err) } @@ -32,7 +36,7 @@ func (c *codec) decodeNestedStruct(reader io.Reader, value *StructValue) error { return nil } -func (c *codec) decodeTopLevelStruct(data []byte, value *StructValue) error { +func (c *codeForStruct) decodeTopLevel(data []byte, value *StructValue) error { reader := bytes.NewReader(data) - return c.decodeNestedStruct(reader, value) + return c.decodeNested(reader, value) } diff --git a/abi/codecForCustomTypes_struct_test.go b/abi/codecForCustomTypes_struct_test.go index 24e7171..a3eeeb9 100644 --- a/abi/codecForCustomTypes_struct_test.go +++ b/abi/codecForCustomTypes_struct_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestCodec_Struct(t *testing.T) { +func TestCodecForStruct(t *testing.T) { codec, _ := newCodec(argsNewCodec{ pubKeyLength: 32, }) diff --git a/abi/codecForSimpleValues_address.go b/abi/codecForSimpleValues_address.go index 83f54a7..7d8cd97 100644 --- a/abi/codecForSimpleValues_address.go +++ b/abi/codecForSimpleValues_address.go @@ -5,7 +5,11 @@ import ( "io" ) -func (c *codec) encodeNestedAddress(writer io.Writer, value AddressValue) error { +type codecForAddress struct { + pubKeyLength int +} + +func (c *codecForAddress) encodeNested(writer io.Writer, value AddressValue) error { err := c.checkPubKeyLength(value.Value) if err != nil { return err @@ -15,11 +19,11 @@ func (c *codec) encodeNestedAddress(writer io.Writer, value AddressValue) error return err } -func (c *codec) encodeTopLevelAddress(writer io.Writer, value AddressValue) error { - return c.encodeNestedAddress(writer, value) +func (c *codecForAddress) encodeTopLevel(writer io.Writer, value AddressValue) error { + return c.encodeNested(writer, value) } -func (c *codec) decodeNestedAddress(reader io.Reader, value *AddressValue) error { +func (c *codecForAddress) decodeNested(reader io.Reader, value *AddressValue) error { data, err := readBytesExactly(reader, c.pubKeyLength) if err != nil { return err @@ -29,7 +33,7 @@ func (c *codec) decodeNestedAddress(reader io.Reader, value *AddressValue) error return nil } -func (c *codec) decodeTopLevelAddress(data []byte, value *AddressValue) error { +func (c *codecForAddress) decodeTopLevel(data []byte, value *AddressValue) error { err := c.checkPubKeyLength(data) if err != nil { return err @@ -39,7 +43,7 @@ func (c *codec) decodeTopLevelAddress(data []byte, value *AddressValue) error { return nil } -func (c *codec) checkPubKeyLength(pubkey []byte) error { +func (c *codecForAddress) checkPubKeyLength(pubkey []byte) error { if len(pubkey) != c.pubKeyLength { return fmt.Errorf("public key (address) has invalid length: %d", len(pubkey)) } diff --git a/abi/codecForSimpleValues_address_test.go b/abi/codecForSimpleValues_address_test.go index 62fc612..2729dae 100644 --- a/abi/codecForSimpleValues_address_test.go +++ b/abi/codecForSimpleValues_address_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestCodec_Address(t *testing.T) { +func TestCodecForAddress(t *testing.T) { codec, _ := newCodec(argsNewCodec{ pubKeyLength: 32, }) diff --git a/abi/codecForSimpleValues_boolean.go b/abi/codecForSimpleValues_boolean.go index d70fcd3..b34d902 100644 --- a/abi/codecForSimpleValues_boolean.go +++ b/abi/codecForSimpleValues_boolean.go @@ -5,7 +5,10 @@ import ( "io" ) -func (c *codec) encodeNestedBool(writer io.Writer, value BoolValue) error { +type codecForBool struct { +} + +func (c *codecForBool) encodeNested(writer io.Writer, value BoolValue) error { if value.Value { _, err := writer.Write([]byte{trueAsByte}) return err @@ -15,7 +18,7 @@ func (c *codec) encodeNestedBool(writer io.Writer, value BoolValue) error { return err } -func (c *codec) encodeTopLevelBool(writer io.Writer, value BoolValue) error { +func (c *codecForBool) encodeTopLevel(writer io.Writer, value BoolValue) error { if !value.Value { // For "false", write nothing. return nil @@ -25,7 +28,7 @@ func (c *codec) encodeTopLevelBool(writer io.Writer, value BoolValue) error { return err } -func (c *codec) decodeNestedBool(reader io.Reader, value *BoolValue) error { +func (c *codecForBool) decodeNested(reader io.Reader, value *BoolValue) error { data, err := readBytesExactly(reader, 1) if err != nil { return err @@ -39,7 +42,7 @@ func (c *codec) decodeNestedBool(reader io.Reader, value *BoolValue) error { return nil } -func (c *codec) decodeTopLevelBool(data []byte, value *BoolValue) error { +func (c *codecForBool) decodeTopLevel(data []byte, value *BoolValue) error { if len(data) == 0 { value.Value = false return nil @@ -58,7 +61,7 @@ func (c *codec) decodeTopLevelBool(data []byte, value *BoolValue) error { return fmt.Errorf("unexpected boolean value: %v", data) } -func (c *codec) byteToBool(data uint8) (bool, error) { +func (c *codecForBool) byteToBool(data uint8) (bool, error) { switch data { case trueAsByte: return true, nil diff --git a/abi/codecForSimpleValues_boolean_test.go b/abi/codecForSimpleValues_boolean_test.go index 6bc6d74..7fbb471 100644 --- a/abi/codecForSimpleValues_boolean_test.go +++ b/abi/codecForSimpleValues_boolean_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestCodec_Boolean(t *testing.T) { +func TestCodecForBool(t *testing.T) { codec, _ := newCodec(argsNewCodec{ pubKeyLength: 32, }) diff --git a/abi/codecForSimpleValues_bytes.go b/abi/codecForSimpleValues_bytes.go index ab9decb..745944d 100644 --- a/abi/codecForSimpleValues_bytes.go +++ b/abi/codecForSimpleValues_bytes.go @@ -4,7 +4,10 @@ import ( "io" ) -func (c *codec) encodeNestedBytes(writer io.Writer, value BytesValue) error { +type codecForBytes struct { +} + +func (c *codecForBytes) encodeNested(writer io.Writer, value BytesValue) error { err := encodeLength(writer, uint32(len(value.Value))) if err != nil { return err @@ -14,12 +17,12 @@ func (c *codec) encodeNestedBytes(writer io.Writer, value BytesValue) error { return err } -func (c *codec) encodeTopLevelBytes(writer io.Writer, value BytesValue) error { +func (c *codecForBytes) encodeTopLevel(writer io.Writer, value BytesValue) error { _, err := writer.Write(value.Value) return err } -func (c *codec) decodeNestedBytes(reader io.Reader, value *BytesValue) error { +func (c *codecForBytes) decodeNested(reader io.Reader, value *BytesValue) error { length, err := decodeLength(reader) if err != nil { return err @@ -34,7 +37,7 @@ func (c *codec) decodeNestedBytes(reader io.Reader, value *BytesValue) error { return nil } -func (c *codec) decodeTopLevelBytes(data []byte, value *BytesValue) error { +func (c *codecForBytes) decodeTopLevel(data []byte, value *BytesValue) error { value.Value = data return nil } diff --git a/abi/codecForSimpleValues_bytes_test.go b/abi/codecForSimpleValues_bytes_test.go index 14cd8b9..19683cb 100644 --- a/abi/codecForSimpleValues_bytes_test.go +++ b/abi/codecForSimpleValues_bytes_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestCodec_Bytes(t *testing.T) { +func TestCodecForBytes(t *testing.T) { codec, _ := newCodec(argsNewCodec{ pubKeyLength: 32, }) diff --git a/abi/codecForSimpleValues_numerical.go b/abi/codecForSimpleValues_numerical.go index b4eea31..8a51e2c 100644 --- a/abi/codecForSimpleValues_numerical.go +++ b/abi/codecForSimpleValues_numerical.go @@ -10,7 +10,10 @@ import ( twos "github.com/multiversx/mx-components-big-int/twos-complement" ) -func (c *codec) encodeNestedNumber(writer io.Writer, value any, numBytes int) error { +type codecForSmallInt struct { +} + +func (c *codecForSmallInt) encodeNested(writer io.Writer, value any, numBytes int) error { buffer := new(bytes.Buffer) err := binary.Write(buffer, binary.BigEndian, value) @@ -31,20 +34,20 @@ func (c *codec) encodeNestedNumber(writer io.Writer, value any, numBytes int) er return nil } -func (c *codec) encodeTopLevelUnsignedNumber(writer io.Writer, value uint64) error { +func (c *codecForSmallInt) encodeTopLevelUnsigned(writer io.Writer, value uint64) error { b := big.NewInt(0).SetUint64(value) data := b.Bytes() _, err := writer.Write(data) return err } -func (c *codec) encodeTopLevelSignedNumber(writer io.Writer, value int64) error { +func (c *codecForSmallInt) encodeTopLevelSigned(writer io.Writer, value int64) error { data := twos.ToBytes(big.NewInt(value)) _, err := writer.Write(data) return err } -func (c *codec) decodeNestedNumber(reader io.Reader, value any, numBytes int) error { +func (c *codecForSmallInt) decodeNested(reader io.Reader, value any, numBytes int) error { data, err := readBytesExactly(reader, numBytes) if err != nil { return err @@ -59,7 +62,7 @@ func (c *codec) decodeNestedNumber(reader io.Reader, value any, numBytes int) er return nil } -func (c *codec) decodeTopLevelUnsignedNumber(data []byte, maxValue uint64) (uint64, error) { +func (c *codecForSmallInt) decodeTopLevelUnsigned(data []byte, maxValue uint64) (uint64, error) { b := big.NewInt(0).SetBytes(data) if !b.IsUint64() { return 0, fmt.Errorf("decoded value is too large or invalid: %s", b) @@ -73,7 +76,7 @@ func (c *codec) decodeTopLevelUnsignedNumber(data []byte, maxValue uint64) (uint return n, nil } -func (c *codec) decodeTopLevelSignedNumber(data []byte, maxValue int64) (int64, error) { +func (c *codecForSmallInt) decodeTopLevelSigned(data []byte, maxValue int64) (int64, error) { b := twos.FromBytes(data) if !b.IsInt64() { diff --git a/abi/codecForSimpleValues_numerical_big.go b/abi/codecForSimpleValues_numerical_big.go index 67c90b5..df7f5aa 100644 --- a/abi/codecForSimpleValues_numerical_big.go +++ b/abi/codecForSimpleValues_numerical_big.go @@ -7,7 +7,10 @@ import ( twos "github.com/multiversx/mx-components-big-int/twos-complement" ) -func (c *codec) encodeNestedUnsignedBigNumber(writer io.Writer, value *big.Int) error { +type codecForBigInt struct { +} + +func (c *codecForBigInt) encodeNestedUnsigned(writer io.Writer, value *big.Int) error { data := value.Bytes() dataLength := len(data) @@ -26,7 +29,7 @@ func (c *codec) encodeNestedUnsignedBigNumber(writer io.Writer, value *big.Int) return nil } -func (c *codec) encodeNestedSignedBigNumber(writer io.Writer, value *big.Int) error { +func (c *codecForBigInt) encodeNestedSigned(writer io.Writer, value *big.Int) error { data := twos.ToBytes(value) dataLength := len(data) @@ -45,7 +48,7 @@ func (c *codec) encodeNestedSignedBigNumber(writer io.Writer, value *big.Int) er return nil } -func (c *codec) encodeTopLevelUnsignedBigNumber(writer io.Writer, value *big.Int) error { +func (c *codecForBigInt) encodeTopLevelUnsigned(writer io.Writer, value *big.Int) error { data := value.Bytes() _, err := writer.Write(data) if err != nil { @@ -55,7 +58,7 @@ func (c *codec) encodeTopLevelUnsignedBigNumber(writer io.Writer, value *big.Int return nil } -func (c *codec) encodeTopLevelSignedBigNumber(writer io.Writer, value *big.Int) error { +func (c *codecForBigInt) encodeTopLevelSigned(writer io.Writer, value *big.Int) error { data := twos.ToBytes(value) _, err := writer.Write(data) if err != nil { @@ -65,7 +68,7 @@ func (c *codec) encodeTopLevelSignedBigNumber(writer io.Writer, value *big.Int) return nil } -func (c *codec) decodeNestedUnsignedBigNumber(reader io.Reader) (*big.Int, error) { +func (c *codecForBigInt) decodeNestedUnsigned(reader io.Reader) (*big.Int, error) { // Read the length of the payload length, err := decodeLength(reader) if err != nil { @@ -81,7 +84,7 @@ func (c *codec) decodeNestedUnsignedBigNumber(reader io.Reader) (*big.Int, error return big.NewInt(0).SetBytes(data), nil } -func (c *codec) decodeNestedSignedBigNumber(reader io.Reader) (*big.Int, error) { +func (c *codecForBigInt) decodeNestedSigned(reader io.Reader) (*big.Int, error) { // Read the length of the payload length, err := decodeLength(reader) if err != nil { @@ -97,10 +100,10 @@ func (c *codec) decodeNestedSignedBigNumber(reader io.Reader) (*big.Int, error) return twos.FromBytes(data), nil } -func (c *codec) decodeTopLevelUnsignedBigNumber(data []byte) *big.Int { +func (c *codecForBigInt) decodeTopLevelUnsigned(data []byte) *big.Int { return big.NewInt(0).SetBytes(data) } -func (c *codec) decodeTopLevelSignedBigNumber(data []byte) *big.Int { +func (c *codecForBigInt) decodeTopLevelSigned(data []byte) *big.Int { return twos.FromBytes(data) } diff --git a/abi/codecForSimpleValues_numerical_big_test.go b/abi/codecForSimpleValues_numerical_big_test.go index 830ae33..987d2fd 100644 --- a/abi/codecForSimpleValues_numerical_big_test.go +++ b/abi/codecForSimpleValues_numerical_big_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestCodec_NumericalBig(t *testing.T) { +func TestCodecForBigInt(t *testing.T) { codec, _ := newCodec(argsNewCodec{ pubKeyLength: 32, }) diff --git a/abi/codecForSimpleValues_numerical_test.go b/abi/codecForSimpleValues_numerical_test.go index 64b3357..f8c4436 100644 --- a/abi/codecForSimpleValues_numerical_test.go +++ b/abi/codecForSimpleValues_numerical_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestCodec_Numerical(t *testing.T) { +func TestCodecForSmallInt(t *testing.T) { codec, _ := newCodec(argsNewCodec{ pubKeyLength: 32, }) diff --git a/abi/codecForSimpleValues_string.go b/abi/codecForSimpleValues_string.go index 59a5320..fa85f52 100644 --- a/abi/codecForSimpleValues_string.go +++ b/abi/codecForSimpleValues_string.go @@ -4,7 +4,10 @@ import ( "io" ) -func (c *codec) encodeNestedString(writer io.Writer, value StringValue) error { +type codecForString struct { +} + +func (c *codecForString) encodeNested(writer io.Writer, value StringValue) error { data := []byte(value.Value) err := encodeLength(writer, uint32(len(data))) if err != nil { @@ -15,12 +18,12 @@ func (c *codec) encodeNestedString(writer io.Writer, value StringValue) error { return err } -func (c *codec) encodeTopLevelString(writer io.Writer, value StringValue) error { +func (c *codecForString) encodeTopLevel(writer io.Writer, value StringValue) error { _, err := writer.Write([]byte(value.Value)) return err } -func (c *codec) decodeNestedString(reader io.Reader, value *StringValue) error { +func (c *codecForString) decodeNested(reader io.Reader, value *StringValue) error { length, err := decodeLength(reader) if err != nil { return err @@ -35,7 +38,7 @@ func (c *codec) decodeNestedString(reader io.Reader, value *StringValue) error { return nil } -func (c *codec) decodeTopLevelString(data []byte, value *StringValue) error { +func (c *codecForString) decodeTopLevel(data []byte, value *StringValue) error { value.Value = string(data) return nil } diff --git a/abi/codecForSimpleValues_string_test.go b/abi/codecForSimpleValues_string_test.go index c2e2e44..92e2018 100644 --- a/abi/codecForSimpleValues_string_test.go +++ b/abi/codecForSimpleValues_string_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestCodec_String(t *testing.T) { +func TestCodecForString(t *testing.T) { codec, _ := newCodec(argsNewCodec{ pubKeyLength: 32, }) From 8170aa0bf1732e4db5875ae851fa65aee9dc5302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Tue, 30 Apr 2024 17:28:24 +0300 Subject: [PATCH 24/24] Fix after review. --- abi/codecForCompositeTypes_list.go | 2 +- abi/codecForCompositeTypes_option.go | 2 +- abi/codecForCustomTypes_enum.go | 2 +- abi/codecForCustomTypes_struct.go | 2 +- abi/interface.go | 9 +++++++++ 5 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 abi/interface.go diff --git a/abi/codecForCompositeTypes_list.go b/abi/codecForCompositeTypes_list.go index 8289c3a..1baf923 100644 --- a/abi/codecForCompositeTypes_list.go +++ b/abi/codecForCompositeTypes_list.go @@ -7,7 +7,7 @@ import ( ) type codecForList struct { - generalCodec *codec + generalCodec generalCodec } func (c *codecForList) encodeNested(writer io.Writer, value InputListValue) error { diff --git a/abi/codecForCompositeTypes_option.go b/abi/codecForCompositeTypes_option.go index 2c9ea98..e64cec5 100644 --- a/abi/codecForCompositeTypes_option.go +++ b/abi/codecForCompositeTypes_option.go @@ -7,7 +7,7 @@ import ( ) type codecForOption struct { - generalCodec *codec + generalCodec generalCodec } func (c *codecForOption) encodeNested(writer io.Writer, value OptionValue) error { diff --git a/abi/codecForCustomTypes_enum.go b/abi/codecForCustomTypes_enum.go index 91c6092..5bfd29d 100644 --- a/abi/codecForCustomTypes_enum.go +++ b/abi/codecForCustomTypes_enum.go @@ -7,7 +7,7 @@ import ( ) type codecForEnum struct { - generalCodec *codec + generalCodec generalCodec } func (c *codecForEnum) encodeNested(writer io.Writer, value EnumValue) error { diff --git a/abi/codecForCustomTypes_struct.go b/abi/codecForCustomTypes_struct.go index 974f5b6..db48024 100644 --- a/abi/codecForCustomTypes_struct.go +++ b/abi/codecForCustomTypes_struct.go @@ -7,7 +7,7 @@ import ( ) type codeForStruct struct { - generalCodec *codec + generalCodec generalCodec } func (c *codeForStruct) encodeNested(writer io.Writer, value StructValue) error { diff --git a/abi/interface.go b/abi/interface.go new file mode 100644 index 0000000..3c2d9fe --- /dev/null +++ b/abi/interface.go @@ -0,0 +1,9 @@ +package abi + +import "io" + +// generalCodec is an internal interface that allows "leaf" codecs to rely on the general "composite" codec, if needed. +type generalCodec interface { + doEncodeNested(writer io.Writer, value any) error + doDecodeNested(reader io.Reader, value any) error +}