Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions abi/abi_builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package abi

import (
"fmt"
"math/big"
"reflect"
"strings"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
)

// BuildABIFields constructs ABI ArgumentMarshaling slice from a struct type using reflection
// It uses the "abiarg" tag to determine field names and optionally types
// Tag format: `abiarg:"fieldName"` or `abiarg:"fieldName,type"`
// If type is omitted, it will be inferred from the Go type
func BuildABIFields(structType any) ([]abi.ArgumentMarshaling, error) {
t := reflect.TypeOf(structType)
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return nil, fmt.Errorf("expected struct type, got %v", t.Kind())
}

fields := make([]abi.ArgumentMarshaling, 0, t.NumField())

for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
abiTag := field.Tag.Get("abiarg")
if abiTag == "" {
continue // Skip fields without abiarg tag
}

parts := strings.Split(abiTag, ",")
name := parts[0]

var abiType string
if len(parts) > 1 {
// Explicit type from tag
abiType = parts[1]
} else {
// Infer type from Go type
inferredType, err := inferABIType(field.Type)
if err != nil {
return nil, fmt.Errorf("field %s: %w", field.Name, err)
}
abiType = inferredType
}

fields = append(fields, abi.ArgumentMarshaling{
Name: name,
Type: abiType,
})
}

return fields, nil
}

// inferABIType automatically maps Go types to Solidity ABI types
func inferABIType(goType reflect.Type) (string, error) {
// Handle special types first (before checking Kind)
switch goType {
case reflect.TypeOf(common.Address{}):
return "address", nil
case reflect.TypeOf(&big.Int{}), reflect.TypeOf(big.Int{}):
// Default to uint256 for big.Int, but can be overridden with explicit tag
return "uint256", nil
case reflect.TypeOf(common.Hash{}):
return "bytes32", nil
}

switch goType.Kind() {
case reflect.Uint8:
return "uint8", nil
case reflect.Uint16:
return "uint16", nil
case reflect.Uint32:
return "uint32", nil
case reflect.Uint64:
return "uint64", nil
case reflect.Int8:
return "int8", nil
case reflect.Int16:
return "int16", nil
case reflect.Int32:
return "int32", nil
case reflect.Int64:
return "int64", nil
case reflect.Bool:
return "bool", nil
case reflect.String:
return "string", nil
case reflect.Slice:
if goType.Elem().Kind() == reflect.Uint8 {
return "bytes", nil
}
return "", fmt.Errorf("unsupported slice type: %v", goType)
case reflect.Array:
if goType.Elem().Kind() == reflect.Uint8 {
return fmt.Sprintf("bytes%d", goType.Len()), nil
}
return "", fmt.Errorf("unsupported array type: %v", goType)
}

return "", fmt.Errorf("unsupported type: %v", goType)
}
107 changes: 107 additions & 0 deletions abi/abi_builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package abi

import (
"math/big"
"testing"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)

func TestBuildABIFields(t *testing.T) {
type TestStruct struct {
Field1 uint8 `abiarg:"field1"`
Field2 uint32 `abiarg:"field2"`
Field3 common.Address `abiarg:"field3"`
Field4 *big.Int `abiarg:"field4,uint256"`
Field5 []byte `abiarg:"field5"`
Field6 string // No tag, should be skipped
}

fields, err := BuildABIFields(TestStruct{})
require.NoError(t, err)
require.Len(t, fields, 5)

expected := []abi.ArgumentMarshaling{
{Name: "field1", Type: "uint8"},
{Name: "field2", Type: "uint32"},
{Name: "field3", Type: "address"},
{Name: "field4", Type: "uint256"},
{Name: "field5", Type: "bytes"},
}

require.Equal(t, expected, fields)
}

