-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #16 from cryptoriver/contract_call_support
Enable arbitrary contract call support
- Loading branch information
Showing
7 changed files
with
737 additions
and
27 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,115 @@ | ||
# ------------------------------------------------------------------------------ | ||
# Build go-flare | ||
# ------------------------------------------------------------------------------ | ||
FROM arm64v8/golang:1.18.5 AS flare | ||
|
||
WORKDIR /app | ||
|
||
ARG GO_FLARE_VERSION=v1.7.1806 | ||
ARG GO_FLARE_REPO=https://github.com/flare-foundation/go-flare | ||
|
||
RUN git clone --branch "$GO_FLARE_VERSION" "${GO_FLARE_REPO}" . | ||
|
||
RUN apt update && \ | ||
apt -y install rsync | ||
RUN cd avalanchego && \ | ||
./scripts/build.sh | ||
|
||
# ------------------------------------------------------------------------------ | ||
# Build flare-rosetta | ||
# ------------------------------------------------------------------------------ | ||
FROM arm64v8/golang:1.18.5 AS rosetta | ||
|
||
ARG ROSETTA_SRC=https://github.com/flare-foundation/flare-rosetta/archive/refs/heads/main.zip | ||
ARG ROSETTA_SRC_ZIP_SUBFOLDER=flare-rosetta-main | ||
|
||
WORKDIR /tmp | ||
|
||
RUN apt update -y && apt install unzip -y | ||
ADD ${ROSETTA_SRC} /tmp/rosetta-source | ||
RUN if [ ! -d "/tmp/rosetta-source" ]; then \ | ||
unzip "/tmp/rosetta-source" && \ | ||
mv /tmp/${ROSETTA_SRC_ZIP_SUBFOLDER} /app; \ | ||
else \ | ||
mv /tmp/rosetta-source /app; \ | ||
fi | ||
|
||
WORKDIR /app/server | ||
|
||
RUN \ | ||
GO_VERSION=$(go version | awk {'print $3'}) \ | ||
GIT_COMMIT=main \ | ||
go mod download && \ | ||
make setup && \ | ||
make build | ||
|
||
# ------------------------------------------------------------------------------ | ||
# Target container for running the rosetta server | ||
# ------------------------------------------------------------------------------ | ||
FROM arm64v8/ubuntu:20.04 | ||
|
||
ENV DEBIAN_FRONTEND=noninteractive | ||
|
||
# Install dependencies | ||
RUN apt-get update -y && \ | ||
apt -y upgrade && \ | ||
apt -y install curl jq netcat dnsutils moreutils | ||
|
||
WORKDIR /app | ||
|
||
ENV HTTP_HOST=0.0.0.0 \ | ||
HTTP_PORT=9650 \ | ||
STAKING_PORT=9651 \ | ||
DB_DIR=/data \ | ||
DB_TYPE=leveldb \ | ||
LOG_DIR=/app/flare/logs \ | ||
MODE=online \ | ||
START_ROSETTA_SERVER_AFTER_BOOTSTRAP=false \ | ||
AUTOCONFIGURE_BOOTSTRAP_ENDPOINT="" \ | ||
AUTOCONFIGURE_BOOTSTRAP_ENDPOINT_RETRY=0 \ | ||
STAKING_ENABLED="true" \ | ||
YES_I_REALLY_KNOW_WHAT_I_AM_DOING="false" | ||
|
||
|
||
|
||
# Intentionally empty, so env vars are evalueted at runtime (script entrypoint_flare.sh) instead of build time | ||
# Default: /app/conf/$NETWORK_ID | ||
ENV CHAIN_CONFIG_DIR="" | ||
# Default: warn | ||
ENV LOG_LEVEL="" | ||
|
||
# ROSETTA ENV VARS | ||
ENV ROSETTA_FLARE_ENDPOINT=http://127.0.0.1:9650 | ||
# Intentionally empty, so env vars are evalueted at runtime (script entrypoint_rosetta.sh) instead of build time | ||
# Default: /app/conf/$NETWORK_ID/server-config.json | ||
ENV ROSETTA_CONFIG_PATH="" | ||
|
||
# Install flare | ||
COPY --from=flare /app/avalanchego/build /app/flare/build | ||
|
||
# Install rosetta server | ||
COPY --from=rosetta /app/server /app/rosetta-server | ||
|
||
# Install node and rosetta configs | ||
COPY --from=rosetta /app/server/rosetta-cli-conf /app/conf | ||
|
||
# Install entrypoints | ||
COPY --from=rosetta /app/server/docker/entrypoint_flare.sh /app/entrypoint_flare.sh | ||
COPY --from=rosetta /app/server/docker/entrypoint_rosetta.sh /app/entrypoint_rosetta.sh | ||
COPY --from=rosetta /app/server/docker/entrypoint_main.sh /app/entrypoint_main.sh | ||
COPY --from=rosetta /app/server/docker/healthcheck.sh /app/healthcheck.sh | ||
|
||
# Copy testnet configs | ||
COPY --from=flare /app/avalanchego/staking/local /app/flare/staking/local | ||
COPY --from=flare /app/config/localflare /app/flare/config/localflare | ||
|
||
RUN chmod +x entrypoint_flare.sh entrypoint_rosetta.sh entrypoint_main.sh | ||
|
||
EXPOSE ${HTTP_PORT} | ||
EXPOSE ${STAKING_PORT} | ||
EXPOSE 8080 | ||
|
||
HEALTHCHECK --interval=5s --timeout=5s --retries=5 --start-period=15s CMD bash /app/healthcheck.sh | ||
|
||
ENTRYPOINT ["/bin/bash"] | ||
CMD ["/app/entrypoint_main.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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
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" | ||
"golang.org/x/crypto/sha3" | ||
) | ||
|
||
// 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) { | ||
arguments := abi.Arguments{} | ||
var argumentsData []interface{} | ||
|
||
var data []byte | ||
data = append(data, methodID...) | ||
|
||
const split = 2 | ||
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") | ||
} | ||
|
||
for i, v := range splitSigByComma { | ||
typed, _ := abi.NewType(v, v, nil) | ||
argument := abi.Arguments{ | ||
{ | ||
Type: typed, | ||
}, | ||
} | ||
|
||
arguments = append(arguments, argument...) | ||
var argData interface{} | ||
const base = 10 | ||
switch { | ||
case v == "address": | ||
{ | ||
argData = common.HexToAddress(methodArgs[i]) | ||
} | ||
case v == "uint32": | ||
{ | ||
u64, err := strconv.ParseUint(methodArgs[i], 10, 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], base) | ||
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"): | ||
{ | ||
// No fixed size set as it would make it an "array" instead | ||
// of a "slice" when encoding. We want it to be a slice. | ||
value := []byte{} | ||
bytes, err := hexutil.Decode(methodArgs[i]) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
copy(value[:], bytes) // nolint:gocritic | ||
argData = value | ||
} | ||
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, errors.New(fmt.Sprintf("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) { | ||
fnSignature := []byte(methodSig) | ||
hash := sha3.NewLegacyKeccak256() | ||
if _, err := hash.Write(fnSignature); err != nil { | ||
return nil, err | ||
} | ||
|
||
return hash.Sum(nil)[: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,63 @@ | ||
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)"), | ||
}, | ||
} | ||
|
||
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.