diff --git a/api/logic/evm.go b/api/logic/evm.go index d451aee7e..df59ac222 100644 --- a/api/logic/evm.go +++ b/api/logic/evm.go @@ -2,11 +2,42 @@ package logic import ( "fmt" + "reflect" "github.com/ethereum/go-ethereum/accounts/abi" ethCommon "github.com/ethereum/go-ethereum/common" ) +// evmPreMarshal converts v to a type that gives us the JSON serialization that we like: +// - large integers are JSON strings instead of JSON numbers +// - byte array types are JSON strings of base64 instead of JSON arrays of numbers +func evmPreMarshal(v interface{}, t abi.Type) interface{} { + switch t.T { + case abi.IntTy, abi.UintTy: + if t.Size > 32 { + return fmt.Sprint(v) + } + case abi.SliceTy, abi.ArrayTy: + rv := reflect.ValueOf(v) + slice := make([]interface{}, 0, rv.Len()) + for i := 0; i < rv.Len(); i++ { + slice = append(slice, evmPreMarshal(rv.Index(i).Interface(), *t.Elem)) + } + return slice + case abi.TupleTy: + rv := reflect.ValueOf(v) + m := map[string]interface{}{} + for i, fieldName := range t.TupleRawNames { + m[fieldName] = evmPreMarshal(rv.Field(i).Interface(), *t.TupleElems[i]) + } + case abi.FixedBytesTy, abi.FunctionTy: + c := reflect.New(t.GetType()).Elem() + c.Set(reflect.ValueOf(v)) + return c.Bytes() + } + return v +} + func EVMParseData(data []byte, contractABI *abi.ABI) (*abi.Method, []interface{}, error) { if len(data) < 4 { return nil, nil, fmt.Errorf("data (%dB) too short to have method ID", len(data)) diff --git a/api/logic/evm_test.go b/api/logic/evm_test.go index da6848671..5a85d3b04 100644 --- a/api/logic/evm_test.go +++ b/api/logic/evm_test.go @@ -1,16 +1,62 @@ package logic import ( + _ "embed" "encoding/hex" + "encoding/json" "math/big" "testing" + "github.com/ethereum/go-ethereum/accounts/abi" ethCommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" "github.com/oasisprotocol/oasis-indexer/analyzer/evmabi" ) +//go:embed test_contracts/artifacts/Varied.json +var artifactVariedJSON []byte +var Varied *abi.ABI + +func init() { + type artifact struct { + ABI *abi.ABI + } + var artifactVaried artifact + if err := json.Unmarshal(artifactVariedJSON, &artifactVaried); err != nil { + panic(err) + } + Varied = artifactVaried.ABI +} + +func TestEVMParseTypes(t *testing.T) { + data, err := hex.DecodeString("a85acffaffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010101010101010101010101010101010101010101010101010101010101010101000000000000000000000000010101010101010101010101010101010101010101010101010101010101010101010101010101010202020200000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000016100000000000000000000000000000000000000000000000000000000000000") + require.NoError(t, err) + method, args, err := EVMParseData(data, Varied) + require.Equal(t, Varied.Methods["test"], *method) + jsonExpected := []string{ + "-1", // int8 + "1", // uint8 + "\"-1\"", // int256 + "\"1\"", // uint256 + "true", // bool + "\"AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=\"", // bytes32 + "\"0x0101010101010101010101010101010101010101\"", // address + "\"AQEBAQEBAQEBAQEBAQEBAQEBAQECAgIC\"", // function (uint16) external returns (uint16) + "[1,1]", // uint16[2] + "\"AQ==\"", // bytes + "\"a\"", // string + "[1]", // uint16[] + "{\"n\":1,\"s\":\"a\"}", // O + } + for i, input := range method.Inputs { + transducedArg := evmPreMarshal(args[i], input.Type) + jsonBytesArg, err1 := json.Marshal(transducedArg) + require.NoError(t, err1) + require.Equal(t, jsonExpected[i], string(jsonBytesArg)) + } +} + func TestEVMParseData(t *testing.T) { // https://explorer.emerald.oasis.dev/tx/0x1ac7521df4cda38c87cff56b1311ee9362168bd794230415a37f2aff3a554a5f/internal-transactions data, err := hex.DecodeString("095ea7b3000000000000000000000000250d48c5e78f1e85f7ab07fec61e93ba703ae6680000000000000000000000000000000000000000000000003782dace9d900000") diff --git a/api/logic/test_contracts/Varied.sol b/api/logic/test_contracts/Varied.sol new file mode 100644 index 000000000..74458f03c --- /dev/null +++ b/api/logic/test_contracts/Varied.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.18; + +struct O { + uint16 n; + string s; +} + +interface Varied { + function test( + int8 i8, + uint8 u8, + int256 i, + uint256 u, + bool b, + // not supported in go-ethereum + // fixed128x18 f18, + // ufixed128x18 uf18, + bytes32 b32, + address a, + function (uint16) external returns (uint16) f, + uint16[2] calldata xy, + bytes calldata buf, + string calldata s, + uint16[] calldata l, + O calldata o + ) external; +} diff --git a/api/logic/test_contracts/artifacts/Varied.json b/api/logic/test_contracts/artifacts/Varied.json new file mode 100644 index 000000000..9ef25cb3c --- /dev/null +++ b/api/logic/test_contracts/artifacts/Varied.json @@ -0,0 +1,142 @@ +{ + "deploy": { + "VM:-": { + "linkReferences": {}, + "autoDeployLib": true + }, + "main:1": { + "linkReferences": {}, + "autoDeployLib": true + }, + "ropsten:3": { + "linkReferences": {}, + "autoDeployLib": true + }, + "rinkeby:4": { + "linkReferences": {}, + "autoDeployLib": true + }, + "kovan:42": { + "linkReferences": {}, + "autoDeployLib": true + }, + "goerli:5": { + "linkReferences": {}, + "autoDeployLib": true + }, + "Custom": { + "linkReferences": {}, + "autoDeployLib": true + } + }, + "data": { + "bytecode": { + "functionDebugData": {}, + "generatedSources": [], + "linkReferences": {}, + "object": "", + "opcodes": "", + "sourceMap": "" + }, + "deployedBytecode": { + "functionDebugData": {}, + "generatedSources": [], + "immutableReferences": {}, + "linkReferences": {}, + "object": "", + "opcodes": "", + "sourceMap": "" + }, + "gasEstimates": null, + "methodIdentifiers": { + "test(int8,uint8,int256,uint256,bool,bytes32,address,function,uint16[2],bytes,string,uint16[],(uint16,string))": "a85acffa" + } + }, + "abi": [ + { + "inputs": [ + { + "internalType": "int8", + "name": "i8", + "type": "int8" + }, + { + "internalType": "uint8", + "name": "u8", + "type": "uint8" + }, + { + "internalType": "int256", + "name": "i", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "u", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "b", + "type": "bool" + }, + { + "internalType": "bytes32", + "name": "b32", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "a", + "type": "address" + }, + { + "internalType": "function (uint16) external returns (uint16)", + "name": "f", + "type": "function" + }, + { + "internalType": "uint16[2]", + "name": "xy", + "type": "uint16[2]" + }, + { + "internalType": "bytes", + "name": "buf", + "type": "bytes" + }, + { + "internalType": "string", + "name": "s", + "type": "string" + }, + { + "internalType": "uint16[]", + "name": "l", + "type": "uint16[]" + }, + { + "components": [ + { + "internalType": "uint16", + "name": "n", + "type": "uint16" + }, + { + "internalType": "string", + "name": "s", + "type": "string" + } + ], + "internalType": "struct O", + "name": "o", + "type": "tuple" + } + ], + "name": "test", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] +}