diff --git a/clients/iota-go/iotago/language_storage.go b/clients/iota-go/iotago/language_storage.go index afa33f2943..b458c56a93 100644 --- a/clients/iota-go/iotago/language_storage.go +++ b/clients/iota-go/iotago/language_storage.go @@ -197,52 +197,385 @@ func (s *StructTag) String() string { } func StructTagFromString(data string) (*StructTag, error) { - parts := strings.Split(data, "::") - address, module := parts[0], parts[1] - - rest := data[len(address)+len(module)+4:] - name := rest - if idx := strings.Index(rest, "<"); idx > 0 { - name = rest[:idx] - } - typeParams := []TypeTag{} - - if strings.Contains(rest, "<") { - typeParamsRawStr := rest[strings.Index(rest, "<")+1 : strings.LastIndex(rest, ">")] - typeParamsTokens := splitGenericParameters(typeParamsRawStr, []string{"<", ">"}) - typeParams = make([]TypeTag, len(typeParamsTokens)) - for i, token := range typeParamsTokens { - param := TypeTag{} - if !strings.Contains(token, "::") { - typeTag, err := TypeTagFromString(token) - if err != nil { - return nil, fmt.Errorf("can't parse TypeParams: %w", err) - } - param = *typeTag + parser := &structTagParser{ + input: []rune(data), + pos: 0, + } + return parser.parseStructTag() +} + +// Token types for the struct tag parser +type tokenType int + +const ( + tokenEOF tokenType = iota + tokenAddr + tokenIdent + tokenDoubleColon // :: + tokenLessThan // < + tokenGreaterThan // > + tokenComma // , + tokenPrimitive +) + +type token struct { + type_ tokenType + value string + pos int +} + +type structTagParser struct { + input []rune + pos int +} + +func (p *structTagParser) skipWhitespace() { + for p.pos < len(p.input) { + c := p.input[p.pos] + if c == ' ' || c == '\t' { + p.pos++ + } else { + break + } + } +} + +func (p *structTagParser) nextToken() (token, error) { + p.skipWhitespace() + + if p.pos >= len(p.input) { + return token{tokenEOF, "", p.pos}, nil + } + + start := p.pos + c := p.input[p.pos] + + // Check for :: first + if c == ':' && p.pos+1 < len(p.input) && p.input[p.pos+1] == ':' { + p.pos += 2 + return token{tokenDoubleColon, "::", start}, nil + } + + // Single character tokens + switch c { + case '<': + p.pos++ + return token{tokenLessThan, "<", start}, nil + case '>': + p.pos++ + return token{tokenGreaterThan, ">", start}, nil + case ',': + p.pos++ + return token{tokenComma, ",", start}, nil + } + + // Address: 0x followed by hex digits + if c == '0' && p.pos+1 < len(p.input) && (p.input[p.pos+1] == 'x' || p.input[p.pos+1] == 'X') { + p.pos += 2 // Skip 0x + hexStart := p.pos + + for p.pos < len(p.input) { + c := p.input[p.pos] + if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') { + p.pos++ } else { - typeParam, err := StructTagFromString(token) - if err != nil { - return nil, fmt.Errorf("can't parse StructTag TypeParams: %w", err) - } - param.Struct = typeParam + break } + } + + if p.pos == hexStart { + return token{}, fmt.Errorf("invalid address at position %d: expected hex digits after 0x", start) + } + + hexLen := p.pos - hexStart + if hexLen > 64 { + return token{}, fmt.Errorf("invalid address at position %d: hex part too long (%d digits, max 64)", start, hexLen) + } + + value := string(p.input[start:p.pos]) + return token{tokenAddr, value, start}, nil + } + + // Check for primitives and identifiers + if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' { + for p.pos < len(p.input) { + c := p.input[p.pos] + if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' { + p.pos++ + } else { + break + } + } - typeParams[i] = param + value := string(p.input[start:p.pos]) + + // Check if it's a primitive type + switch value { + case "bool", "u8", "u16", "u32", "u64", "u128", "u256", "address", "signer": + return token{tokenPrimitive, value, start}, nil + default: + return token{tokenIdent, value, start}, nil } } - if len(typeParams) == 0 { - typeParams = nil + return token{}, fmt.Errorf("unexpected character '%c' at position %d", c, start) +} + +func (p *structTagParser) parseStructTagCore() (*StructTag, error) { + // Address + addrToken, err := p.nextToken() + if err != nil { + return nil, err + } + if addrToken.type_ != tokenAddr { + return nil, fmt.Errorf("expected address at position %d, got %s", addrToken.pos, addrToken.value) + } + + address, err := AddressFromHex(addrToken.value) + if err != nil { + return nil, fmt.Errorf("invalid address '%s' at position %d: %w", addrToken.value, addrToken.pos, err) + } + + // First :: + dcolonToken, err := p.nextToken() + if err != nil { + return nil, err + } + if dcolonToken.type_ != tokenDoubleColon { + return nil, fmt.Errorf("expected '::' at position %d, got '%s'", dcolonToken.pos, dcolonToken.value) + } + + // Module identifier + moduleToken, err := p.nextToken() + if err != nil { + return nil, err + } + if moduleToken.type_ != tokenIdent { + return nil, fmt.Errorf("expected module identifier at position %d, got '%s'", moduleToken.pos, moduleToken.value) + } + + // Second :: + dcolonToken2, err := p.nextToken() + if err != nil { + return nil, err + } + if dcolonToken2.type_ != tokenDoubleColon { + return nil, fmt.Errorf("expected '::' at position %d, got '%s'", dcolonToken2.pos, dcolonToken2.value) + } + + // Name identifier + nameToken, err := p.nextToken() + if err != nil { + return nil, err + } + if nameToken.type_ != tokenIdent { + return nil, fmt.Errorf("expected struct name identifier at position %d, got '%s'", nameToken.pos, nameToken.value) + } + + // Optional type parameters + var typeParams []TypeTag + nextTok, err := p.nextToken() + if err != nil { + return nil, err + } + + if nextTok.type_ == tokenLessThan { + typeParams, err = p.parseTypeParams() + if err != nil { + return nil, err + } + + // final > + gtToken, err := p.nextToken() + if err != nil { + return nil, err + } + if gtToken.type_ != tokenGreaterThan { + return nil, fmt.Errorf("expected '>' at position %d, got '%s'", gtToken.pos, gtToken.value) + } + } else { + // Put back the token for the caller to consume + p.pos -= len([]rune(nextTok.value)) } return &StructTag{ - Address: MustAddressFromHex(address), - Module: module, - Name: name, + Address: address, + Module: Identifier(moduleToken.value), + Name: Identifier(nameToken.value), TypeParams: typeParams, }, nil } +func (p *structTagParser) parseStructTag() (*StructTag, error) { + // address + addrToken, err := p.nextToken() + if err != nil { + return nil, err + } + if addrToken.type_ != tokenAddr { + return nil, fmt.Errorf("expected address at position %d, got %s", addrToken.pos, addrToken.value) + } + + address, err := AddressFromHex(addrToken.value) + if err != nil { + return nil, fmt.Errorf("invalid address '%s' at position %d: %w", addrToken.value, addrToken.pos, err) + } + + // first :: + dcolonToken, err := p.nextToken() + if err != nil { + return nil, err + } + if dcolonToken.type_ != tokenDoubleColon { + return nil, fmt.Errorf("expected '::' at position %d, got '%s'", dcolonToken.pos, dcolonToken.value) + } + + // module identifier + moduleToken, err := p.nextToken() + if err != nil { + return nil, err + } + if moduleToken.type_ != tokenIdent { + return nil, fmt.Errorf("expected module identifier at position %d, got '%s'", moduleToken.pos, moduleToken.value) + } + + // second :: + dcolonToken2, err := p.nextToken() + if err != nil { + return nil, err + } + if dcolonToken2.type_ != tokenDoubleColon { + return nil, fmt.Errorf("expected '::' at position %d, got '%s'", dcolonToken2.pos, dcolonToken2.value) + } + + // name identifier + nameToken, err := p.nextToken() + if err != nil { + return nil, err + } + if nameToken.type_ != tokenIdent { + return nil, fmt.Errorf("expected struct name identifier at position %d, got '%s'", nameToken.pos, nameToken.value) + } + + // optional type parameters + var typeParams []TypeTag + nextTok, err := p.nextToken() + if err != nil { + return nil, err + } + + switch nextTok.type_ { + case tokenLessThan: + typeParams, err = p.parseTypeParams() + if err != nil { + return nil, err + } + + // final > + gtToken, err := p.nextToken() + if err != nil { + return nil, err + } + if gtToken.type_ != tokenGreaterThan { + return nil, fmt.Errorf("expected '>' at position %d, got '%s'", gtToken.pos, gtToken.value) + } + + // check for eof + eofToken, err := p.nextToken() + if err != nil { + return nil, err + } + if eofToken.type_ != tokenEOF { + return nil, fmt.Errorf("expected end of input at position %d, got '%s'", eofToken.pos, eofToken.value) + } + case tokenEOF: + // No type parameters, which is fine + default: + return nil, fmt.Errorf("expected '<' or end of input at position %d, got '%s'", nextTok.pos, nextTok.value) + } + + return &StructTag{ + Address: address, + Module: Identifier(moduleToken.value), + Name: Identifier(nameToken.value), + TypeParams: typeParams, + }, nil +} + +func (p *structTagParser) parseTypeParams() ([]TypeTag, error) { + var typeParams []TypeTag + + for { + typeTag, err := p.parseTypeTag() + if err != nil { + return nil, err + } + typeParams = append(typeParams, *typeTag) + + // Check for comma or end + nextTok, err := p.nextToken() + if err != nil { + return nil, err + } + + switch nextTok.type_ { + case tokenComma: + continue // Parse next type parameter + case tokenGreaterThan: + // Put back the > for the caller to consume + p.pos -= len(nextTok.value) + return typeParams, nil + default: + return nil, fmt.Errorf("expected ',' or '>' at position %d, got '%s'", nextTok.pos, nextTok.value) + } + } +} + +func (p *structTagParser) parseTypeTag() (*TypeTag, error) { + oldPos := p.pos + nextTok, err := p.nextToken() + if err != nil { + return nil, err + } + + if nextTok.type_ == tokenPrimitive { + // It's a primitive type + switch nextTok.value { + case "bool": + return &TypeTag{Bool: &serialization.EmptyEnum{}}, nil + case "u8": + return &TypeTag{U8: &serialization.EmptyEnum{}}, nil + case "u16": + return &TypeTag{U16: &serialization.EmptyEnum{}}, nil + case "u32": + return &TypeTag{U32: &serialization.EmptyEnum{}}, nil + case "u64": + return &TypeTag{U64: &serialization.EmptyEnum{}}, nil + case "u128": + return &TypeTag{U128: &serialization.EmptyEnum{}}, nil + case "u256": + return &TypeTag{U256: &serialization.EmptyEnum{}}, nil + case "address": + return &TypeTag{Address: &serialization.EmptyEnum{}}, nil + case "signer": + return &TypeTag{Signer: &serialization.EmptyEnum{}}, nil + default: + return nil, fmt.Errorf("unknown primitive type '%s' at position %d", nextTok.value, nextTok.pos) + } + } else if nextTok.type_ == tokenAddr { + // It's a struct tag - rewind and parse as struct + p.pos = oldPos + structTag, err := p.parseStructTagCore() + if err != nil { + return nil, err + } + return &TypeTag{Struct: structTag}, nil + } else { + return nil, fmt.Errorf("expected primitive type or address at position %d, got '%s'", nextTok.pos, nextTok.value) + } +} + +// splitGenericParameters is kept for backward compatibility with other parsing code func splitGenericParameters(str string, genericSeparators []string) []string { var left, right string if genericSeparators != nil { diff --git a/clients/iota-go/iotago/language_storage_test.go b/clients/iota-go/iotago/language_storage_test.go index 2a5a6f5111..988fb84915 100644 --- a/clients/iota-go/iotago/language_storage_test.go +++ b/clients/iota-go/iotago/language_storage_test.go @@ -1,6 +1,7 @@ package iotago_test import ( + "strings" "testing" "github.com/stretchr/testify/require" @@ -49,7 +50,7 @@ func TestTypeTagString(t *testing.T) { func TestStructTagEncoding(t *testing.T) { { - s1 := "0x2::foo::bar<0x3::baz::qux<0x4::nested::result, 0x5::funny::other>, bool>" + s1 := "0x2::foo::bar<0x3::baz::qux<0x4::nested::result, 0x5::funny::other, 0x6::more::another>, bool>" structTag, err := iotago.StructTagFromString(s1) require.NoError(t, err) @@ -71,6 +72,11 @@ func TestStructTagEncoding(t *testing.T) { require.Equal(t, iotago.Identifier("funny"), typeParam01.Module) require.Equal(t, iotago.Identifier("other"), typeParam01.Name) + typeParam02 := structTag.TypeParams[0].Struct.TypeParams[2].Struct + require.Equal(t, iotago.MustObjectIDFromHex("0x6"), typeParam02.Address) + require.Equal(t, iotago.Identifier("more"), typeParam02.Module) + require.Equal(t, iotago.Identifier("another"), typeParam02.Name) + require.NotNil(t, structTag.TypeParams[1].Bool) } @@ -93,3 +99,201 @@ func TestStructTagEncoding(t *testing.T) { require.Equal(t, iotago.Identifier("TESTCOIN"), typeParam0.Name) } } + +func TestStructTagParsingEdgeCases(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + errMsg string + }{ + // Whitespace tests + { + name: "whitespace after <", + input: "0x1::module::name< bool>", + }, + { + name: "whitespace before >", + input: "0x1::module::name", + }, + { + name: "whitespace around comma", + input: "0x1::module::name<0x2::mod::test , bool>", + }, + { + name: "multiple spaces", + input: "0x1::module::name< bool , u64 >", + }, + { + name: "tabs and spaces", + input: "0x1::module::name<\tbool\t,\tu64\t>", + }, + + // Address format tests + { + name: "short address", + input: "0x1::module::name", + }, + { + name: "long address", + input: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef::module::name", + }, + { + name: "uppercase hex", + input: "0x1234ABCD::module::name", + }, + { + name: "mixed case hex", + input: "0x1234aBcD::module::name", + }, + + // Primitive type tests + { + name: "all primitive types", + input: "0x1::module::name", + }, + + // Error cases + { + name: "missing 0x prefix", + input: "1::module::name", + wantErr: true, + errMsg: "unexpected character '1' at position 0", + }, + { + name: "invalid hex", + input: "0xgg::module::name", + wantErr: true, + errMsg: `invalid address`, + }, + { + name: "too long address", + input: "0x" + strings.Repeat("1", 65) + "::module::name", + wantErr: true, + errMsg: "invalid address", + }, + { + name: "missing double colon", + input: "0x1:module::name", + wantErr: true, + errMsg: `unexpected character ':' at position 3`, + }, + { + name: "invalid identifier", + input: "0x1::123::name", + wantErr: true, + errMsg: "unexpected character", + }, + { + name: "empty type params", + input: "0x1::module::name<>", + wantErr: true, + errMsg: `expected primitive type or address at position 18, got '>'`, + }, + { + name: "unmatched <", + input: "0x1::module::name' at position 22, got ''`, + }, + { + name: "unmatched >", + input: "0x1::module::name>", + wantErr: true, + errMsg: "expected end of input", + }, + { + name: "trailing comma", + input: "0x1::module::name", + wantErr: true, + errMsg: "expected primitive type or address at position 23, got '>'", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := iotago.StructTagFromString(tt.input) + + if tt.wantErr { + require.Error(t, err) + if tt.errMsg != "" { + require.Contains(t, err.Error(), tt.errMsg) + } + } else { + require.NoError(t, err) + require.NotNil(t, result) + } + }) + } +} + +func TestStructTagRoundTrip(t *testing.T) { + tests := []string{ + "0x1::module::name", + "0x2::foo::bar", + "0x3::test::deep<0x4::inner::struct, u32>", + "0x2::foo::bar<0x3::baz::qux<0x4::nested::result, 0x5::funny::other, 0x6::more::another>, bool>", + "0xa::very_long_module_name::VeryLongStructName<0xabcd::another::Type>", + "0x123::mod::name", + } + + for _, input := range tests { + t.Run(input, func(t *testing.T) { + // Parse the input + parsed, err := iotago.StructTagFromString(input) + require.NoError(t, err) + + // Convert back to string + strResult := parsed.String() + + // Parse again to ensure consistency + reparsed, err := iotago.StructTagFromString(strResult) + require.NoError(t, err) + + // Compare the structs field by field + require.Equal(t, parsed.Address, reparsed.Address) + require.Equal(t, parsed.Module, reparsed.Module) + require.Equal(t, parsed.Name, reparsed.Name) + require.Equal(t, len(parsed.TypeParams), len(reparsed.TypeParams)) + + // Deep comparison of type params would be more complex, + // but the String() comparison should be sufficient for the round-trip test + require.Equal(t, parsed.String(), reparsed.String()) + }) + } +} + +func TestStructTagWhitespaceVariations(t *testing.T) { + // All these variations should parse to the same canonical form + canonical := "0x1::module::name" + variations := []string{ + "0x1::module::name< bool, u64>", + "0x1::module::name", + "0x1::module::name", + "0x1::module::name< bool , u64 >", + "0x1::module::name< bool , u64 >", + "0x1::module::name<\tbool\t,\tu64\t>", + "0x1::module::name<\tbool,u64\t>", + } + + // Parse canonical form + canonicalParsed, err := iotago.StructTagFromString(canonical) + require.NoError(t, err) + canonicalStr := canonicalParsed.String() + + for _, variation := range variations { + t.Run("variation: "+variation, func(t *testing.T) { + parsed, err := iotago.StructTagFromString(variation) + require.NoError(t, err) + + // Should produce the same canonical string representation + require.Equal(t, canonicalStr, parsed.String()) + + // Should have the same structure + require.Equal(t, canonicalParsed.Address, parsed.Address) + require.Equal(t, canonicalParsed.Module, parsed.Module) + require.Equal(t, canonicalParsed.Name, parsed.Name) + require.Equal(t, len(canonicalParsed.TypeParams), len(parsed.TypeParams)) + }) + } +} diff --git a/clients/iota-go/iotago/resourcetype.go b/clients/iota-go/iotago/resourcetype.go index db5c6ea09b..3541d0e913 100644 --- a/clients/iota-go/iotago/resourcetype.go +++ b/clients/iota-go/iotago/resourcetype.go @@ -11,8 +11,7 @@ type ResourceType struct { Module Identifier ObjectName Identifier // it can be function name or struct name, etc. - SubType1 *ResourceType `bcs:"optional"` - SubType2 *ResourceType `bcs:"optional"` + SubTypes []*ResourceType `bcs:"optional"` } func IsSameResource(a, b string) (bool, error) { @@ -36,58 +35,106 @@ func MustNewResourceType(s string) *ResourceType { } func NewResourceType(str string) (*ResourceType, error) { - var err error - + // Find the generic part <...> ltIdx := strings.Index(str, "<") - var subType1, subType2 *ResourceType + var subTypes []*ResourceType + var baseStr string + if ltIdx != -1 { gtIdx := strings.LastIndex(str, ">") if gtIdx != len(str)-1 { return nil, errors.New("invalid type string literal") } - commaIdx := strings.Index(str, ",") - if commaIdx == -1 { - subType1, err = NewResourceType(str[ltIdx+1 : gtIdx]) - if err != nil { - return nil, err - } - } else { - subType1, err = NewResourceType(str[ltIdx+1 : commaIdx]) - if err != nil { - return nil, err - } - subType2, err = NewResourceType(strings.TrimSpace(str[commaIdx+1 : gtIdx])) + + // Extract the base type (before <) + baseStr = str[:ltIdx] + + genericPart := str[ltIdx+1 : gtIdx] + if genericPart == "" { + return nil, errors.New("empty generic parameters") + } + + subtypeStrs, err := splitSubtypes(genericPart) + if err != nil { + return nil, err + } + + for _, subtypeStr := range subtypeStrs { + subtype, err := NewResourceType(strings.TrimSpace(subtypeStr)) if err != nil { return nil, err } + subTypes = append(subTypes, subtype) } + } else { + baseStr = str + } + + // Parse the base type (address::module::name) + parts := strings.Split(baseStr, "::") + if len(parts) != 3 { + return nil, fmt.Errorf("invalid resource type string: %q", str) } - parts := strings.Split(str, "::") addr, err := AddressFromHex(parts[0]) if err != nil { return nil, err } - if len(parts) < 3 { - return nil, fmt.Errorf("invalid resource type string: %q", str) - } + module := parts[1] - var objectName string - if idx := strings.Index(parts[2], "<"); idx > 0 { - objectName = parts[2][:idx] - } else { - objectName = parts[2] - } + objectName := parts[2] return &ResourceType{ Address: addr, Module: module, ObjectName: objectName, - SubType1: subType1, - SubType2: subType2, + SubTypes: subTypes, }, nil } +// splitSubtypes splits a string by commas at depth 0 only +// e.g. "A, D" -> ["A", "D"] +func splitSubtypes(s string) ([]string, error) { + var result []string + var current strings.Builder + depth := 0 + + for _, r := range s { + switch r { + case '<': + depth++ + case '>': + depth-- + if depth < 0 { + return nil, errors.New("unmatched closing bracket") + } + case ',': + if depth == 0 { + part := strings.TrimSpace(current.String()) + if part == "" { + return nil, errors.New("empty subtype entry") + } + result = append(result, part) + current.Reset() + continue + } + } + current.WriteRune(r) + } + + if depth != 0 { + return nil, errors.New("unmatched brackets") + } + + part := strings.TrimSpace(current.String()) + if part == "" { + return nil, errors.New("empty subtype entry") + } + result = append(result, part) + + return result, nil +} + func (t *ResourceType) UnmarshalJSON(data []byte) error { resource, err := NewResourceType(string(data[1 : len(data)-1])) if err != nil { @@ -101,6 +148,7 @@ func (t *ResourceType) Contains(address *Address, moduleName string, funcName st if t == nil { return false } + if t.Module == moduleName && t.ObjectName == funcName { if address == nil { return true @@ -109,42 +157,46 @@ func (t *ResourceType) Contains(address *Address, moduleName string, funcName st return true } } - if t.SubType1 == nil { - return false + + for _, subType := range t.SubTypes { + if subType.Contains(address, moduleName, funcName) { + return true + } } - return t.SubType1.Contains(address, moduleName, funcName) || t.SubType2.Contains(address, moduleName, funcName) + + return false } func (t *ResourceType) String() string { - if t.SubType2 != nil { - return fmt.Sprintf( - "%v::%v::%v<%v, %v>", - t.Address.String(), - t.Module, - t.ObjectName, - t.SubType1.String(), - t.SubType1.String(), - ) - } else if t.SubType1 != nil { - return fmt.Sprintf("%v::%v::%v<%v>", t.Address.String(), t.Module, t.ObjectName, t.SubType1.String()) - } else { + if len(t.SubTypes) == 0 { return fmt.Sprintf("%v::%v::%v", t.Address.String(), t.Module, t.ObjectName) } + + var subtypeStrs []string + for _, subtype := range t.SubTypes { + subtypeStrs = append(subtypeStrs, subtype.String()) + } + + return fmt.Sprintf("%v::%v::%v<%v>", + t.Address.String(), + t.Module, + t.ObjectName, + strings.Join(subtypeStrs, ", ")) } func (t *ResourceType) ShortString() string { - if t.SubType2 != nil { - return fmt.Sprintf( - "%v::%v::%v<%v, %v>", - t.Address.ShortString(), - t.Module, - t.ObjectName, - t.SubType1.ShortString(), - t.SubType1.ShortString(), - ) - } else if t.SubType1 != nil { - return fmt.Sprintf("%v::%v::%v<%v>", t.Address.ShortString(), t.Module, t.ObjectName, t.SubType1.ShortString()) - } else { + if len(t.SubTypes) == 0 { return fmt.Sprintf("%v::%v::%v", t.Address.ShortString(), t.Module, t.ObjectName) } + + var subtypeStrs []string + for _, subtype := range t.SubTypes { + subtypeStrs = append(subtypeStrs, subtype.ShortString()) + } + + return fmt.Sprintf("%v::%v::%v<%v>", + t.Address.ShortString(), + t.Module, + t.ObjectName, + strings.Join(subtypeStrs, ", ")) } diff --git a/clients/iota-go/iotago/resourcetype_test.go b/clients/iota-go/iotago/resourcetype_test.go index 5bf1a009f9..5c91883e07 100644 --- a/clients/iota-go/iotago/resourcetype_test.go +++ b/clients/iota-go/iotago/resourcetype_test.go @@ -17,42 +17,148 @@ func TestNewResourceType(t *testing.T) { wantErr bool }{ { - name: "with array", - str: "0x2::dynamic_field::Field<0x1::ascii::String, 0x2::balance::Balance<0x2::iota::IOTA>>", + name: "no subtype", + str: "0x23::coin::Xxxx", want: &iotago.ResourceType{ - iotago.MustAddressFromHex("0x2"), "dynamic_field", "Field", - &iotago.ResourceType{ - iotago.MustAddressFromHex("0x1"), "ascii", "String", - nil, - nil, + Address: iotago.MustAddressFromHex("0x23"), + Module: "coin", + ObjectName: "Xxxx", + SubTypes: nil, + }, + }, + { + name: "one subtype", + str: "0x1::m1::f1<0x2::m2::f2>", + want: &iotago.ResourceType{ + Address: iotago.MustAddressFromHex("0x1"), + Module: "m1", + ObjectName: "f1", + SubTypes: []*iotago.ResourceType{ + { + Address: iotago.MustAddressFromHex("0x2"), + Module: "m2", + ObjectName: "f2", + SubTypes: nil, + }, }, - &iotago.ResourceType{ - iotago.MustAddressFromHex("0x2"), "balance", "Balance", - &iotago.ResourceType{ - iotago.MustAddressFromHex("0x2"), "iota", "IOTA", - nil, - nil, + }, + }, + { + name: "multiple subtypes", + str: "0x111::aaa::AAA<0x222::bbb::BBB, 0x333::ccc::CCC, 0x444::ddd::DDD>", + want: &iotago.ResourceType{ + Address: iotago.MustAddressFromHex("0x111"), + Module: "aaa", + ObjectName: "AAA", + SubTypes: []*iotago.ResourceType{ + { + Address: iotago.MustAddressFromHex("0x222"), + Module: "bbb", + ObjectName: "BBB", + SubTypes: nil, + }, + { + Address: iotago.MustAddressFromHex("0x333"), + Module: "ccc", + ObjectName: "CCC", + SubTypes: nil, + }, + { + Address: iotago.MustAddressFromHex("0x444"), + Module: "ddd", + ObjectName: "DDD", + SubTypes: nil, }, - nil, }, }, }, { - name: "sample", - str: "0x23::coin::Xxxx", - want: &iotago.ResourceType{iotago.MustAddressFromHex("0x23"), "coin", "Xxxx", nil, nil}, + name: "nested generics two levels", + str: "0x2::dynamic_field::Field<0x1::ascii::String, 0x2::balance::Balance<0x2::iota::IOTA>>", + want: &iotago.ResourceType{ + Address: iotago.MustAddressFromHex("0x2"), + Module: "dynamic_field", + ObjectName: "Field", + SubTypes: []*iotago.ResourceType{ + { + Address: iotago.MustAddressFromHex("0x1"), + Module: "ascii", + ObjectName: "String", + SubTypes: nil, + }, + { + Address: iotago.MustAddressFromHex("0x2"), + Module: "balance", + ObjectName: "Balance", + SubTypes: []*iotago.ResourceType{ + { + Address: iotago.MustAddressFromHex("0x2"), + Module: "iota", + ObjectName: "IOTA", + SubTypes: nil, + }, + }, + }, + }, + }, }, { - name: "three level", - str: "0xabc::Coin::Xxxx<0x789::AAA::ppp<0x111::mod3::func3>>", + name: "deeply nested generics", + str: "0x1::outer::O<0x2::mid::M<0x3::inner::I, 0x4::k::K>, 0x5::leaf::L>", want: &iotago.ResourceType{ - iotago.MustAddressFromHex("0xabc"), "Coin", "Xxxx", - &iotago.ResourceType{ - iotago.MustAddressFromHex("0x789"), "AAA", "ppp", - &iotago.ResourceType{iotago.MustAddressFromHex("0x111"), "mod3", "func3", nil, nil}, - nil, + Address: iotago.MustAddressFromHex("0x1"), + Module: "outer", + ObjectName: "O", + SubTypes: []*iotago.ResourceType{ + { + Address: iotago.MustAddressFromHex("0x2"), + Module: "mid", + ObjectName: "M", + SubTypes: []*iotago.ResourceType{ + { + Address: iotago.MustAddressFromHex("0x3"), + Module: "inner", + ObjectName: "I", + SubTypes: nil, + }, + { + Address: iotago.MustAddressFromHex("0x4"), + Module: "k", + ObjectName: "K", + SubTypes: nil, + }, + }, + }, + { + Address: iotago.MustAddressFromHex("0x5"), + Module: "leaf", + ObjectName: "L", + SubTypes: nil, + }, + }, + }, + }, + { + name: "whitespace tolerance", + str: "0x1::m1::f1< 0x2::m2::f2 , 0x3::m3::f3 >", + want: &iotago.ResourceType{ + Address: iotago.MustAddressFromHex("0x1"), + Module: "m1", + ObjectName: "f1", + SubTypes: []*iotago.ResourceType{ + { + Address: iotago.MustAddressFromHex("0x2"), + Module: "m2", + ObjectName: "f2", + SubTypes: nil, + }, + { + Address: iotago.MustAddressFromHex("0x3"), + Module: "m3", + ObjectName: "f3", + SubTypes: nil, + }, }, - nil, }, }, { @@ -67,17 +173,27 @@ func TestNewResourceType(t *testing.T) { }, { name: "error format2", - str: "0x1::m1::f1<<0x3::m3::f3>0x2::m2::f2>", + str: "0x1::m1::f1<0x2::m2::f2<0x3::m3::f3>", wantErr: true, }, { - name: "error format2", - str: "0x1::m1::f1<<0x3::m3::f3>0x2::m2::f2>", + name: "error format3", + str: "<0x3::m3::f3>0x1::m1::f1<0x2::m2::f2>", wantErr: true, }, { - name: "error format3", - str: "<0x3::m3::f3>0x1::m1::f1<0x2::m2::f2>", + name: "error format - empty subtype", + str: "0x1::m1::f1<0x2::m2::f2,>", + wantErr: true, + }, + { + name: "error format - empty generic", + str: "0x1::m1::f1<>", + wantErr: true, + }, + { + name: "error format - wrong parts count", + str: "0x1::m1", wantErr: true, }, } @@ -105,41 +221,71 @@ func TestContains(t *testing.T) { want bool }{ { - name: "successful, two levels", + name: "successful, matches outer type", str: "0xe87e::swap::Pool<0x2f63::testcoin::TESTCOIN>", target: &iotago.ResourceType{Module: "swap", ObjectName: "Pool"}, want: true, }, { - name: "successful, two levels, inner", + name: "successful, matches single subtype", str: "0xe87e::swap::Pool<0x2f63::testcoin::TESTCOIN>", target: &iotago.ResourceType{Module: "testcoin", ObjectName: "TESTCOIN"}, want: true, }, { - name: "successful, dynamic field 1", + name: "successful, matches first subtype in multiple subtypes", str: "0x2::dynamic_field::Field<0x1::ascii::String, 0x2::balance::Balance<0x2::iota::IOTA>>", target: &iotago.ResourceType{Module: "ascii", ObjectName: "String"}, want: true, }, { - name: "successful, dynamic field 2", + name: "successful, matches second subtype in multiple subtypes", str: "0x2::dynamic_field::Field<0x1::ascii::String, 0x2::balance::Balance<0x2::iota::IOTA>>", target: &iotago.ResourceType{Module: "balance", ObjectName: "Balance"}, want: true, }, { - name: "successful, dynamic field inner", + name: "successful, matches deeply nested subtype", str: "0x2::dynamic_field::Field<0x1::ascii::String, 0x2::balance::Balance<0x2::iota::IOTA>>", target: &iotago.ResourceType{Module: "iota", ObjectName: "IOTA"}, want: true, }, { - name: "failed, two levels", + name: "successful, matches in complex nested structure", + str: "0x111::aaa::AAA<0x222::bbb::BBB, 0x333::ccc::CCC, 0x444::ddd::DDD>", + target: &iotago.ResourceType{Module: "ccc", ObjectName: "CCC"}, + want: true, + }, + { + name: "successful, matches deeply nested type", + str: "0x1::outer::O<0x2::mid::M<0x3::inner::I, 0x4::k::K>, 0x5::leaf::L>", + target: &iotago.ResourceType{Module: "inner", ObjectName: "I"}, + want: true, + }, + { + name: "successful, with address match", + str: "0xe87e::swap::Pool<0x2f63::testcoin::TESTCOIN>", + target: &iotago.ResourceType{Address: iotago.MustAddressFromHex("0xe87e"), Module: "swap", ObjectName: "Pool"}, + want: true, + }, + { + name: "failed, wrong module name", str: "0xe87e::swap::Pool<0x2f63::testcoin::TESTCOIN>", target: &iotago.ResourceType{Module: "name", ObjectName: "Pool"}, want: false, }, + { + name: "failed, wrong address", + str: "0xe87e::swap::Pool<0x2f63::testcoin::TESTCOIN>", + target: &iotago.ResourceType{Address: iotago.MustAddressFromHex("0x1"), Module: "swap", ObjectName: "Pool"}, + want: false, + }, + { + name: "failed, not found anywhere", + str: "0x111::aaa::AAA<0x222::bbb::BBB, 0x333::ccc::CCC>", + target: &iotago.ResourceType{Module: "nonexistent", ObjectName: "NotFound"}, + want: false, + }, } for _, tt := range tests { t.Run( @@ -168,21 +314,62 @@ func TestResourceTypeShortString(t *testing.T) { want string }{ { - arg: &iotago.ResourceType{iotago.MustAddressFromHex("0x1"), "m1", "f1", nil, nil}, + name: "no subtypes", + arg: &iotago.ResourceType{ + Address: iotago.MustAddressFromHex("0x1"), + Module: "m1", + ObjectName: "f1", + SubTypes: nil, + }, want: "0x1::m1::f1", }, { + name: "nested generics", arg: &iotago.ResourceType{ - iotago.MustAddressFromHex("0x1"), "m1", "f1", - &iotago.ResourceType{ - iotago.MustAddressFromHex("2"), "m2", "f2", - &iotago.ResourceType{iotago.MustAddressFromHex("0x123abcdef"), "m3", "f3", nil, nil}, - nil, + Address: iotago.MustAddressFromHex("0x1"), + Module: "m1", + ObjectName: "f1", + SubTypes: []*iotago.ResourceType{ + { + Address: iotago.MustAddressFromHex("0x2"), + Module: "m2", + ObjectName: "f2", + SubTypes: []*iotago.ResourceType{ + { + Address: iotago.MustAddressFromHex("0x123abcdef"), + Module: "m3", + ObjectName: "f3", + SubTypes: nil, + }, + }, + }, }, - nil, }, want: "0x1::m1::f1<0x2::m2::f2<0x123abcdef::m3::f3>>", }, + { + name: "multiple subtypes", + arg: &iotago.ResourceType{ + Address: iotago.MustAddressFromHex("0x1"), + Module: "outer", + ObjectName: "Type", + SubTypes: []*iotago.ResourceType{ + { + Address: iotago.MustAddressFromHex("0x2"), + Module: "mod1", + ObjectName: "Type1", + SubTypes: nil, + }, + { + Address: iotago.MustAddressFromHex("0x3"), + Module: "mod2", + ObjectName: "Type2", + SubTypes: nil, + }, + }, + }, + want: "0x1::outer::Type<0x2::mod1::Type1, 0x3::mod2::Type2>", + }, } for _, tt := range tests { t.Run( diff --git a/clients/iota-go/iotajsonrpc/transactions.go b/clients/iota-go/iotajsonrpc/transactions.go index adf49b58cc..787e6dd39e 100644 --- a/clients/iota-go/iotajsonrpc/transactions.go +++ b/clients/iota-go/iotajsonrpc/transactions.go @@ -465,8 +465,8 @@ func (r *IotaTransactionBlockResponse) GetCreatedCoinByType(module string, coinT if err != nil { return nil, fmt.Errorf("invalid resource string: %w", err) } - if resource.Module == "coin" && resource.SubType1 != nil { - if resource.SubType1.Module == module && resource.SubType1.ObjectName == coinType { + if resource.Module == "coin" && len(resource.SubTypes) > 0 { + if resource.SubTypes[0].Module == module && resource.SubTypes[0].ObjectName == coinType { if ref != nil { return nil, fmt.Errorf("multiple created coins found for %s::%s: first = %v, second = %v", module, coinType, @@ -510,8 +510,8 @@ func (r *IotaTransactionBlockResponse) GetMutatedCoinByType(module string, coinT if err != nil { return nil, fmt.Errorf("invalid resource string: %w", err) } - if resource.Module == "coin" && resource.SubType1 != nil { - if resource.SubType1.Module == module && resource.SubType1.ObjectName == coinType { + if resource.Module == "coin" && len(resource.SubTypes) > 0 { + if resource.SubTypes[0].Module == module && resource.SubTypes[0].ObjectName == coinType { if ref != nil { return nil, fmt.Errorf("multiple mutated coins found for %s::%s: first = %v, second = %v", module, coinType, diff --git a/clients/iscmove/isc_test.go b/clients/iscmove/isc_test.go index 0d43f16681..4143a94c4c 100644 --- a/clients/iscmove/isc_test.go +++ b/clients/iscmove/isc_test.go @@ -58,7 +58,7 @@ func TestISCCodec(t *testing.T) { Assets: *iscmove.NewAssets(123456). SetCoin(iotajsonrpc.MustCoinTypeFromString("0x1::a::A"), 100). AddObject(*iotatest.TestAddress, iotago.MustTypeFromString("0x2::a::B")), - }, "17fd55be42d7") + }, "06caa3333f80") } func TestUnmarshalBCS(t *testing.T) { diff --git a/clients/iscmove/iscmoveclient/client_assets_bag_test.go b/clients/iscmove/iscmoveclient/client_assets_bag_test.go index 27a2a56bbe..e6617d0866 100644 --- a/clients/iscmove/iscmoveclient/client_assets_bag_test.go +++ b/clients/iscmove/iscmoveclient/client_assets_bag_test.go @@ -79,7 +79,7 @@ func TestAssetsBagPlaceCoin(t *testing.T) { coinResource, err := iotago.NewResourceType(*getCoinRef.Data.Type) require.NoError(t, err) - testCointype, err := iotajsonrpc.CoinTypeFromString(coinResource.SubType1.String()) + testCointype, err := iotajsonrpc.CoinTypeFromString(coinResource.SubTypes[0].String()) require.NoError(t, err) _, err = PTBTestWrapper( @@ -125,7 +125,7 @@ func TestAssetsBagPlaceCoinAmount(t *testing.T) { coinResource, err := iotago.NewResourceType(*getCoinRef.Data.Type) require.NoError(t, err) - testCointype, err := iotajsonrpc.CoinTypeFromString(coinResource.SubType1.String()) + testCointype, err := iotajsonrpc.CoinTypeFromString(coinResource.SubTypes[0].String()) require.NoError(t, err) _, err = PTBTestWrapper( @@ -240,7 +240,7 @@ func TestGetAssetsBagFromAssetsBagID(t *testing.T) { coinResource, err := iotago.NewResourceType(*getCoinRef.Data.Type) require.NoError(t, err) - testCointype, err := iotajsonrpc.CoinTypeFromString(coinResource.SubType1.String()) + testCointype, err := iotajsonrpc.CoinTypeFromString(coinResource.SubTypes[0].String()) require.NoError(t, err) _, err = PTBTestWrapper( @@ -289,7 +289,7 @@ func TestGetAssetsBagFromAnchorID(t *testing.T) { coinResource, err := iotago.NewResourceType(*getCoinRef.Data.Type) require.NoError(t, err) - testCointype, err := iotajsonrpc.CoinTypeFromString(coinResource.SubType1.String()) + testCointype, err := iotajsonrpc.CoinTypeFromString(coinResource.SubTypes[0].String()) require.NoError(t, err) borrowAnchorAssetsAndPlaceCoin( @@ -414,7 +414,7 @@ func TestGetAssetsBagFromRequestID(t *testing.T) { coinResource, err := iotago.NewResourceType(*getCoinRef.Data.Type) require.NoError(t, err) - testCointype, err := iotajsonrpc.CoinTypeFromString(coinResource.SubType1.String()) + testCointype, err := iotajsonrpc.CoinTypeFromString(coinResource.SubTypes[0].String()) require.NoError(t, err) txnResponse, err := newAssetsBag(client, cryptolibSigner) diff --git a/clients/iscmove/iscmoveclient/client_request_test.go b/clients/iscmove/iscmoveclient/client_request_test.go index 56a3e97674..9e08004df0 100644 --- a/clients/iscmove/iscmoveclient/client_request_test.go +++ b/clients/iscmove/iscmoveclient/client_request_test.go @@ -171,7 +171,7 @@ func TestCreateAndSendRequest(t *testing.T) { coinResource, assetErr := iotago.NewResourceType(*getCoinRef.Data.Type) require.NoError(t, assetErr) - testCointype, assetErr := iotajsonrpc.CoinTypeFromString(coinResource.SubType1.String()) + testCointype, assetErr := iotajsonrpc.CoinTypeFromString(coinResource.SubTypes[0].String()) require.NoError(t, assetErr) ref := getCoinRef.Data.Ref() _, assetErr = PTBTestWrapper( @@ -237,7 +237,7 @@ func TestCreateAndSendRequest(t *testing.T) { coinResource, assetErr := iotago.NewResourceType(*getCoinRef.Data.Type) require.NoError(t, assetErr) - testCointype, assetErr := iotajsonrpc.CoinTypeFromString(coinResource.SubType1.String()) + testCointype, assetErr := iotajsonrpc.CoinTypeFromString(coinResource.SubTypes[0].String()) require.NoError(t, assetErr) ref := getCoinRef.Data.Ref() _, assetErr = PTBTestWrapper( diff --git a/packages/chain/mempool/distsync/msg_share_request_test.go b/packages/chain/mempool/distsync/msg_share_request_test.go index 3644841afb..9b27b9a2a3 100644 --- a/packages/chain/mempool/distsync/msg_share_request_test.go +++ b/packages/chain/mempool/distsync/msg_share_request_test.go @@ -62,6 +62,6 @@ func TestMsgShareRequestSerialization(t *testing.T) { req, } - bcs.TestCodecAndHash(t, msg, "113393f61482") + bcs.TestCodecAndHash(t, msg, "aac151a3dc18") } } diff --git a/packages/isc/assets_test.go b/packages/isc/assets_test.go index e18af56314..0f8974a3a7 100644 --- a/packages/isc/assets_test.go +++ b/packages/isc/assets_test.go @@ -45,7 +45,7 @@ func TestAssetsSerialization(t *testing.T) { AddBaseTokens(42). AddCoin(coin.MustTypeFromString("0xa1::a::A"), 100). AddObject(isc.NewIotaObject(iotago.ObjectID{1, 2, 3}, iotago.MustTypeFromString("0xa1::c::C"))) - bcs.TestCodecAndHash(t, assets, "1d7bc26ebfeb") + bcs.TestCodecAndHash(t, assets, "1c9aa250386c") rwutil.BytesTest(t, assets, isc.AssetsFromBytes) } @@ -116,12 +116,12 @@ func TestAssetsCodec(t *testing.T) { AddBaseTokens(42). AddCoin(coin.MustTypeFromString("0xa1::a::A"), 100). AddObject(isc.NewIotaObject(*iotatest.TestAddress, iotago.MustTypeFromString("0xa1::c::C"))) - bcs.TestCodecAndHash(t, assets, "d005fba295b6") + bcs.TestCodecAndHash(t, assets, "252b32db3752") } func TestCoinBalancesCodec(t *testing.T) { coinBalance := isc.NewCoinBalances(). Set(coin.MustTypeFromString("0xa1::a::A"), 100). Set(coin.MustTypeFromString("0xa2::b::B"), 200) - bcs.TestCodecAndHash(t, coinBalance, "9d070cb05d31") + bcs.TestCodecAndHash(t, coinBalance, "9c48f7195149") } diff --git a/packages/isc/request_test.go b/packages/isc/request_test.go index 65bc216391..d201f7900e 100644 --- a/packages/isc/request_test.go +++ b/packages/isc/request_test.go @@ -131,7 +131,7 @@ func TestRequestDataSerialization(t *testing.T) { onledgerReq.Object.AssetsBag.AssetsBag = iscmovetest.TestAssetsBag req, err = isc.OnLedgerFromMoveRequest(&onledgerReq, cryptolib.TestAddress) require.NoError(t, err) - bcs.TestCodecAndHash(t, isc.Request(req), "5e4b3106e265") + bcs.TestCodecAndHash(t, isc.Request(req), "8d6536ddeb04") }) } diff --git a/packages/vm/core/blocklog/blocklog_test.go b/packages/vm/core/blocklog/blocklog_test.go index ce4e6bdf46..bc71ac2c94 100644 --- a/packages/vm/core/blocklog/blocklog_test.go +++ b/packages/vm/core/blocklog/blocklog_test.go @@ -154,7 +154,7 @@ func TestGetEventsInternal(t *testing.T) { func TestBlockInfoMarshalling(t *testing.T) { t.Run("v0", func(t *testing.T) { - const v0hex = "002a00000000b421501f01000000640000000000000000000000000000000100000000000000000000000000000001000000000000000000000000000000e80300000000000000000000000000009edb91da930100000000000000000000005c2605000000000000000000000000000000000000000000000000000000000000000000000000000000000000000204696f746104494f5441000004496f746104494f544104494f54410f687474703a2f2f696f74612e6f726709e0afdabfb6f592bd8a010a0008000200e807f403" + const v0hex = "002a00000000b421501f01000000640000000000000000000000000000000100000000000000000000000000000001000000000000000000000000000000e80300000000000000000000000000009edb91da930100000000000000000000005c2605000000000000000000000000000000000000000000000000000000000000000000000000000000000000000204696f746104494f54410004496f746104494f544104494f54410f687474703a2f2f696f74612e6f726709e0afdabfb6f592bd8a010a0008000200e807f403" var blockIndex uint32 = 42 expected := &BlockInfo{ SchemaVersion: blockInfoSchemaVersion0,