-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support arbitrary contract calls (#224)
* Support arbitrary contract calls (#223) * support generic contract calls * support test on arm64 * address pr comments * address pr comments * panic if type conversion fails * address pr comments * fix * fix checking generic contract call method * fix docker build * define consts at top of file * cleanup construction checks * add link to type source * fix linting errors * add comment explaining how generic check works * add more comments --------- Co-authored-by: cryptoriver <114266151+cryptoriver@users.noreply.github.com>
- Loading branch information
1 parent
a0889a6
commit b847524
Showing
9 changed files
with
807 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
# ------------------------------------------------------------------------------ | ||
# Build avalanche | ||
# ------------------------------------------------------------------------------ | ||
FROM arm64v8/golang:1.20.10-bullseye AS avalanche | ||
|
||
ARG AVALANCHE_VERSION | ||
|
||
RUN git clone https://github.com/ava-labs/avalanchego.git \ | ||
/go/src/github.com/ava-labs/avalanchego | ||
|
||
WORKDIR /go/src/github.com/ava-labs/avalanchego | ||
|
||
RUN git checkout $AVALANCHE_VERSION && \ | ||
./scripts/build.sh | ||
|
||
# ------------------------------------------------------------------------------ | ||
# Build avalanche rosetta | ||
# ------------------------------------------------------------------------------ | ||
FROM arm64v8/golang:1.20.10-bullseye AS rosetta | ||
|
||
ARG ROSETTA_VERSION | ||
|
||
RUN git clone https://github.com/ava-labs/avalanche-rosetta.git \ | ||
/go/src/github.com/ava-labs/avalanche-rosetta | ||
|
||
WORKDIR /go/src/github.com/ava-labs/avalanche-rosetta | ||
|
||
RUN git checkout $ROSETTA_VERSION && \ | ||
go mod download | ||
|
||
RUN \ | ||
GO_VERSION=$(go version | awk {'print $3'}) \ | ||
GIT_COMMIT=$(git rev-parse HEAD) \ | ||
make build | ||
|
||
# ------------------------------------------------------------------------------ | ||
# Target container for running the node and rosetta server | ||
# ------------------------------------------------------------------------------ | ||
FROM arm64v8/ubuntu:20.04 | ||
|
||
# Install dependencies | ||
RUN apt-get update -y && \ | ||
apt-get install -y wget | ||
|
||
WORKDIR /app | ||
|
||
# Install avalanche daemon | ||
COPY --from=avalanche \ | ||
/go/src/github.com/ava-labs/avalanchego/build/avalanchego \ | ||
/app/avalanchego | ||
|
||
# Install rosetta server | ||
COPY --from=rosetta \ | ||
/go/src/github.com/ava-labs/avalanche-rosetta/rosetta-server \ | ||
/app/rosetta-server | ||
|
||
# Install rosetta runner | ||
COPY --from=rosetta \ | ||
/go/src/github.com/ava-labs/avalanche-rosetta/rosetta-runner \ | ||
/app/rosetta-runner | ||
|
||
# Install service start script | ||
COPY --from=rosetta \ | ||
/go/src/github.com/ava-labs/avalanche-rosetta/docker/entrypoint.sh \ | ||
/app/entrypoint.sh | ||
|
||
EXPOSE 9650 | ||
EXPOSE 9651 | ||
EXPOSE 8080 | ||
|
||
ENTRYPOINT ["/app/entrypoint.sh"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
package service | ||
|
||
import ( | ||
"encoding/hex" | ||
"errors" | ||
"fmt" | ||
"log" | ||
"math/big" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/ethereum/go-ethereum/accounts/abi" | ||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/ethereum/go-ethereum/common/hexutil" | ||
"github.com/ethereum/go-ethereum/crypto" | ||
) | ||
|
||
// The following implementations are derived from rosetta-geth-sdk: | ||
// | ||
// https://github.com/coinbase/rosetta-geth-sdk/blob/master/services/construction/contract_call_data.go | ||
|
||
const ( | ||
split = 2 | ||
base10 = 10 | ||
) | ||
|
||
// constructContractCallDataGeneric constructs the data field of a transaction. | ||
// The methodArgs can be already in ABI encoded format in case of a single string | ||
// It can also be passed in as a slice of args, which requires further encoding. | ||
func constructContractCallDataGeneric(methodSig string, methodArgs interface{}) ([]byte, error) { | ||
data, err := contractCallMethodID(methodSig) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// switch on the type of the method args. method args can come in from json as either a string or list of strings | ||
switch methodArgs := methodArgs.(type) { | ||
// case 0: no method arguments, return the selector | ||
case nil: | ||
return data, nil | ||
|
||
// case 1: method args are pre-compiled ABI data. decode the hex and create the call data directly | ||
case string: | ||
methodArgs = strings.TrimPrefix(methodArgs, "0x") | ||
b, decErr := hex.DecodeString(methodArgs) | ||
if decErr != nil { | ||
return nil, fmt.Errorf("error decoding method args hex data: %w", decErr) | ||
} | ||
return append(data, b...), nil | ||
|
||
// case 2: method args are a list of interface{} which will be converted to string before encoding | ||
case []interface{}: | ||
var strList []string | ||
for i, genericVal := range methodArgs { | ||
strVal, isStrVal := genericVal.(string) | ||
if !isStrVal { | ||
return nil, fmt.Errorf("invalid method_args type at index %d: %T (must be a string)", | ||
i, genericVal, | ||
) | ||
} | ||
strList = append(strList, strVal) | ||
} | ||
return encodeMethodArgsStrings(data, methodSig, strList) | ||
|
||
// case 3: method args are encoded as a list of strings, which will be decoded | ||
case []string: | ||
return encodeMethodArgsStrings(data, methodSig, methodArgs) | ||
|
||
// case 4: there is no known way to decode the method args | ||
default: | ||
return nil, fmt.Errorf( | ||
"invalid method_args type, accepted values are []string and hex-encoded string."+ | ||
" type received=%T value=%#v", methodArgs, methodArgs, | ||
) | ||
} | ||
} | ||
|
||
// encodeMethodArgsStrings constructs the data field of a transaction for a list of string args. | ||
// It attempts to first convert the string arg to it's corresponding type in the method signature, | ||
// and then performs abi encoding to the converted args list and construct the data. | ||
func encodeMethodArgsStrings(methodID []byte, methodSig string, methodArgs []string) ([]byte, error) { | ||
var data []byte | ||
data = append(data, methodID...) | ||
|
||
splitSigByLeadingParenthesis := strings.Split(methodSig, "(") | ||
if len(splitSigByLeadingParenthesis) < split { | ||
return data, nil | ||
} | ||
splitSigByTrailingParenthesis := strings.Split(splitSigByLeadingParenthesis[1], ")") | ||
if len(splitSigByTrailingParenthesis) < 1 { | ||
return data, nil | ||
} | ||
splitSigByComma := strings.Split(splitSigByTrailingParenthesis[0], ",") | ||
if len(splitSigByComma) != len(methodArgs) { | ||
return nil, errors.New("invalid method arguments") | ||
} | ||
|
||
arguments := abi.Arguments{} | ||
argumentsData := make([]interface{}, 0, len(splitSigByComma)) | ||
for i, v := range splitSigByComma { | ||
typed, _ := abi.NewType(v, v, nil) | ||
argument := abi.Arguments{ | ||
{ | ||
Type: typed, | ||
}, | ||
} | ||
|
||
arguments = append(arguments, argument...) | ||
var argData interface{} | ||
switch { | ||
case v == "address": | ||
{ | ||
argData = common.HexToAddress(methodArgs[i]) | ||
} | ||
case v == "uint32": | ||
{ | ||
u64, err := strconv.ParseUint(methodArgs[i], base10, 32) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
argData = uint32(u64) | ||
} | ||
case strings.HasPrefix(v, "uint") || strings.HasPrefix(v, "int"): | ||
{ | ||
value := new(big.Int) | ||
value.SetString(methodArgs[i], base10) | ||
argData = value | ||
} | ||
case v == "bytes32": | ||
{ | ||
value := [32]byte{} | ||
bytes, err := hexutil.Decode(methodArgs[i]) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
copy(value[:], bytes) | ||
argData = value | ||
} | ||
case strings.HasPrefix(v, "bytes"): | ||
{ | ||
bytes, err := hexutil.Decode(methodArgs[i]) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
argData = bytes | ||
} | ||
case strings.HasPrefix(v, "string"): | ||
{ | ||
argData = methodArgs[i] | ||
} | ||
case strings.HasPrefix(v, "bool"): | ||
{ | ||
value, err := strconv.ParseBool(methodArgs[i]) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
argData = value | ||
} | ||
default: | ||
return nil, fmt.Errorf("invalid argument type: %s", v) | ||
} | ||
argumentsData = append(argumentsData, argData) | ||
} | ||
|
||
abiEncodeData, err := arguments.PackValues(argumentsData) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to encode arguments: %w", err) | ||
} | ||
|
||
data = append(data, abiEncodeData...) | ||
return data, nil | ||
} | ||
|
||
// contractCallMethodID calculates the first 4 bytes of the method | ||
// signature for function call on contract | ||
func contractCallMethodID(methodSig string) ([]byte, error) { | ||
if len(methodSig) < 4 { | ||
return nil, fmt.Errorf("method signature is empty or too small") | ||
} | ||
return crypto.Keccak256([]byte(methodSig))[:4], nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package service | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/ethereum/go-ethereum/common/hexutil" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestConstruction_ContractCallData(t *testing.T) { | ||
tests := map[string]struct { | ||
methodSig string | ||
methodArgs interface{} | ||
|
||
expectedResponse string | ||
expectedError error | ||
}{ | ||
"happy path: nil args": { | ||
methodSig: "deposit()", | ||
methodArgs: nil, | ||
expectedResponse: "0xd0e30db0", | ||
}, | ||
"happy path: single string arg": { | ||
methodSig: "attest((bytes32,(address,uint64,bool,bytes32,bytes,uint256)))", | ||
methodArgs: "0x00000000000000000000000000000000000000000000000000000000000000201cdb5651ea836ecc9be70d044e2cf7a416e5257ec8d954deb9d09a66a8264b8e000000000000000000000000000000000000000000000000000000000000004000000000000000000000000026c58c5095c8fac99e518ee951ba8f56d3c75e8e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001", | ||
expectedResponse: "0xf17325e700000000000000000000000000000000000000000000000000000000000000201cdb5651ea836ecc9be70d044e2cf7a416e5257ec8d954deb9d09a66a8264b8e000000000000000000000000000000000000000000000000000000000000004000000000000000000000000026c58c5095c8fac99e518ee951ba8f56d3c75e8e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001", | ||
}, | ||
"happy path: list of string args": { | ||
methodSig: "register(string,address,bool)", | ||
methodArgs: []string{"bool abc", "0x0000000000000000000000000000000000000000", "true"}, | ||
expectedResponse: "0x60d7a2780000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000008626f6f6c20616263000000000000000000000000000000000000000000000000", | ||
}, | ||
"happy path: list of non string args": { | ||
methodSig: "register(string,address,bool)", | ||
methodArgs: []interface{}{"bool abc", "0x0000000000000000000000000000000000000000", "true"}, | ||
expectedResponse: "0x60d7a2780000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000008626f6f6c20616263000000000000000000000000000000000000000000000000", | ||
}, | ||
"error: case string: invalid method args hex data": { | ||
methodSig: "attest((bytes32,(address,uint64,bool,bytes32,bytes,uint256)))", | ||
methodArgs: "!!!", | ||
expectedError: errors.New("error decoding method args hex data: encoding/hex: invalid byte: U+0021 '!'"), | ||
}, | ||
"error: case []interface: ": { | ||
methodSig: "register(string,address,bool)", | ||
methodArgs: []interface{}{"bool abc", "0x0000000000000000000000000000000000000000", true}, | ||
expectedError: errors.New("invalid method_args type at index 2: bool (must be a string)"), | ||
}, | ||
"error: bad argument type": { | ||
methodSig: "attest(bytes32,foo)", | ||
methodArgs: []string{"0x0000000000000000000000000000000000000000000000000000000000000000", "bar"}, | ||
expectedError: fmt.Errorf("invalid argument type: %s", "foo"), | ||
}, | ||
} | ||
|
||
for name, test := range tests { | ||
t.Run(name, func(t *testing.T) { | ||
bytes, err := constructContractCallDataGeneric(test.methodSig, test.methodArgs) | ||
if err != nil { | ||
fmt.Println(err) | ||
assert.EqualError(t, err, test.expectedError.Error()) | ||
} else { | ||
assert.Equal(t, test.expectedResponse, hexutil.Encode(bytes)) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.