umbracle/ethgo

ABI encodeFunctionCall

maxence-charriere opened this issue · 9 comments

Hello,
Could it be possible to get the web3 encodeFunctionCall?

=> https://web3js.readthedocs.io/en/v1.5.2/web3-eth-abi.html#encodefunctioncall

I tried to make something but I did not completely get how to format dynamic types.
It would be very helpful if you can provide an out-of-the-box function that does this.

I was actually able to get what I wanted with this:

func ContractFunc(def string, args ...interface{}) (string, error) {
	var b strings.Builder

	m, err := abi.NewMethod(def)
	if err != nil {
		return "", errors.New("parsing method failed").
			Tag("definition", def).
			Wrap(err)
	}

	b.WriteString(fmt.Sprintf("0x%x", m.ID()))

	s, err := m.Inputs.Encode(args)
	if err != nil {
		return "", errors.New("encoding arguments failed").
			Tag("definition", def).
			Wrap(err)
	}
	b.WriteString(fmt.Sprintf("%x", s))

	return b.String(), nil
}

Thanks for this awesome package!

I will open the issue again because I think it is a good idea to include a helper method in Method to make the encoding and decoding, something like this:

m, err := abi.NewMethod()
input, err := m.Encode(args)
...
val, err := m.Decode(output)

After being able to format properly a method call, I tried to decode a result.

ABI function:

function getTileIdsByPublisher(address publisher) view returns (uint256[])

Result of the function:

0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003

My code to decode the return value:

func DecodeContractFuncResult(def string, data string) (interface{}, error) {
	m, err := abi.NewMethod(def)
	if err != nil {
		return nil, errors.New("parsing method failed").
			Tag("definition", def).
			Wrap(err)
	}

	res, err := m.Outputs.Decode([]byte(data))
	if err != nil {
		return nil, errors.New("decoding method result failed").
			Tag("method", def).
			Wrap(err)
	}
	return res, nil
}

I got this error:

decoding method result failed:
        method: function getTileIdsByPublisher(address publisher) view returns (uint256[])
        error:  offset larger than int64: 3472328296227680304

@ferranbt Any idea why this fails?

It looks like the return data from the smart contract do not correspond to the output (uint256[]).

It does, I unit tested it with javascript. Also got it from a javascript client.

Hey @ferranbt, I made a unit test to compare what the smart contract is returning vs what type encode for uint256[] returns:

func TestEncodeWithType(t *testing.T) {
	utests := []struct {
		scenario string
		typ      string
		val      interface{}
		expected string
	}{
		{
			scenario: "empty uint256[]",
			typ:      "uint256[]",
			val:      []uint{},
			expected: "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
		},
		{
			scenario: "uint256[] with 1 element",
			typ:      "uint256[]",
			val:      []uint{1},
			expected: "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001",
		},
	}

	for _, u := range utests {
		t.Run(u.scenario, func(t *testing.T) {
			typ, err := abi.NewType(u.typ)
			require.NoError(t, err)

			data, err := typ.Encode(u.val)
			require.NoError(t, err)
			require.Equal(t, u.expected, fmt.Sprintf("0x%x", data))
		})
	}
}

Output:

▶ go test
--- FAIL: TestEncodeWithType (0.00s)
    --- FAIL: TestEncodeWithType/empty_uint256[] (0.00s)
        contract_test.go:93:
            	Error Trace:	contract_test.go:93
            	Error:      	Not equal:
            	            	expected: "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"
            	            	actual  : "0x0000000000000000000000000000000000000000000000000000000000000000"

            	            	Diff:
            	            	--- Expected
            	            	+++ Actual
            	            	@@ -1 +1 @@
            	            	-0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000
            	            	+0x0000000000000000000000000000000000000000000000000000000000000000
            	Test:       	TestEncodeWithType/empty_uint256[]
    --- FAIL: TestEncodeWithType/uint256[]_with_1_element (0.00s)
        contract_test.go:93:
            	Error Trace:	contract_test.go:93
            	Error:      	Not equal:
            	            	expected: "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001"
            	            	actual  : "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001"

            	            	Diff:
            	            	--- Expected
            	            	+++ Actual
            	            	@@ -1 +1 @@
            	            	-0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001
            	            	+0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001
            	Test:       	TestEncodeWithType/uint256[]_with_1_element
FAIL
exit status 1
FAIL	github.com/maxence-charriere/nftile/pkg/eth	0.295s

Look like the one returned by the smart contract has some header data.

Note that I was able to decode the smart contract function return with web3js decodeParameter and retrieve the expected array.

The solidity version I use is 0.8.4.

I also made a test to try encoding from a method and noticed it was failing:

func TestEncodeFromMethod(t *testing.T) {
	utests := []struct {
		scenario string
		val      []uint
	}{
		{
			scenario: "encode empty slice",
			val:      []uint{},
		},
		{
			scenario: "encode slice with 1 element",
			val:      []uint{1},
		},
	}

	for _, u := range utests {
		t.Run(u.scenario, func(t *testing.T) {
			f := "function getTileIdsByPublisher(address publisher) view returns (uint256[])"
			m, err := abi.NewMethod(f)
			require.NoError(t, err)

			_, err = m.Outputs.Encode([]uint{1})
			require.NoError(t, err)
		})
	}
}

Output:

--- FAIL: TestEncodeFromMethod (0.00s)
    --- FAIL: TestEncodeFromMethod/encode_empty_slice (0.00s)
        contract_test.go:120:
            	Error Trace:	contract_test.go:120
            	Error:      	Received unexpected error:
            	            	failed to encode uint as Slice
            	Test:       	TestEncodeFromMethod/encode_empty_slice
    --- FAIL: TestEncodeFromMethod/encode_slice_with_1_element (0.00s)
        contract_test.go:120:
            	Error Trace:	contract_test.go:120
            	Error:      	Received unexpected error:
            	            	failed to encode uint as Slice
            	Test:       	TestEncodeFromMethod/encode_slice_with_1_element
FAIL
exit status 1
FAIL	github.com/maxence-charriere/nftile/pkg/eth	2.366s

Look like the one returned by the smart contract has some header data.

I think your input is "tuple(uint256[])" while the other one is "uint256[]", the tuple encoding is the extra header.

I also made a test to try encoding from a method and noticed it was failing:

For "uint256" you have to use big.Int library. Though I will open an issue to use also any other uint type as well if this is a confusion.

This library uses integration tests with real smart contracts deployed in a local network to validate the abi encodings. I just added uint256[] to the tests and it works fine:
https://github.com/umbracle/go-web3/blob/fix-encode-uint256-slice/abi/encoding_test.go#L126

This is the smart contract it tests against:

pragma solidity ^0.5.5;
pragma experimental ABIEncoderV2;

contract Sample {
	// structs
	
	function set(uint256[] memory arg0) public view returns (uint256[] memory) {
		return (arg0);
	}
}

Reopening to track the []uint256 issue.