func TestBuildABIFields_TypeInference(t *testing.T) {
type TestStruct struct {
Uint8Field uint8 `abiarg:"uint8Field"`
Uint16Field uint16 `abiarg:"uint16Field"`
Uint32Field uint32 `abiarg:"uint32Field"`
Uint64Field uint64 `abiarg:"uint64Field"`
BoolField bool `abiarg:"boolField"`
StringField string `abiarg:"stringField"`
BytesField []byte `abiarg:"bytesField"`
AddressField common.Address `abiarg:"addressField"`
HashField common.Hash `abiarg:"hashField"`
BigIntField *big.Int `abiarg:"bigIntField"` // Inferred as uint256
BigIntExplict *big.Int `abiarg:"bigIntExplict,uint128"`
}

fields, err := BuildABIFields(TestStruct{})
require.NoError(t, err)
require.Len(t, fields, 11)

expected := []abi.ArgumentMarshaling{
{Name: "uint8Field", Type: "uint8"},
{Name: "uint16Field", Type: "uint16"},
{Name: "uint32Field", Type: "uint32"},
{Name: "uint64Field", Type: "uint64"},
{Name: "boolField", Type: "bool"},
{Name: "stringField", Type: "string"},
{Name: "bytesField", Type: "bytes"},
{Name: "addressField", Type: "address"},
{Name: "hashField", Type: "bytes32"},
{Name: "bigIntField", Type: "uint256"},
{Name: "bigIntExplict", Type: "uint128"},
}

require.Equal(t, expected, fields)
}

func TestBuildABIFields_ErrorCases(t *testing.T) {
t.Run("non-struct type", func(t *testing.T) {
_, err := BuildABIFields(42)
require.Error(t, err)
require.Contains(t, err.Error(), "expected struct type")
})

t.Run("unsupported field type", func(t *testing.T) {
type BadStruct struct {
InvalidField map[string]string `abiarg:"invalid"`
}
_, err := BuildABIFields(BadStruct{})
require.Error(t, err)
require.Contains(t, err.Error(), "unsupported type")
})
}

func TestBuildABIFields_WithPointer(t *testing.T) {
type TestStruct struct {
Field1 uint8 `abiarg:"field1"`
Field2 uint32 `abiarg:"field2"`
}

// Test with pointer to struct
fields, err := BuildABIFields(&TestStruct{})
require.NoError(t, err)
require.Len(t, fields, 2)

expected := []abi.ArgumentMarshaling{
{Name: "field1", Type: "uint8"},
{Name: "field2", Type: "uint32"},
}

require.Equal(t, expected, fields)
}
61 changes: 61 additions & 0 deletions abi/abi_decode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package abi

import (
"errors"
"fmt"
"reflect"

"github.com/ethereum/go-ethereum/accounts/abi"
)

// DecodeABIEncodedStructArray is a generic helper that decodes ABI-encoded tuple array
// It handles the ABI unpacking and type conversion boilerplate
func DecodeABIEncodedStructArray[T any](
encodedBytes []byte,
converter func(any) (T, error),
) ([]T, error) {
if len(encodedBytes) == 0 {
return nil, errors.New("encoded bytes are empty")
}

var item T
abiFields, err := BuildABIFields(item)
if err != nil {
return nil, fmt.Errorf("failed to build ABI fields: %w", err)
}

arrayType, err := abi.NewType("tuple[]", "", abiFields)
if err != nil {
return nil, fmt.Errorf("failed to create array type: %w", err)
}

args := abi.Arguments{{Type: arrayType, Name: "data"}}

unpacked, err := args.Unpack(encodedBytes)
if err != nil {
return nil, fmt.Errorf("failed to unpack data: %w", err)
}

if len(unpacked) == 0 {
return nil, errors.New("unpacked data is empty")
}

// The unpacked[0] contains the slice, but we need to extract it via reflection
// since the ABI library returns anonymous structs
val := reflect.ValueOf(unpacked[0])
if val.Kind() != reflect.Slice {
return nil, fmt.Errorf("expected slice, got %v", val.Kind())
}

result := make([]T, val.Len())
for i := 0; i < val.Len(); i++ {
item := val.Index(i).Interface()
converted, err := converter(item)
if err != nil {
return nil, fmt.Errorf("failed to convert item %d: %w", i, err)
}
result[i] = converted
}

return result, nil
}
Loading
Loading