Skip to content

Commit

Permalink
Add new type parse from string (#114)
Browse files Browse the repository at this point in the history
* Add new type parse from string

* Add link
  • Loading branch information
ferranbt authored Dec 6, 2023
1 parent 3429ee5 commit b4d0ec6
Show file tree
Hide file tree
Showing 2 changed files with 217 additions and 0 deletions.
123 changes: 123 additions & 0 deletions accounts/abi/type2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package abi

import (
"fmt"
"reflect"
"strings"
)

func NewTypeFromString(s string) (Type, error) {
if strings.HasPrefix(s, "tuple") {
sig, args, err := newTypeForTuple(s)
if err != nil {
return Type{}, err
}
return NewType(sig, "", args)
}
return NewType(s, "", nil)
}

func (t Type) Pack(v interface{}) ([]byte, error) {
return t.pack(reflect.ValueOf(v))
}

func (t Type) Unpack(data []byte, obj interface{}) error {
fmt.Println(toGoType(0, t, data))

return nil
}

// newTypeForTuple implements the format described in https://blog.ricmoo.com/human-readable-contract-abis-in-ethers-js-141902f4d917
func newTypeForTuple(s string) (string, []ArgumentMarshaling, error) {
if !strings.HasPrefix(s, "tuple") {
return "", nil, fmt.Errorf("'tuple' prefix not found")
}

pos := strings.Index(s, "(")
if pos == -1 {
return "", nil, fmt.Errorf("not a tuple, '(' not found")
}

// this is the type of the tuple. It can either be
// tuple, tuple[] or tuple[x]
sig := s[:pos]
s = s[pos:]

// Now, decode the arguments of the tuple
// tuple(arg1, arg2, tuple(arg3, arg4)).
// We need to find the commas that are not inside a nested tuple.
// We do this by keeping a counter of the number of open parens.

var (
parenthesisCount int
fields []string
)

lastComma := 1
for indx, c := range s {
switch c {
case '(':
parenthesisCount++
case ')':
parenthesisCount--
if parenthesisCount == 0 {
fields = append(fields, s[lastComma:indx])

// this should be the end of the tuple
if indx != len(s)-1 {
return "", nil, fmt.Errorf("invalid tuple, it does not end with ')'")
}
}
case ',':
if parenthesisCount == 1 {
fields = append(fields, s[lastComma:indx])
lastComma = indx + 1
}
}
}

// trim the args of spaces
for i := range fields {
fields[i] = strings.TrimSpace(fields[i])
}

// decode the type of each field
var args []ArgumentMarshaling
for _, field := range fields {
// anonymous fields are not supported so the first
// string should be the identifier of the field.

spacePos := strings.Index(field, " ")
if spacePos == -1 {
return "", nil, fmt.Errorf("invalid tuple field name not found '%s'", field)
}

name := field[:spacePos]
field = field[spacePos+1:]

if strings.HasPrefix(field, "tuple") {
// decode a recursive tuple
sig, elems, err := newTypeForTuple(field)
if err != nil {
return "", nil, err
}
args = append(args, ArgumentMarshaling{
Name: name,
Type: sig,
Components: elems,
})
} else {
// basic type. Try to decode it to see
// if it is a correct abi type.
if _, err := NewType(field, "", nil); err != nil {
return "", nil, fmt.Errorf("invalid tuple basic field '%s': %v", field, err)
}
args = append(args, ArgumentMarshaling{
Name: name,
Type: field,
})
}
}

return sig, args, nil
}
94 changes: 94 additions & 0 deletions accounts/abi/type2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package abi

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestNewType2FromString(t *testing.T) {
type struct1 struct {
A uint64
B []struct {
X struct {
C uint64
}
D uint64
}
E struct {
F uint64
G uint64
}
}

cases := []struct {
input string
want string
obj interface{}
}{
{
"uint64[]",
"uint64[]",
[]uint64{1, 2, 3},
},
{
"tuple(a uint64, b uint64)",
"(uint64,uint64)",
&struct {
A uint64
B uint64
}{A: 1, B: 2},
},
{
"tuple(a uint64, b tuple(c uint64), d uint64)",
"(uint64,(uint64),uint64)",
&struct {
A uint64
B struct {
C uint64
}
D uint64
}{A: 1, B: struct{ C uint64 }{C: 2}, D: 3},
},
{
"tuple(a uint64, b tuple[](x tuple(c uint64), d uint64), e tuple(f uint64, g uint64))",
"(uint64,((uint64),uint64)[],(uint64,uint64))",
&struct1{
A: 1,
B: []struct {
X struct {
C uint64
}
D uint64
}{
{
X: struct {
C uint64
}{C: 2},
D: 3,
},
{
X: struct {
C uint64
}{C: 4},
D: 5,
},
},
E: struct {
F uint64
G uint64
}{F: 6, G: 7},
},
},
}

for _, c := range cases {
typ, err := NewTypeFromString(c.input)

require.NoError(t, err)
require.Equal(t, c.want, typ.String())

_, err = typ.Pack(c.obj)
require.NoError(t, err)
}
}

0 comments on commit b4d0ec6

Please sign in to comment.