diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b85175..58ceeb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,16 +17,25 @@ jobs: outputs: IMAGE_TAG: ${{ steps.ct_image_tag.outputs.IMAGE_TAG }} IMAGE_PATH_0: ghcr.io/${{ github.repository }} + ROSETTA_BRANCH: ${{ steps.ct_image_tag.outputs.ROSETTA_BRANCH }} steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - id: ct_image_tag run: | - TAG_NORMALIZED=$(echo ${GITHUB_REF#refs/*/} | sed "s/[^[:alpha:]0-9.-]/-/g") + # For pull requests, use GITHUB_HEAD_REF (source branch of PR) + # For push events, extract branch name from GITHUB_REF + if [ -n "$GITHUB_HEAD_REF" ]; then + BRANCH_NAME=$GITHUB_HEAD_REF + else + BRANCH_NAME=${GITHUB_REF#refs/*/} + fi + TAG_NORMALIZED=$(echo "$BRANCH_NAME" | sed "s/[^[:alpha:]0-9.-]/-/g") if [[ "$TAG_NORMALIZED" == "main" ]]; then echo "IMAGE_TAG=latest" >> $GITHUB_OUTPUT else echo "IMAGE_TAG=$TAG_NORMALIZED" >> $GITHUB_OUTPUT fi + echo "ROSETTA_BRANCH=$BRANCH_NAME" >> $GITHUB_OUTPUT build-container: runs-on: ubuntu-latest @@ -49,7 +58,7 @@ jobs: - name: Build and push container image shell: bash run: | - docker build --progress=plain --no-cache --build-arg ROSETTA_SRC=. --tag ${{ needs.define-env.outputs.IMAGE_PATH_0 }}:${{ needs.define-env.outputs.IMAGE_TAG }} -f ./server/Dockerfile . + docker build --progress=plain --no-cache --build-arg ROSETTA_SRC=. --build-arg ROSETTA_BRANCH=${{ needs.define-env.outputs.ROSETTA_BRANCH }} --tag ${{ needs.define-env.outputs.IMAGE_PATH_0 }}:${{ needs.define-env.outputs.IMAGE_TAG }} -f ./server/Dockerfile . docker push ${{ needs.define-env.outputs.IMAGE_PATH_0 }}:${{ needs.define-env.outputs.IMAGE_TAG }} - name: get image sha256 id: image_sha256 @@ -58,135 +67,39 @@ jobs: export IMAGE_SHA256_0_FULL=$(docker image inspect --format='{{json .RepoDigests}}' ${{ needs.define-env.outputs.IMAGE_PATH_0 }}:${{ needs.define-env.outputs.IMAGE_TAG }} | jq --raw-output '.[0]') echo "IMAGE_SHA256_0=${IMAGE_SHA256_0_FULL#*@}" >> $GITHUB_OUTPUT - - test-container-localflare-online: + test-localflare: runs-on: ubuntu-latest needs: - build-container - define-env - strategy: - fail-fast: false - matrix: - index: [1, 2, 3, 4, 5] - START_ROSETTA_SERVER_AFTER_BOOTSTRAP: [true, false] - services: - flare_node: - image: ${{ needs.define-env.outputs.IMAGE_PATH_0 }}@${{ needs.build-container.outputs.IMAGE_SHA256_0 }} - ports: - - 9650:9650 - - 9651:9651 - - 8080:8080 - env: - DEBUG: true - NETWORK_ID: localflare - FLARE_LOCAL_TXS_ENABLED: true - START_ROSETTA_SERVER_AFTER_BOOTSTRAP: ${{ matrix.START_ROSETTA_SERVER_AFTER_BOOTSTRAP }} - EXTRA_ARGUMENTS: --staking-tls-cert-file=/app/flare/staking/local/staker1.crt --staking-tls-key-file=/app/flare/staking/local/staker1.key - options: >- - --health-interval 15s - --health-timeout 15s - --health-retries 10 - --health-start-period 30s steps: - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 with: go-version: ${{ env.go_version }} - - run: sudo apt update -y && sudo apt install curl -y - - - name: Wait for go-flare to bind to port - shell: bash - run: curl -s --retry 6 --retry-delay 10 --retry-connrefused http://localhost:9650 || exit 1 - - - name: Import testnet - shell: bash - run: | - curl -s -o /tmp/test_pchain_import.sh https://raw.githubusercontent.com/flare-foundation/go-flare/114017731f4f8f6192a4df2748da914c0257e16b/avalanchego/scripts/test_pchain_import.sh - sed -i 's/localhost/127.0.0.1/g' /tmp/test_pchain_import.sh - sed -i 's/ | jq .//g' /tmp/test_pchain_import.sh - bash /tmp/test_pchain_import.sh - while [[ "$(curl -X POST --data '{ "jsonrpc": "2.0", "method": "platform.getHeight", "params": {}, "id": 1 }' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/P | jq -r .result.height)" != "2" ]] - do - echo "Block height not reached.. Block Height:" $(curl -X POST --data '{ "jsonrpc": "2.0", "method": "platform.getHeight", "params": {}, "id": 1 }' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/P | jq -r .result.height) - sleep 1 - done - - name: Wait for rosetta to bind to port - shell: bash - run: curl -s --retry 6 --retry-delay 10 --retry-connrefused http://localhost:8080 || exit 1 - - - name: List networks Rosetta API lists - shell: bash - run: | - curl -s --location --request POST 'http://127.0.0.1:8080/network/list' \ - --header 'Content-Type: application/json' \ - --data-raw '{ "metadata" : {} }' \ - --fail || exit 1 - - name: Install rosetta cli - shell: bash - run: | - mkdir -p /tmp/rosetta - curl -o /tmp/rosetta/install.sh -sSfL https://raw.githubusercontent.com/coinbase/mesh-cli/refs/tags/v0.10.4/scripts/install.sh - sed -i 's/REPO="rosetta-cli"/REPO="mesh-cli"/g' /tmp/rosetta/install.sh - bash /tmp/rosetta/install.sh -b /tmp/rosetta/ v0.10.4 - - name: Run rosetta-cli check:construction - timeout-minutes: 5 - shell: bash - run: | - pushd server/rosetta-cli-conf/localflare - /tmp/rosetta/rosetta-cli --configuration-file=./config.json check:construction - popd + - name: Set up Docker Compose + uses: docker/setup-compose-action@v1 - - test-container-localflare-offline: - runs-on: ubuntu-latest - needs: - - build-container - - define-env - strategy: - fail-fast: false - matrix: - index: [1, 2, 3, 4, 5] - services: - flare_node: - image: ${{ needs.define-env.outputs.IMAGE_PATH_0 }}@${{ needs.build-container.outputs.IMAGE_SHA256_0 }} - ports: - - 9650:9650 - - 9651:9651 - - 8080:8080 - env: - DEBUG: true - NETWORK_ID: localflare - FLARE_LOCAL_TXS_ENABLED: true - MODE: offline - EXTRA_ARGUMENTS: --staking-tls-cert-file=/app/flare/staking/local/staker1.crt --staking-tls-key-file=/app/flare/staking/local/staker1.key - options: >- - --health-interval 15s - --health-timeout 15s - --health-retries 10 - --health-start-period 30s - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/setup-node@v6 with: - go-version: ${{ env.go_version }} - - run: sudo apt update -y && sudo apt install curl -y + node-version: 20 - - name: Wait for rosetta to bind to port - shell: bash - run: curl -s --retry 6 --retry-delay 10 --retry-connrefused http://localhost:8080 || exit 1 + - run: sudo apt update -y && sudo apt install curl jq -y - - name: List networks Rosetta API lists + - name: Install yarn + run: npm install -g yarn + + - name: Run localflare test script + env: + CI: true + ROSETTA_IMAGE: ${{ needs.define-env.outputs.IMAGE_PATH_0 }}@${{ needs.build-container.outputs.IMAGE_SHA256_0 }} + START_ROSETTA_SERVER_AFTER_BOOTSTRAP: true + MODE: online shell: bash run: | - curl -s --location --request POST 'http://127.0.0.1:8080/network/list' \ - --header 'Content-Type: application/json' \ - --data-raw '{ "metadata" : {} }' \ - --fail || exit 1 - - - - + ./test-localflare.sh test-make_build: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index a4767d2..674a68a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ data __debug_bin* build_docker.sh .idea +tmp/ \ No newline at end of file diff --git a/server/Dockerfile b/server/Dockerfile index be51353..c0f68d3 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -5,7 +5,7 @@ FROM golang:1.22 AS flare WORKDIR /app -ARG GO_FLARE_VERSION=v1.11.0 +ARG GO_FLARE_VERSION=v1.11.13-rc0 ARG GO_FLARE_REPO=https://github.com/flare-foundation/go-flare RUN git clone --branch "$GO_FLARE_VERSION" "${GO_FLARE_REPO}" . @@ -20,8 +20,9 @@ RUN cd avalanchego && \ # ------------------------------------------------------------------------------ FROM golang:1.22 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 +ARG ROSETTA_BRANCH=main +ARG ROSETTA_SRC=https://github.com/flare-foundation/flare-rosetta/archive/refs/heads/${ROSETTA_BRANCH}.zip +ARG ROSETTA_SRC_ZIP_SUBFOLDER=flare-rosetta-${ROSETTA_BRANCH} WORKDIR /tmp diff --git a/server/client/client.go b/server/client/client.go index 71f0b9d..917d9c0 100644 --- a/server/client/client.go +++ b/server/client/client.go @@ -2,85 +2,54 @@ package client import ( "context" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/network/peer" - "github.com/ava-labs/avalanchego/utils/json" "math/big" "strings" "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/rpc" - ethtypes "github.com/ava-labs/coreth/core/types" + "github.com/ava-labs/coreth/core/types" "github.com/ava-labs/coreth/interfaces" - ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ava-labs/coreth/plugin/evm" + "github.com/ethereum/go-ethereum/common" + + "github.com/ava-labs/avalanche-rosetta/constants" ) // Interface compliance var _ Client = &client{} type Client interface { - IsBootstrapped(context.Context, string, ...rpc.Option) (bool, error) + // info.Client methods + InfoClient + ChainID(context.Context) (*big.Int, error) - BlockByHash(context.Context, ethcommon.Hash) (*ethtypes.Block, error) - BlockByNumber(context.Context, *big.Int) (*ethtypes.Block, error) - HeaderByHash(context.Context, ethcommon.Hash) (*ethtypes.Header, error) - HeaderByNumber(context.Context, *big.Int) (*ethtypes.Header, error) - TransactionByHash(context.Context, ethcommon.Hash) (*ethtypes.Transaction, bool, error) - TransactionReceipt(context.Context, ethcommon.Hash) (*ethtypes.Receipt, error) + BlockByHash(context.Context, common.Hash) (*types.Block, error) + BlockByNumber(context.Context, *big.Int) (*types.Block, error) + HeaderByHash(context.Context, common.Hash) (*types.Header, error) + HeaderByNumber(context.Context, *big.Int) (*types.Header, error) + TransactionByHash(context.Context, common.Hash) (*types.Transaction, bool, error) + TransactionReceipt(context.Context, common.Hash) (*types.Receipt, error) TraceTransaction(context.Context, string) (*Call, []*FlatCall, error) TraceBlockByHash(context.Context, string) ([]*Call, [][]*FlatCall, error) - SendTransaction(context.Context, *ethtypes.Transaction) error - BalanceAt(context.Context, ethcommon.Address, *big.Int) (*big.Int, error) - NonceAt(context.Context, ethcommon.Address, *big.Int) (uint64, error) + SendTransaction(context.Context, *types.Transaction) error + BalanceAt(context.Context, common.Address, *big.Int) (*big.Int, error) + NonceAt(context.Context, common.Address, *big.Int) (uint64, error) SuggestGasPrice(context.Context) (*big.Int, error) EstimateGas(context.Context, interfaces.CallMsg) (uint64, error) TxPoolContent(context.Context) (*TxPoolContent, error) - GetNetworkName(context.Context, ...rpc.Option) (string, error) - Peers(context.Context, ...rpc.Option) ([]info.Peer, error) - // Peers_v1_11 enables info.peers RPC call compatible with v1.11. - // Once the dependencies are upgraded, this should be replaced with Peers. - Peers_v1_11(context.Context, []ids.NodeID, ...rpc.Option) ([]Peer_v1_11, error) - GetContractInfo(ethcommon.Address, bool) (string, uint8, error) + GetContractInfo(common.Address, bool) (string, uint8, error) CallContract(context.Context, interfaces.CallMsg, *big.Int) ([]byte, error) + IssueTx(ctx context.Context, txBytes []byte, options ...rpc.Option) (ids.ID, error) + GetAtomicUTXOs(ctx context.Context, addrs []ids.ShortID, sourceChain string, limit uint32, startAddress ids.ShortID, startUTXOID ids.ID, options ...rpc.Option) ([][]byte, ids.ShortID, ids.ID, error) + EstimateBaseFee(ctx context.Context) (*big.Int, error) } -type clientFix struct { - requester rpc.EndpointRequester -} - -func newClientFix(uri string) *clientFix { - return &clientFix{ - requester: rpc.NewEndpointRequester( - uri+"/ext/info", - "info", - ), - } -} - -type Peer_v1_11 struct { - peer.Info - - Benched []string `json:"benched"` -} - -type PeersReply_v1_11 struct { - // Number of elements in [Peers] - NumPeers json.Uint64 `json:"numPeers"` - // Each element is a peer - Peers []Peer_v1_11 `json:"peers"` -} - -func (cf *clientFix) Peers_v1_11(ctx context.Context, nodeIDs []ids.NodeID, options ...rpc.Option) ([]Peer_v1_11, error) { - res := &PeersReply_v1_11{} - err := cf.requester.SendRequest(ctx, "peers", &info.PeersArgs{ - NodeIDs: nodeIDs, - }, res, options...) - return res.Peers, err -} +type EvmClient evm.Client type client struct { info.Client - *clientFix + EvmClient *EthClient *ContractClient } @@ -94,10 +63,10 @@ func NewClient(ctx context.Context, endpoint string) (Client, error) { return nil, err } - return client{ + return &client{ Client: info.NewClient(endpoint), - clientFix: newClientFix(endpoint), + EvmClient: evm.NewClient(endpoint, constants.CChain.String()), EthClient: eth, ContractClient: NewContractClient(eth.Client), }, nil -} +} \ No newline at end of file diff --git a/server/client/contract.go b/server/client/contract.go index 4793ca4..4a3266b 100644 --- a/server/client/contract.go +++ b/server/client/contract.go @@ -13,14 +13,19 @@ const ( // ContractClient is a client for the calling contract information type ContractClient struct { ethClient ethclient.Client - cache *cache.LRU + cache *cache.LRU[common.Address, *ContractInfo] +} + +type ContractInfo struct { + Symbol string + Decimals uint8 } // NewContractClient returns a new ContractInfo client func NewContractClient(c ethclient.Client) *ContractClient { return &ContractClient{ ethClient: c, - cache: &cache.LRU{Size: contractCacheSize}, + cache: &cache.LRU[common.Address, *ContractInfo]{Size: contractCacheSize}, } } @@ -28,14 +33,8 @@ func NewContractClient(c ethclient.Client) *ContractClient { func (c *ContractClient) GetContractInfo(addr common.Address, erc20 bool) (string, uint8, error) { // We don't define another struct because this is never used outside of this // function. - type ContractInfo struct { - Symbol string - Decimals uint8 - } - if currency, cached := c.cache.Get(addr); cached { - cast := currency.(*ContractInfo) - return cast.Symbol, cast.Decimals, nil + return currency.Symbol, currency.Decimals, nil } token, err := NewContractInfoToken(addr, c.ethClient) diff --git a/server/client/eth.go b/server/client/eth.go index 059a595..509c079 100644 --- a/server/client/eth.go +++ b/server/client/eth.go @@ -2,7 +2,6 @@ package client import ( "context" - "fmt" "github.com/ava-labs/coreth/eth/tracers" "github.com/ava-labs/coreth/ethclient" @@ -24,7 +23,7 @@ type EthClient struct { // NewEthClient returns a new EVM client func NewEthClient(ctx context.Context, endpoint string) (*EthClient, error) { - endpointURL := fmt.Sprintf("%s%s", endpoint, prefixEth) + endpointURL := endpoint + prefixEth c, err := rpc.DialContext(ctx, endpointURL) if err != nil { diff --git a/server/client/info_client.go b/server/client/info_client.go new file mode 100644 index 0000000..fc14507 --- /dev/null +++ b/server/client/info_client.go @@ -0,0 +1,17 @@ +package client + +import ( + "context" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/rpc" +) + +// InfoClient collects all Avalanchego info.Client methods common +// to Rosetta Clients +type InfoClient interface { + GetBlockchainID(context.Context, string, ...rpc.Option) (ids.ID, error) + IsBootstrapped(context.Context, string, ...rpc.Option) (bool, error) + Peers(context.Context, ...rpc.Option) ([]info.Peer, error) +} diff --git a/server/client/mock_client.go b/server/client/mock_client.go new file mode 100644 index 0000000..78429c8 --- /dev/null +++ b/server/client/mock_client.go @@ -0,0 +1,817 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ava-labs/avalanche-rosetta/client (interfaces: Client,PChainClient) +// +// Generated by this command: +// +// mockgen -package=client -destination=client/mock_client.go github.com/ava-labs/avalanche-rosetta/client Client,PChainClient +// + +// Package client is a generated GoMock package. +package client + +import ( + context "context" + big "math/big" + reflect "reflect" + + api "github.com/ava-labs/avalanchego/api" + info "github.com/ava-labs/avalanchego/api/info" + ids "github.com/ava-labs/avalanchego/ids" + indexer "github.com/ava-labs/avalanchego/indexer" + rpc "github.com/ava-labs/avalanchego/utils/rpc" + avm "github.com/ava-labs/avalanchego/vms/avm" + platformvm "github.com/ava-labs/avalanchego/vms/platformvm" + signer "github.com/ava-labs/avalanchego/vms/platformvm/signer" + types "github.com/ava-labs/coreth/core/types" + interfaces "github.com/ava-labs/coreth/interfaces" + common "github.com/ethereum/go-ethereum/common" + gomock "go.uber.org/mock/gomock" +) + +// MockClient is a mock of Client interface. +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// BalanceAt mocks base method. +func (m *MockClient) BalanceAt(arg0 context.Context, arg1 common.Address, arg2 *big.Int) (*big.Int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BalanceAt", arg0, arg1, arg2) + ret0, _ := ret[0].(*big.Int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BalanceAt indicates an expected call of BalanceAt. +func (mr *MockClientMockRecorder) BalanceAt(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BalanceAt", reflect.TypeOf((*MockClient)(nil).BalanceAt), arg0, arg1, arg2) +} + +// BlockByHash mocks base method. +func (m *MockClient) BlockByHash(arg0 context.Context, arg1 common.Hash) (*types.Block, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BlockByHash", arg0, arg1) + ret0, _ := ret[0].(*types.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BlockByHash indicates an expected call of BlockByHash. +func (mr *MockClientMockRecorder) BlockByHash(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockByHash", reflect.TypeOf((*MockClient)(nil).BlockByHash), arg0, arg1) +} + +// BlockByNumber mocks base method. +func (m *MockClient) BlockByNumber(arg0 context.Context, arg1 *big.Int) (*types.Block, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BlockByNumber", arg0, arg1) + ret0, _ := ret[0].(*types.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BlockByNumber indicates an expected call of BlockByNumber. +func (mr *MockClientMockRecorder) BlockByNumber(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockByNumber", reflect.TypeOf((*MockClient)(nil).BlockByNumber), arg0, arg1) +} + +// CallContract mocks base method. +func (m *MockClient) CallContract(arg0 context.Context, arg1 interfaces.CallMsg, arg2 *big.Int) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CallContract", arg0, arg1, arg2) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CallContract indicates an expected call of CallContract. +func (mr *MockClientMockRecorder) CallContract(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallContract", reflect.TypeOf((*MockClient)(nil).CallContract), arg0, arg1, arg2) +} + +// ChainID mocks base method. +func (m *MockClient) ChainID(arg0 context.Context) (*big.Int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ChainID", arg0) + ret0, _ := ret[0].(*big.Int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ChainID indicates an expected call of ChainID. +func (mr *MockClientMockRecorder) ChainID(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChainID", reflect.TypeOf((*MockClient)(nil).ChainID), arg0) +} + +// EstimateBaseFee mocks base method. +func (m *MockClient) EstimateBaseFee(arg0 context.Context) (*big.Int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EstimateBaseFee", arg0) + ret0, _ := ret[0].(*big.Int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EstimateBaseFee indicates an expected call of EstimateBaseFee. +func (mr *MockClientMockRecorder) EstimateBaseFee(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EstimateBaseFee", reflect.TypeOf((*MockClient)(nil).EstimateBaseFee), arg0) +} + +// EstimateGas mocks base method. +func (m *MockClient) EstimateGas(arg0 context.Context, arg1 interfaces.CallMsg) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EstimateGas", arg0, arg1) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EstimateGas indicates an expected call of EstimateGas. +func (mr *MockClientMockRecorder) EstimateGas(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EstimateGas", reflect.TypeOf((*MockClient)(nil).EstimateGas), arg0, arg1) +} + +// GetAtomicUTXOs mocks base method. +func (m *MockClient) GetAtomicUTXOs(arg0 context.Context, arg1 []ids.ShortID, arg2 string, arg3 uint32, arg4 ids.ShortID, arg5 ids.ID, arg6 ...rpc.Option) ([][]byte, ids.ShortID, ids.ID, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1, arg2, arg3, arg4, arg5} + for _, a := range arg6 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetAtomicUTXOs", varargs...) + ret0, _ := ret[0].([][]byte) + ret1, _ := ret[1].(ids.ShortID) + ret2, _ := ret[2].(ids.ID) + ret3, _ := ret[3].(error) + return ret0, ret1, ret2, ret3 +} + +// GetAtomicUTXOs indicates an expected call of GetAtomicUTXOs. +func (mr *MockClientMockRecorder) GetAtomicUTXOs(arg0, arg1, arg2, arg3, arg4, arg5 any, arg6 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1, arg2, arg3, arg4, arg5}, arg6...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAtomicUTXOs", reflect.TypeOf((*MockClient)(nil).GetAtomicUTXOs), varargs...) +} + +// GetBlockchainID mocks base method. +func (m *MockClient) GetBlockchainID(arg0 context.Context, arg1 string, arg2 ...rpc.Option) (ids.ID, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetBlockchainID", varargs...) + ret0, _ := ret[0].(ids.ID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBlockchainID indicates an expected call of GetBlockchainID. +func (mr *MockClientMockRecorder) GetBlockchainID(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockchainID", reflect.TypeOf((*MockClient)(nil).GetBlockchainID), varargs...) +} + +// GetContractInfo mocks base method. +func (m *MockClient) GetContractInfo(arg0 common.Address, arg1 bool) (string, byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetContractInfo", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(byte) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetContractInfo indicates an expected call of GetContractInfo. +func (mr *MockClientMockRecorder) GetContractInfo(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContractInfo", reflect.TypeOf((*MockClient)(nil).GetContractInfo), arg0, arg1) +} + +// HeaderByHash mocks base method. +func (m *MockClient) HeaderByHash(arg0 context.Context, arg1 common.Hash) (*types.Header, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HeaderByHash", arg0, arg1) + ret0, _ := ret[0].(*types.Header) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HeaderByHash indicates an expected call of HeaderByHash. +func (mr *MockClientMockRecorder) HeaderByHash(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeaderByHash", reflect.TypeOf((*MockClient)(nil).HeaderByHash), arg0, arg1) +} + +// HeaderByNumber mocks base method. +func (m *MockClient) HeaderByNumber(arg0 context.Context, arg1 *big.Int) (*types.Header, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HeaderByNumber", arg0, arg1) + ret0, _ := ret[0].(*types.Header) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HeaderByNumber indicates an expected call of HeaderByNumber. +func (mr *MockClientMockRecorder) HeaderByNumber(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeaderByNumber", reflect.TypeOf((*MockClient)(nil).HeaderByNumber), arg0, arg1) +} + +// IsBootstrapped mocks base method. +func (m *MockClient) IsBootstrapped(arg0 context.Context, arg1 string, arg2 ...rpc.Option) (bool, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "IsBootstrapped", varargs...) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsBootstrapped indicates an expected call of IsBootstrapped. +func (mr *MockClientMockRecorder) IsBootstrapped(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsBootstrapped", reflect.TypeOf((*MockClient)(nil).IsBootstrapped), varargs...) +} + +// IssueTx mocks base method. +func (m *MockClient) IssueTx(arg0 context.Context, arg1 []byte, arg2 ...rpc.Option) (ids.ID, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "IssueTx", varargs...) + ret0, _ := ret[0].(ids.ID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IssueTx indicates an expected call of IssueTx. +func (mr *MockClientMockRecorder) IssueTx(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IssueTx", reflect.TypeOf((*MockClient)(nil).IssueTx), varargs...) +} + +// NonceAt mocks base method. +func (m *MockClient) NonceAt(arg0 context.Context, arg1 common.Address, arg2 *big.Int) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NonceAt", arg0, arg1, arg2) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NonceAt indicates an expected call of NonceAt. +func (mr *MockClientMockRecorder) NonceAt(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NonceAt", reflect.TypeOf((*MockClient)(nil).NonceAt), arg0, arg1, arg2) +} + +// Peers mocks base method. +func (m *MockClient) Peers(arg0 context.Context, arg1 ...rpc.Option) ([]info.Peer, error) { + m.ctrl.T.Helper() + varargs := []any{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Peers", varargs...) + ret0, _ := ret[0].([]info.Peer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Peers indicates an expected call of Peers. +func (mr *MockClientMockRecorder) Peers(arg0 any, arg1 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Peers", reflect.TypeOf((*MockClient)(nil).Peers), varargs...) +} + +// SendTransaction mocks base method. +func (m *MockClient) SendTransaction(arg0 context.Context, arg1 *types.Transaction) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendTransaction", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendTransaction indicates an expected call of SendTransaction. +func (mr *MockClientMockRecorder) SendTransaction(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendTransaction", reflect.TypeOf((*MockClient)(nil).SendTransaction), arg0, arg1) +} + +// SuggestGasPrice mocks base method. +func (m *MockClient) SuggestGasPrice(arg0 context.Context) (*big.Int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SuggestGasPrice", arg0) + ret0, _ := ret[0].(*big.Int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SuggestGasPrice indicates an expected call of SuggestGasPrice. +func (mr *MockClientMockRecorder) SuggestGasPrice(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SuggestGasPrice", reflect.TypeOf((*MockClient)(nil).SuggestGasPrice), arg0) +} + +// TraceBlockByHash mocks base method. +func (m *MockClient) TraceBlockByHash(arg0 context.Context, arg1 string) ([]*Call, [][]*FlatCall, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TraceBlockByHash", arg0, arg1) + ret0, _ := ret[0].([]*Call) + ret1, _ := ret[1].([][]*FlatCall) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// TraceBlockByHash indicates an expected call of TraceBlockByHash. +func (mr *MockClientMockRecorder) TraceBlockByHash(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TraceBlockByHash", reflect.TypeOf((*MockClient)(nil).TraceBlockByHash), arg0, arg1) +} + +// TraceTransaction mocks base method. +func (m *MockClient) TraceTransaction(arg0 context.Context, arg1 string) (*Call, []*FlatCall, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TraceTransaction", arg0, arg1) + ret0, _ := ret[0].(*Call) + ret1, _ := ret[1].([]*FlatCall) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// TraceTransaction indicates an expected call of TraceTransaction. +func (mr *MockClientMockRecorder) TraceTransaction(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TraceTransaction", reflect.TypeOf((*MockClient)(nil).TraceTransaction), arg0, arg1) +} + +// TransactionByHash mocks base method. +func (m *MockClient) TransactionByHash(arg0 context.Context, arg1 common.Hash) (*types.Transaction, bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TransactionByHash", arg0, arg1) + ret0, _ := ret[0].(*types.Transaction) + ret1, _ := ret[1].(bool) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// TransactionByHash indicates an expected call of TransactionByHash. +func (mr *MockClientMockRecorder) TransactionByHash(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactionByHash", reflect.TypeOf((*MockClient)(nil).TransactionByHash), arg0, arg1) +} + +// TransactionReceipt mocks base method. +func (m *MockClient) TransactionReceipt(arg0 context.Context, arg1 common.Hash) (*types.Receipt, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TransactionReceipt", arg0, arg1) + ret0, _ := ret[0].(*types.Receipt) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TransactionReceipt indicates an expected call of TransactionReceipt. +func (mr *MockClientMockRecorder) TransactionReceipt(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactionReceipt", reflect.TypeOf((*MockClient)(nil).TransactionReceipt), arg0, arg1) +} + +// TxPoolContent mocks base method. +func (m *MockClient) TxPoolContent(arg0 context.Context) (*TxPoolContent, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TxPoolContent", arg0) + ret0, _ := ret[0].(*TxPoolContent) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TxPoolContent indicates an expected call of TxPoolContent. +func (mr *MockClientMockRecorder) TxPoolContent(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TxPoolContent", reflect.TypeOf((*MockClient)(nil).TxPoolContent), arg0) +} + +// MockPChainClient is a mock of PChainClient interface. +type MockPChainClient struct { + ctrl *gomock.Controller + recorder *MockPChainClientMockRecorder +} + +// MockPChainClientMockRecorder is the mock recorder for MockPChainClient. +type MockPChainClientMockRecorder struct { + mock *MockPChainClient +} + +// NewMockPChainClient creates a new mock instance. +func NewMockPChainClient(ctrl *gomock.Controller) *MockPChainClient { + mock := &MockPChainClient{ctrl: ctrl} + mock.recorder = &MockPChainClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPChainClient) EXPECT() *MockPChainClientMockRecorder { + return m.recorder +} + +// GetAssetDescription mocks base method. +func (m *MockPChainClient) GetAssetDescription(arg0 context.Context, arg1 string, arg2 ...rpc.Option) (*avm.GetAssetDescriptionReply, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetAssetDescription", varargs...) + ret0, _ := ret[0].(*avm.GetAssetDescriptionReply) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAssetDescription indicates an expected call of GetAssetDescription. +func (mr *MockPChainClientMockRecorder) GetAssetDescription(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAssetDescription", reflect.TypeOf((*MockPChainClient)(nil).GetAssetDescription), varargs...) +} + +// GetAtomicUTXOs mocks base method. +func (m *MockPChainClient) GetAtomicUTXOs(arg0 context.Context, arg1 []ids.ShortID, arg2 string, arg3 uint32, arg4 ids.ShortID, arg5 ids.ID, arg6 ...rpc.Option) ([][]byte, ids.ShortID, ids.ID, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1, arg2, arg3, arg4, arg5} + for _, a := range arg6 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetAtomicUTXOs", varargs...) + ret0, _ := ret[0].([][]byte) + ret1, _ := ret[1].(ids.ShortID) + ret2, _ := ret[2].(ids.ID) + ret3, _ := ret[3].(error) + return ret0, ret1, ret2, ret3 +} + +// GetAtomicUTXOs indicates an expected call of GetAtomicUTXOs. +func (mr *MockPChainClientMockRecorder) GetAtomicUTXOs(arg0, arg1, arg2, arg3, arg4, arg5 any, arg6 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1, arg2, arg3, arg4, arg5}, arg6...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAtomicUTXOs", reflect.TypeOf((*MockPChainClient)(nil).GetAtomicUTXOs), varargs...) +} + +// GetBalance mocks base method. +func (m *MockPChainClient) GetBalance(arg0 context.Context, arg1 []ids.ShortID, arg2 ...rpc.Option) (*platformvm.GetBalanceResponse, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetBalance", varargs...) + ret0, _ := ret[0].(*platformvm.GetBalanceResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBalance indicates an expected call of GetBalance. +func (mr *MockPChainClientMockRecorder) GetBalance(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBalance", reflect.TypeOf((*MockPChainClient)(nil).GetBalance), varargs...) +} + +// GetBlock mocks base method. +func (m *MockPChainClient) GetBlock(arg0 context.Context, arg1 ids.ID, arg2 ...rpc.Option) ([]byte, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetBlock", varargs...) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBlock indicates an expected call of GetBlock. +func (mr *MockPChainClientMockRecorder) GetBlock(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlock", reflect.TypeOf((*MockPChainClient)(nil).GetBlock), varargs...) +} + +// GetBlockchainID mocks base method. +func (m *MockPChainClient) GetBlockchainID(arg0 context.Context, arg1 string, arg2 ...rpc.Option) (ids.ID, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetBlockchainID", varargs...) + ret0, _ := ret[0].(ids.ID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBlockchainID indicates an expected call of GetBlockchainID. +func (mr *MockPChainClientMockRecorder) GetBlockchainID(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockchainID", reflect.TypeOf((*MockPChainClient)(nil).GetBlockchainID), varargs...) +} + +// GetContainerByIndex mocks base method. +func (m *MockPChainClient) GetContainerByIndex(arg0 context.Context, arg1 uint64, arg2 ...rpc.Option) (indexer.Container, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetContainerByIndex", varargs...) + ret0, _ := ret[0].(indexer.Container) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetContainerByIndex indicates an expected call of GetContainerByIndex. +func (mr *MockPChainClientMockRecorder) GetContainerByIndex(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContainerByIndex", reflect.TypeOf((*MockPChainClient)(nil).GetContainerByIndex), varargs...) +} + +// GetCurrentValidators mocks base method. +func (m *MockPChainClient) GetCurrentValidators(arg0 context.Context, arg1 ids.ID, arg2 []ids.NodeID, arg3 ...rpc.Option) ([]platformvm.ClientPermissionlessValidator, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetCurrentValidators", varargs...) + ret0, _ := ret[0].([]platformvm.ClientPermissionlessValidator) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCurrentValidators indicates an expected call of GetCurrentValidators. +func (mr *MockPChainClientMockRecorder) GetCurrentValidators(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentValidators", reflect.TypeOf((*MockPChainClient)(nil).GetCurrentValidators), varargs...) +} + +// GetHeight mocks base method. +func (m *MockPChainClient) GetHeight(arg0 context.Context, arg1 ...rpc.Option) (uint64, error) { + m.ctrl.T.Helper() + varargs := []any{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetHeight", varargs...) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHeight indicates an expected call of GetHeight. +func (mr *MockPChainClientMockRecorder) GetHeight(arg0 any, arg1 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeight", reflect.TypeOf((*MockPChainClient)(nil).GetHeight), varargs...) +} + +// GetLastAccepted mocks base method. +func (m *MockPChainClient) GetLastAccepted(arg0 context.Context, arg1 ...rpc.Option) (indexer.Container, uint64, error) { + m.ctrl.T.Helper() + varargs := []any{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetLastAccepted", varargs...) + ret0, _ := ret[0].(indexer.Container) + ret1, _ := ret[1].(uint64) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetLastAccepted indicates an expected call of GetLastAccepted. +func (mr *MockPChainClientMockRecorder) GetLastAccepted(arg0 any, arg1 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastAccepted", reflect.TypeOf((*MockPChainClient)(nil).GetLastAccepted), varargs...) +} + +// GetNodeID mocks base method. +func (m *MockPChainClient) GetNodeID(arg0 context.Context, arg1 ...rpc.Option) (ids.NodeID, *signer.ProofOfPossession, error) { + m.ctrl.T.Helper() + varargs := []any{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetNodeID", varargs...) + ret0, _ := ret[0].(ids.NodeID) + ret1, _ := ret[1].(*signer.ProofOfPossession) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetNodeID indicates an expected call of GetNodeID. +func (mr *MockPChainClientMockRecorder) GetNodeID(arg0 any, arg1 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodeID", reflect.TypeOf((*MockPChainClient)(nil).GetNodeID), varargs...) +} + +// GetRewardUTXOs mocks base method. +func (m *MockPChainClient) GetRewardUTXOs(arg0 context.Context, arg1 *api.GetTxArgs, arg2 ...rpc.Option) ([][]byte, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetRewardUTXOs", varargs...) + ret0, _ := ret[0].([][]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRewardUTXOs indicates an expected call of GetRewardUTXOs. +func (mr *MockPChainClientMockRecorder) GetRewardUTXOs(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRewardUTXOs", reflect.TypeOf((*MockPChainClient)(nil).GetRewardUTXOs), varargs...) +} + +// GetStake mocks base method. +func (m *MockPChainClient) GetStake(arg0 context.Context, arg1 []ids.ShortID, arg2 bool, arg3 ...rpc.Option) (map[ids.ID]uint64, [][]byte, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetStake", varargs...) + ret0, _ := ret[0].(map[ids.ID]uint64) + ret1, _ := ret[1].([][]byte) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetStake indicates an expected call of GetStake. +func (mr *MockPChainClientMockRecorder) GetStake(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStake", reflect.TypeOf((*MockPChainClient)(nil).GetStake), varargs...) +} + +// GetTx mocks base method. +func (m *MockPChainClient) GetTx(arg0 context.Context, arg1 ids.ID, arg2 ...rpc.Option) ([]byte, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetTx", varargs...) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTx indicates an expected call of GetTx. +func (mr *MockPChainClientMockRecorder) GetTx(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTx", reflect.TypeOf((*MockPChainClient)(nil).GetTx), varargs...) +} + +// GetTxFee mocks base method. +func (m *MockPChainClient) GetTxFee(arg0 context.Context, arg1 ...rpc.Option) (*info.GetTxFeeResponse, error) { + m.ctrl.T.Helper() + varargs := []any{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetTxFee", varargs...) + ret0, _ := ret[0].(*info.GetTxFeeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTxFee indicates an expected call of GetTxFee. +func (mr *MockPChainClientMockRecorder) GetTxFee(arg0 any, arg1 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTxFee", reflect.TypeOf((*MockPChainClient)(nil).GetTxFee), varargs...) +} + +// GetUTXOs mocks base method. +func (m *MockPChainClient) GetUTXOs(arg0 context.Context, arg1 []ids.ShortID, arg2 uint32, arg3 ids.ShortID, arg4 ids.ID, arg5 ...rpc.Option) ([][]byte, ids.ShortID, ids.ID, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1, arg2, arg3, arg4} + for _, a := range arg5 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetUTXOs", varargs...) + ret0, _ := ret[0].([][]byte) + ret1, _ := ret[1].(ids.ShortID) + ret2, _ := ret[2].(ids.ID) + ret3, _ := ret[3].(error) + return ret0, ret1, ret2, ret3 +} + +// GetUTXOs indicates an expected call of GetUTXOs. +func (mr *MockPChainClientMockRecorder) GetUTXOs(arg0, arg1, arg2, arg3, arg4 any, arg5 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1, arg2, arg3, arg4}, arg5...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUTXOs", reflect.TypeOf((*MockPChainClient)(nil).GetUTXOs), varargs...) +} + +// IsBootstrapped mocks base method. +func (m *MockPChainClient) IsBootstrapped(arg0 context.Context, arg1 string, arg2 ...rpc.Option) (bool, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "IsBootstrapped", varargs...) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsBootstrapped indicates an expected call of IsBootstrapped. +func (mr *MockPChainClientMockRecorder) IsBootstrapped(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsBootstrapped", reflect.TypeOf((*MockPChainClient)(nil).IsBootstrapped), varargs...) +} + +// IssueTx mocks base method. +func (m *MockPChainClient) IssueTx(arg0 context.Context, arg1 []byte, arg2 ...rpc.Option) (ids.ID, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "IssueTx", varargs...) + ret0, _ := ret[0].(ids.ID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IssueTx indicates an expected call of IssueTx. +func (mr *MockPChainClientMockRecorder) IssueTx(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IssueTx", reflect.TypeOf((*MockPChainClient)(nil).IssueTx), varargs...) +} + +// Peers mocks base method. +func (m *MockPChainClient) Peers(arg0 context.Context, arg1 ...rpc.Option) ([]info.Peer, error) { + m.ctrl.T.Helper() + varargs := []any{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Peers", varargs...) + ret0, _ := ret[0].([]info.Peer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Peers indicates an expected call of Peers. +func (mr *MockPChainClientMockRecorder) Peers(arg0 any, arg1 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Peers", reflect.TypeOf((*MockPChainClient)(nil).Peers), varargs...) +} diff --git a/server/client/pchainclient.go b/server/client/pchainclient.go new file mode 100644 index 0000000..d1117e8 --- /dev/null +++ b/server/client/pchainclient.go @@ -0,0 +1,96 @@ +package client + +import ( + "context" + "strings" + + "github.com/ava-labs/avalanchego/api" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/indexer" + "github.com/ava-labs/avalanchego/utils/rpc" + "github.com/ava-labs/avalanchego/vms/avm" + "github.com/ava-labs/avalanchego/vms/platformvm" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + + "github.com/ava-labs/avalanche-rosetta/constants" +) + +// Interface compliance +var _ PChainClient = &pchainClient{} + +// PChainClient contains all client methods used to interact with avalanchego in order to support P-chain operations in Rosetta. +// +// These methods are cloned from the underlying avalanchego client interfaces, following the example of Client interface used to support C-chain operations. +type PChainClient interface { + // info.Client methods + InfoClient + GetNodeID(context.Context, ...rpc.Option) (ids.NodeID, *signer.ProofOfPossession, error) + GetTxFee(context.Context, ...rpc.Option) (*info.GetTxFeeResponse, error) + + // indexer.Client methods + // Note: we use indexer only to be able to retrieve blocks by height. + // Blocks by ID are retrieved via platformVM.GetBlock, thus ignoring the proposerVM part + // and using Pchain Block ID rather than encompassing Snowman++ block ID + GetContainerByIndex(ctx context.Context, index uint64, options ...rpc.Option) (indexer.Container, error) + GetLastAccepted(context.Context, ...rpc.Option) (indexer.Container, uint64, error) + + // platformvm.Client methods + GetUTXOs( + ctx context.Context, + addrs []ids.ShortID, + limit uint32, + startAddress ids.ShortID, + startUTXOID ids.ID, + options ...rpc.Option, + ) ([][]byte, ids.ShortID, ids.ID, error) + GetAtomicUTXOs( + ctx context.Context, + addrs []ids.ShortID, + sourceChain string, + limit uint32, + startAddress ids.ShortID, + startUTXOID ids.ID, + options ...rpc.Option, + ) ([][]byte, ids.ShortID, ids.ID, error) + GetRewardUTXOs(context.Context, *api.GetTxArgs, ...rpc.Option) ([][]byte, error) + GetHeight(ctx context.Context, options ...rpc.Option) (uint64, error) + GetBalance(ctx context.Context, addrs []ids.ShortID, options ...rpc.Option) (*platformvm.GetBalanceResponse, error) + GetTx(ctx context.Context, txID ids.ID, options ...rpc.Option) ([]byte, error) + GetBlock(ctx context.Context, blockID ids.ID, options ...rpc.Option) ([]byte, error) + IssueTx(ctx context.Context, tx []byte, options ...rpc.Option) (ids.ID, error) + GetStake(ctx context.Context, addrs []ids.ShortID, validatorsOnly bool, options ...rpc.Option) (map[ids.ID]uint64, [][]byte, error) + GetCurrentValidators(ctx context.Context, subnetID ids.ID, nodeIDs []ids.NodeID, options ...rpc.Option) ([]platformvm.ClientPermissionlessValidator, error) + + // avm.Client methods + GetAssetDescription(ctx context.Context, assetID string, options ...rpc.Option) (*avm.GetAssetDescriptionReply, error) +} + +type ( + indexerClient = indexer.Client + platformvmClient = platformvm.Client + infoClient = info.Client +) + +type pchainClient struct { + platformvmClient + indexerClient + infoClient + xChainClient avm.Client +} + +// NewPChainClient returns a new client for Avalanche APIs related to P-chain +func NewPChainClient(_ context.Context, rpcBaseURL, indexerBaseURL string) PChainClient { + rpcBaseURL = strings.TrimSuffix(rpcBaseURL, "/") + + return pchainClient{ + platformvmClient: platformvm.NewClient(rpcBaseURL), + xChainClient: avm.NewClient(rpcBaseURL, constants.XChain.String()), + infoClient: info.NewClient(rpcBaseURL), + indexerClient: indexer.NewClient(indexerBaseURL + "/ext/index/P/block"), + } +} + +func (p pchainClient) GetAssetDescription(ctx context.Context, assetID string, options ...rpc.Option) (*avm.GetAssetDescriptionReply, error) { + return p.xChainClient.GetAssetDescription(ctx, assetID, options...) +} diff --git a/server/cmd/server/README.md b/server/cmd/server/README.md new file mode 100644 index 0000000..7464bd7 --- /dev/null +++ b/server/cmd/server/README.md @@ -0,0 +1,34 @@ +# Avalanche Rosetta: some technical notes + +Some notes to ease up maintenance handover + +## Emerging architecture + +Avalanche-Rosetta used to be centered around the C-chain. Support for P-Chain and X-chain (the latter to allow import/export tracking) has been added later on. To host these chains a structure is emerging: + +- Clients: client package collects all the calls to AvalancheGo node backing Rosetta server. Note that to support P-chain, the indexer must be supported by the AvalancheGo node backing Rosetta. The indexer is required to poll P-chain block by height (C-chain supports that natively, P-chain does not). +- Backends: backends of P-chain and X-chain pull information from client and implements the logic for the various services that Rosetta provides. C-chain has not (yet) a backend since its logic is still implemented at the service level. Creation of C-chain backend is left for a future refactoring. +- Services: the entry points for the API that Rosetta provides. Services route the request to the right backend (P-chain, X-chain or fallback to C-chain logic not yet repackaged into its backend). Moreover it returns the backend response to client. + +## P-chain Block querying and Genesis special case + +P-chain blocks are queried as follows: + +- by hash. In such case GetBlock API of platformVM is hit +- by height. In such case the indexer is used. Note that the indexer returns the whole proposerVM blocks. So block retrieved by proposerVM is further processed to extract the P-chain inner block and only the latter is returned. + +**P-chain Genesis Data must not be polled from APIs**. The reasons are that: + +- `NetworkStatus` endpoint must return Genesis ID and Timestamp if AvalancheGo P-chain client has not complete bootstrapping yet +- If AvalancheGo P-chain node has not done bootstrapping, GetBlock endpoint cannot serve P-chain Genesis block. + +Genesis Block information is parsed directly from AvalancheGo codebase rather than be called from GetBlock endpoint. + +## Snowman++ handling + +Snowman++ headers (aka proposerVM header) are not returned to client. P-chain blocks are referenced by: + +- Their height +- Their BlockID (not the full proposerVM block ID) + +ProposerVM headers are used only to retrieved timestamp for pre-Banff blocks. Note that indexer timestamp is never used as it cannot guarantee a deployment-independent information. diff --git a/server/cmd/server/config.go b/server/cmd/server/config.go index 1e96359..d5273c3 100644 --- a/server/cmd/server/config.go +++ b/server/cmd/server/config.go @@ -3,15 +3,17 @@ package main import ( "encoding/json" "errors" + "fmt" "os" + "github.com/ethereum/go-ethereum/common" + "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/mapper" "github.com/ava-labs/avalanche-rosetta/service" - ethcommon "github.com/ethereum/go-ethereum/common" ) var ( - errMissingRPC = errors.New("avalanche rpc endpoint is not provided") errInvalidMode = errors.New("invalid rosetta mode") errGenesisBlockRequired = errors.New("genesis block hash is not provided") errInvalidTokenAddress = errors.New("invalid token address provided") @@ -22,7 +24,8 @@ var ( type config struct { Mode string `json:"mode"` - RPCEndpoint string `json:"rpc_endpoint"` + RPCBaseURL string `json:"rpc_base_url"` + IndexerBaseURL string `json:"indexer_base_url"` ListenAddr string `json:"listen_addr"` NetworkName string `json:"network_name"` ChainID int64 `json:"chain_id"` @@ -31,6 +34,7 @@ type config struct { IngestionMode string `json:"ingestion_mode"` TokenWhiteList []string `json:"token_whitelist"` + BridgeTokenList []string `json:"bridge_tokens"` IndexUnknownTokens bool `json:"index_unknown_tokens"` ValidateERC20Whitelist bool `json:"validate_erc20_whitelist"` } @@ -48,7 +52,7 @@ func readConfig(path string) (*config, error) { return cfg, err } -func (c *config) ApplyDefaults() { +func (c *config) applyDefaults() { if c.Mode == "" { c.Mode = service.ModeOnline } @@ -57,8 +61,12 @@ func (c *config) ApplyDefaults() { c.IngestionMode = service.StandardIngestion } - if c.RPCEndpoint == "" { - c.RPCEndpoint = "http://localhost:9650" + if c.RPCBaseURL == "" { + c.RPCBaseURL = "http://localhost:9650" + } + + if c.IndexerBaseURL == "" { + c.IndexerBaseURL = c.RPCBaseURL } if c.ListenAddr == "" { @@ -66,15 +74,21 @@ func (c *config) ApplyDefaults() { } } -func (c *config) Validate() error { - c.ApplyDefaults() +func (c *config) validate() error { + if !(c.Mode == service.ModeOffline || c.Mode == service.ModeOnline) { + return errInvalidMode + } + + if c.Mode == service.ModeOffline && c.ChainID == 0 { + return errors.New("chainID must be configured when offline mode is selected") + } - if c.RPCEndpoint == "" { - return errMissingRPC + if c.NetworkName == "" { + return errors.New("network name not provided") } - if !(c.Mode == service.ModeOffline || c.Mode == service.ModeOnline) { - return errInvalidMode + if !mapper.IsSupportedHRP(c.NetworkName) { + return fmt.Errorf("network name %q not mapping to any known network ID", c.NetworkName) } if c.GenesisBlockHash == "" { @@ -83,12 +97,25 @@ func (c *config) Validate() error { if len(c.TokenWhiteList) != 0 { for _, token := range c.TokenWhiteList { - if !ethcommon.IsHexAddress(token) { + if !common.IsHexAddress(token) { return errInvalidTokenAddress } } } + if len(c.BridgeTokenList) != 0 { + for _, token := range c.BridgeTokenList { + if !common.IsHexAddress(token) { + return errInvalidTokenAddress + } + + // include all bridge tokens within list of tokens whitelisted for indexing + if !mapper.EqualFoldContains(c.TokenWhiteList, token) { + c.TokenWhiteList = append(c.TokenWhiteList, c.BridgeTokenList...) + } + } + } + if !(c.IngestionMode == service.AnalyticsIngestion || c.IngestionMode == service.StandardIngestion) { return errInvalidIngestionMode } @@ -99,9 +126,9 @@ func (c *config) Validate() error { return nil } -func (c *config) ValidateWhitelistOnlyValidErc20s(cli client.Client) error { +func (c *config) validateWhitelistOnlyValidErc20s(cli client.Client) error { for _, token := range c.TokenWhiteList { - ethAddress := ethcommon.HexToAddress(token) + ethAddress := common.HexToAddress(token) symbol, decimals, err := cli.GetContractInfo(ethAddress, true) if err != nil { return err @@ -112,3 +139,8 @@ func (c *config) ValidateWhitelistOnlyValidErc20s(cli client.Client) error { } return nil } + +func (c *config) avalancheNetworkID() uint32 { + // error checked in config.validate + return mapper.HRPToChainID[c.NetworkName] +} diff --git a/server/cmd/server/main.go b/server/cmd/server/main.go index 6a3e7fb..6ae0bee 100644 --- a/server/cmd/server/main.go +++ b/server/cmd/server/main.go @@ -4,23 +4,35 @@ import ( "bytes" "context" "flag" - "io/ioutil" + "io" "log" "math/big" "net/http" + "time" + "github.com/ava-labs/avalanchego/ids" "github.com/coinbase/rosetta-sdk-go/asserter" "github.com/coinbase/rosetta-sdk-go/server" "github.com/coinbase/rosetta-sdk-go/types" "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/constants" "github.com/ava-labs/avalanche-rosetta/mapper" "github.com/ava-labs/avalanche-rosetta/service" + "github.com/ava-labs/avalanche-rosetta/service/backend/cchainatomictx" + "github.com/ava-labs/avalanche-rosetta/service/backend/pchain" + "github.com/ava-labs/avalanche-rosetta/service/backend/pchain/indexer" + + pmapper "github.com/ava-labs/avalanche-rosetta/mapper/pchain" ) var ( cmdName = "avalanche-rosetta" cmdVersion = service.MiddlewareVersion + + // This is set moderately high to account for debug_trace calls. + defaultReadTimeout = 3 * time.Minute + defaultWriteTimeout = 3 * time.Minute ) var opts struct { @@ -48,11 +60,15 @@ func main() { if err != nil { log.Fatal("config read error:", err) } - if err := cfg.Validate(); err != nil { + + // set defaults for unspecified configs + cfg.applyDefaults() + + if err := cfg.validate(); err != nil { log.Fatal("config validation error:", err) } - apiClient, err := client.NewClient(context.Background(), cfg.RPCEndpoint) + cChainClient, err := client.NewClient(context.Background(), cfg.RPCBaseURL) if err != nil { log.Fatal("client init error:", err) } @@ -63,7 +79,7 @@ func main() { // // TODO: Only perform this check after the underlying node is bootstrapped if cfg.Mode == service.ModeOnline && cfg.ValidateERC20Whitelist { - if err := cfg.ValidateWhitelistOnlyValidErc20s(apiClient); err != nil { + if err := cfg.validateWhitelistOnlyValidErc20s(cChainClient); err != nil { log.Fatal("token whitelist validation error:", err) } } @@ -72,12 +88,7 @@ func main() { if cfg.ChainID == 0 { log.Println("chain id is not provided, fetching from rpc...") - - if cfg.Mode == service.ModeOffline { - log.Fatal("cant fetch chain id in offline mode") - } - - chainID, err := apiClient.ChainID(context.Background()) + chainID, err := cChainClient.ChainID(context.Background()) if err != nil { log.Fatal("cant fetch chain id from rpc:", err) } @@ -85,64 +96,94 @@ func main() { } var assetID string - var AP5Activation uint64 + var ap5Activation uint64 switch cfg.ChainID { case mapper.FlareChainID: assetID = mapper.FlareAssetID - AP5Activation = uint64(0) + ap5Activation = uint64(0) case mapper.CostwoChainID: assetID = mapper.CostwoAssetID - AP5Activation = uint64(0) + ap5Activation = uint64(0) case mapper.LocalFlareChainID: assetID = mapper.LocalFlareAssetID - AP5Activation = uint64(0) + ap5Activation = uint64(0) default: log.Fatal("invalid ChainID:", cfg.ChainID) } - if cfg.NetworkName == "" { - log.Println("network name is not provided, fetching from rpc...") - - if cfg.Mode == service.ModeOffline { - log.Fatal("cant fetch network name in offline mode") - } - - networkName, err := apiClient.GetNetworkName(context.Background()) - if err != nil { - log.Fatal("cant fetch network name:", err) - } - cfg.NetworkName = networkName + // Note: Rosetta is currently configure with capitalized NetworkNames + // and service network requests are carried our with capital case. + // while avalanchego requires lower-case network names. + // We convert to lower case upon specific calls to avalanchego clients. + networkP := &types.NetworkIdentifier{ + Blockchain: service.BlockchainName, + Network: cfg.NetworkName, + SubNetworkIdentifier: &types.SubNetworkIdentifier{ + Network: constants.PChain.String(), + }, } - - network := &types.NetworkIdentifier{ + networkC := &types.NetworkIdentifier{ Blockchain: service.BlockchainName, Network: cfg.NetworkName, } - asserter, err := asserter.NewServer( - mapper.OperationTypes, // supported operation types - true, // historical balance lookup - []*types.NetworkIdentifier{network}, // supported networks - []string{}, // call methods - false, // mempool coins + avaxAssetID, err := ids.FromString(assetID) + if err != nil { + log.Fatal("parse asset id failed:", err) + } + + pChainClient := client.NewPChainClient(context.Background(), cfg.RPCBaseURL, cfg.IndexerBaseURL) + pIndexerParser, err := indexer.NewParser(pChainClient, cfg.avalancheNetworkID()) + if err != nil { + log.Fatal("unable to initialize p-chain indexer parser:", err) + } + + pChainBackend, err := pchain.NewBackend( + cfg.Mode, + pChainClient, + pIndexerParser, + avaxAssetID, + networkP, + cfg.avalancheNetworkID(), ) if err != nil { - log.Fatal("server asserter init error:", err) + log.Fatal("unable to initialize p-chain backend:", err) } + cChainAtomicTxBackend := cchainatomictx.NewBackend(cChainClient, avaxAssetID, cfg.avalancheNetworkID()) + serviceConfig := &service.Config{ Mode: cfg.Mode, ChainID: big.NewInt(cfg.ChainID), - NetworkID: network, + NetworkID: networkC, GenesisBlockHash: cfg.GenesisBlockHash, FlareAssetID: assetID, - AP5Activation: AP5Activation, + AP5Activation: ap5Activation, IndexUnknownTokens: cfg.IndexUnknownTokens, IngestionMode: cfg.IngestionMode, TokenWhiteList: cfg.TokenWhiteList, + BridgeTokenList: cfg.BridgeTokenList, + } + + var operationTypes []string + operationTypes = append(operationTypes, mapper.OperationTypes...) + operationTypes = append(operationTypes, pmapper.OperationTypes...) + + asserter, err := asserter.NewServer( + operationTypes, // supported operation types + true, // historical balance lookup + []*types.NetworkIdentifier{ // supported networks + networkP, + networkC, + }, // supported networks + []string{}, // call methods + false, // mempool coins + ) + if err != nil { + log.Fatal("server asserter init error:", err) } - handler := configureRouter(serviceConfig, asserter, apiClient) + handler := configureRouter(serviceConfig, asserter, cChainClient, pChainBackend, cChainAtomicTxBackend) if cfg.LogRequests { handler = inspectMiddleware(handler) } @@ -155,23 +196,32 @@ func main() { service.BlockchainName, cfg.ChainID, cfg.NetworkName, - cfg.RPCEndpoint, + cfg.RPCBaseURL, ) log.Printf("starting rosetta server at %s\n", cfg.ListenAddr) - log.Fatal(http.ListenAndServe(cfg.ListenAddr, router)) + server := &http.Server{ + Addr: cfg.ListenAddr, + Handler: router, + ReadTimeout: defaultReadTimeout, + WriteTimeout: defaultWriteTimeout, + } + + log.Fatal(server.ListenAndServe()) } func configureRouter( serviceConfig *service.Config, asserter *asserter.Asserter, apiClient client.Client, + pChainBackend *pchain.Backend, + cChainAtomicTxBackend *cchainatomictx.Backend, ) http.Handler { - networkService := service.NewNetworkService(serviceConfig, apiClient) - blockService := service.NewBlockService(serviceConfig, apiClient) - accountService := service.NewAccountService(serviceConfig, apiClient) + networkService := service.NewNetworkService(serviceConfig, apiClient, pChainBackend) + blockService := service.NewBlockService(serviceConfig, apiClient, pChainBackend) + accountService := service.NewAccountService(serviceConfig, apiClient, pChainBackend, cChainAtomicTxBackend) mempoolService := service.NewMempoolService(serviceConfig, apiClient) - constructionService := service.NewConstructionService(serviceConfig, apiClient) + constructionService := service.NewConstructionService(serviceConfig, apiClient, pChainBackend, cChainAtomicTxBackend) callService := service.NewCallService(serviceConfig, apiClient) return server.NewRouter( @@ -187,12 +237,12 @@ func configureRouter( // Inspect middlware used to inspect the body of requets func inspectMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, err := ioutil.ReadAll(r.Body) + body, err := io.ReadAll(r.Body) if err != nil { panic(err) } body = bytes.TrimSpace(body) - r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + r.Body = io.NopCloser(bytes.NewBuffer(body)) log.Printf("[DEBUG] %s %s: %s\n", r.Method, r.URL.Path, body) next.ServeHTTP(w, r) diff --git a/server/constants/chain_id_aliases.go b/server/constants/chain_id_aliases.go new file mode 100644 index 0000000..c842646 --- /dev/null +++ b/server/constants/chain_id_aliases.go @@ -0,0 +1,41 @@ +package constants + +import "errors" + +var ErrUnknownChainIDAlias = errors.New("unknown chain ID alias") + +type ChainIDAlias uint16 + +const ( + // values are ASCII values because why not? + AnyChain ChainIDAlias = 0 // default value, with some usage in some P-Chain APIs + PChain ChainIDAlias = 80 // "P" + CChain ChainIDAlias = 67 // "C" + XChain ChainIDAlias = 88 // "X" +) + +func (ni ChainIDAlias) String() string { + switch ni { + case PChain: + return "P" + case CChain: + return "C" + case XChain: + return "X" + default: + return "" // this specific value signal some P-Chain API that any source ChainID is fine + } +} + +func FromString(s string) (ChainIDAlias, error) { + switch { + case s == "P": + return PChain, nil + case s == "C": + return CChain, nil + case s == "X": + return XChain, nil + default: + return AnyChain, ErrUnknownChainIDAlias + } +} diff --git a/server/constants/network.go b/server/constants/network.go new file mode 100644 index 0000000..9963090 --- /dev/null +++ b/server/constants/network.go @@ -0,0 +1,7 @@ +package constants + +const ( + MainnetNetwork = "flare" + TestnetNetwork = "fuji" // Using fuji for backwards compatibility with tests + LocalNetwork = "localflare" +) diff --git a/server/docker/docker-compose.yml b/server/docker/docker-compose.yml new file mode 100644 index 0000000..8f38424 --- /dev/null +++ b/server/docker/docker-compose.yml @@ -0,0 +1,118 @@ +services: + validator1: + image: ghcr.io/flare-foundation/go-flare:v1.11.13-rc0 + environment: + LOG_LEVEL: info + NETWORK_ID: localflare + AUTOCONFIGURE_PUBLIC_IP: 0 + AUTOCONFIGURE_BOOTSTRAP: 0 + PUBLIC_IP: 10.20.30.1 + EXTRA_ARGUMENTS: --staking-tls-cert-file=/app/avalanchego/staking/local/staker1.crt --staking-tls-key-file=/app/avalanchego/staking/local/staker1.key --staking-signer-key-file=/app/avalanchego/staking/local/signer1.key + ports: + - 9650:9650 + - 9651:9651 + volumes: + - ./localflare:/app/avalanchego/staking/local:ro + networks: + nodes: + ipv4_address: 10.20.30.1 + healthcheck: + test: curl localhost:9650/ext/health + + validator2: + image: ghcr.io/flare-foundation/go-flare:v1.11.13-rc0 + environment: + LOG_LEVEL: info + NETWORK_ID: localflare + AUTOCONFIGURE_PUBLIC_IP: 0 + AUTOCONFIGURE_BOOTSTRAP: 0 + PUBLIC_IP: 10.20.30.2 + BOOTSTRAP_IPS: 10.20.30.1:9651 + BOOTSTRAP_IDS: NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg + EXTRA_ARGUMENTS: --staking-tls-cert-file=/app/avalanchego/staking/local/staker2.crt --staking-tls-key-file=/app/avalanchego/staking/local/staker2.key --staking-signer-key-file=/app/avalanchego/staking/local/signer2.key + volumes: + - ./localflare:/app/avalanchego/staking/local:ro + networks: + nodes: + ipv4_address: 10.20.30.2 + + validator3: + image: ghcr.io/flare-foundation/go-flare:v1.11.13-rc0 + environment: + LOG_LEVEL: info + NETWORK_ID: localflare + AUTOCONFIGURE_PUBLIC_IP: 0 + AUTOCONFIGURE_BOOTSTRAP: 0 + STAKER_INDEX: 3 + PUBLIC_IP: 10.20.30.3 + BOOTSTRAP_IPS: 10.20.30.1:9651 + BOOTSTRAP_IDS: NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg + EXTRA_ARGUMENTS: --staking-tls-cert-file=/app/avalanchego/staking/local/staker3.crt --staking-tls-key-file=/app/avalanchego/staking/local/staker3.key --staking-signer-key-file=/app/avalanchego/staking/local/signer3.key + volumes: + - ./localflare:/app/avalanchego/staking/local:ro + networks: + nodes: + ipv4_address: 10.20.30.3 + + validator4: + image: ghcr.io/flare-foundation/go-flare:v1.11.13-rc0 + environment: + LOG_LEVEL: info + NETWORK_ID: localflare + AUTOCONFIGURE_PUBLIC_IP: 0 + AUTOCONFIGURE_BOOTSTRAP: 0 + PUBLIC_IP: 10.20.30.4 + BOOTSTRAP_IPS: 10.20.30.1:9651 + BOOTSTRAP_IDS: NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg + EXTRA_ARGUMENTS: --staking-tls-cert-file=/app/avalanchego/staking/local/staker4.crt --staking-tls-key-file=/app/avalanchego/staking/local/staker4.key --staking-signer-key-file=/app/avalanchego/staking/local/signer4.key + volumes: + - ./localflare:/app/avalanchego/staking/local:ro + networks: + nodes: + ipv4_address: 10.20.30.4 + + validator5: + image: ghcr.io/flare-foundation/go-flare:v1.11.13-rc0 + environment: + LOG_LEVEL: info + NETWORK_ID: localflare + AUTOCONFIGURE_PUBLIC_IP: 0 + AUTOCONFIGURE_BOOTSTRAP: 0 + PUBLIC_IP: 10.20.30.5 + BOOTSTRAP_IPS: 10.20.30.1:9651 + BOOTSTRAP_IDS: NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg + EXTRA_ARGUMENTS: --staking-tls-cert-file=/app/avalanchego/staking/local/staker5.crt --staking-tls-key-file=/app/avalanchego/staking/local/staker5.key --staking-signer-key-file=/app/avalanchego/staking/local/signer5.key + volumes: + - ./localflare:/app/avalanchego/staking/local:ro + networks: + nodes: + ipv4_address: 10.20.30.5 + + rosetta: + image: ${ROSETTA_IMAGE} + environment: + LOG_LEVEL: info + NETWORK_ID: localflare + AUTOCONFIGURE_PUBLIC_IP: 0 + AUTOCONFIGURE_BOOTSTRAP: 0 + PUBLIC_IP: 10.20.30.6 + BOOTSTRAP_IPS: 10.20.30.1:9651 + BOOTSTRAP_IDS: NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg + FLARE_LOCAL_TXS_ENABLED: true + START_ROSETTA_SERVER_AFTER_BOOTSTRAP: ${START_ROSETTA_SERVER_AFTER_BOOTSTRAP} + MODE: ${MODE} + ports: + - 8080:8080 + depends_on: + - validator1 + networks: + nodes: + ipv4_address: 10.20.30.6 + +networks: + nodes: + driver: bridge + ipam: + config: + - subnet: 10.20.30.0/24 + gateway: 10.20.30.254 \ No newline at end of file diff --git a/server/docker/entrypoint_rosetta.sh b/server/docker/entrypoint_rosetta.sh index 53981be..0b0429a 100644 --- a/server/docker/entrypoint_rosetta.sh +++ b/server/docker/entrypoint_rosetta.sh @@ -56,7 +56,7 @@ if [ "$MODE" = "online" ]; then sleep 1 done - jq --arg c "${ROSETTA_FLARE_ENDPOINT}" '.rpc_endpoint=$c' "${ROSETTA_CONFIG_PATH}" | sponge "${ROSETTA_CONFIG_PATH}" + jq --arg c "${ROSETTA_FLARE_ENDPOINT}" '.rpc_base_url=$c' "${ROSETTA_CONFIG_PATH}" | sponge "${ROSETTA_CONFIG_PATH}" fi diff --git a/server/docker/localflare/README.md b/server/docker/localflare/README.md new file mode 100644 index 0000000..b128b95 --- /dev/null +++ b/server/docker/localflare/README.md @@ -0,0 +1,207 @@ +# Local Network Staking Keys + +This folder contains the staking keys (TLS and BLS) referenced by the local network genesis. + +**NOTE:** These keys **are** intended to be public. They **must** only be used for local test networks. + +Each staker's Base64 encoded keys are included below for ease of use with the `--staking-tls-key-file-content`, `--staking-tls-cert-file-content` and `--staking-signer-key-file-content` flags. + +## Staker1 + +### NodeID + +``` +NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg +``` + +### Key Base64 + +``` +LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS0FJQkFBS0NBZ0VBeW1Fa2NQMXRHS1dCL3pFMElZaGEwZEp2UFplc2s3c3k2UTdZN25hLytVWjRTRDc3CmFwTzJDQnB2NXZaZHZjY0VlQ2VhKzBtUnJQdTlNZ1hyWkcwdm9lejhDZHE1bGc4RzYzY2lTcjFRWWFuL0pFcC8KbFZOaTNqMGlIQ1k5ZmR5dzhsb1ZkUitKYWpaRkpIQUVlK2hZZmFvekx3TnFkTHlldXZuZ3kxNWFZZjBXR3FVTQpmUjREby85QVpnQ2pLMkFzcU9RWVVZb2Zqcm9JUEdpdUJ2VDBFeUFPUnRzRTFsdGtKQjhUUDBLYVJZMlhmMThFCkhpZGgrcm0xakJYT1g3YlgrZ002U2J4U0F3YnZ5UXdpbG9ncnVadkxlQmkvTU5qcXlNZkNiTmZaUmVHR0JObnEKSXdxM3FvRDR1dUV0NkhLc0NyQTZNa2s4T3YrWWlrT1FWR01GRjE5OCt4RnpxZy9FakIzbjFDbm5NNCtGcndHbQpTODBkdTZsNXVlUklFV0VBQ0YrSDRabU96WDBxS2Qxb2RCS090dmlSNkRQOVlQbElEbDVXNTFxV1BlVElIZS8zCjhBMGxpN3VDTVJOUDdxdkZibnlHM3d1TXEyUEtwVTFYd0gzeU5aWFVYYnlZenlRVDRrTkFqYXpwZXRDMWFiYVoKQm5QYklSKzhHZG16OUd4SjJDazRDd0h6c3cvRkxlOVR0Z0RpR3ZOQU5SalJaZUdKWHZ6RWpTVG5FRGtxWUxWbgpVUk15RktIcHdJMzdzek9Ebms2K0hFWU9QbFdFd0tQU2h5cTRqZFE3bnNEY3huZkZveWdGUjVuQ0RJNmlFaTA1CmN6SVhiSFp2anBuME9qcjhsKzc5Qmt6Z1c0VDlQVFJuTU1PUU5JQXMxemRmQlV1YU1aOFh1amh2UTlNQ0F3RUEKQVFLQ0FnRUF1VU00TXQ4cjhiWUJUUFZqL1padlhVakFZS2ZxYWNxaWprcnpOMGtwOEM0Y2lqWnR2V0MrOEtnUwo3R0YzNnZTM0dLOVk1dFN3TUtTNnk0SXp2RmxmazJINFQ2VVU0MU9hU0E5bEt2b25EV0NybWpOQW5CZ2JsOHBxCjRVMzRXTEdnb2hycExiRFRBSkh4dGF0OXoxZ2hPZGlHeG5EZ0VVRmlKVlA5L3UyKzI1anRsVEttUGhzdHhnRXkKbUszWXNTcDNkNXhtenE0Y3VYRi9mSjF2UWhzWEhETHFIdDc4aktaWkErQVdwSUI1N1ZYeTY3eTFiazByR25USwp4eFJuT2FPT0R1YkpneHFNRVExV2tMczFKb3c5U3NwZDl2RGdoUHp0NFNOTXpvckI4WURFU01pYjE3eEY2aVhxCmpGajZ4NkhCOEg3bXA0WDNSeU1ZSnVvMnc2bHB6QnNFbmNVWXBLaHFNYWJGMEkvZ2lJNVZkcFNEdmtDQ09GZW4KbldaTFY5QWkveDd0VHEvMEYrY1ZNNjlNZ2ZlOGlZeW1xbGZkNldSWklUS2ZWaU5IQUxsRy9QcTl5SEpzejdOZwpTOEJLT0R0L3NqNFEweEx0RkRUL0RtcFA1MGlxN1NpUzE0b2JjS2NRcjhGQWpNL3NPWS9VbGc0TThNQTdFdWdTCnBESndMbDZYRG9JTU1DTndaMUhHc0RzdHpteDVNZjUwYlM0dGJLNGlaemNwUFg1UkJUbFZkbzlNVFNnbkZpenAKSWkxTmpITHVWVkNTTGIxT2pvVGd1MGNRRmlXRUJDa0MxWHVvUjhSQ1k2aVdWclVINEdlem5pN2NrdDJtSmFOQQpwZDYvODdkRktFM2poNVQ2alplSk1KZzVza1RaSFNvekpEdWFqOXBNSy9KT05TRDA2c0VDZ2dFQkFQcTJsRW1kCmcxaHBNSXFhN2V5MXVvTGQxekZGemxXcnhUSkxsdTM4TjY5bVlET0hyVi96cVJHT3BaQisxbkg3dFFKSVQvTDEKeExOMzNtRlZxQ3JOOHlVbVoraVVXaW9hSTVKWjFqekNnZW1WR2VCZ29kd1A5TU9aZnh4ckRwMTdvVGRhYmFFcQo3WmFCWW5ZOHhLLzRiQ3h1L0I0bUZpRjNaYThaVGQvKzJ5ZXY3Sk0rRTNNb3JXYzdyckttMUFwZmxmeHl0ZGhPCkpMQmlxT2Nxb2JJM2RnSHl6ZXNWYjhjVDRYQ3BvUmhkckZ3b3J0MEpJN3J5ZmRkZDQ5dk1KM0VsUmJuTi9oNEYKZjI0Y1dZL3NRUHEvbmZEbWVjMjhaN25WemExRDRyc3pOeWxZRHZ6ZGpGMFExbUw1ZEZWbnRXYlpBMUNOdXJWdwpuVGZ3dXlROFJGOVluWU1DZ2dFQkFNNmxwTmVxYWlHOWl4S1NyNjVwWU9LdEJ5VUkzL2VUVDR2Qm5yRHRZRis4Cm9oaUtnSXltRy92SnNTZHJ5bktmd0pPYkV5MmRCWWhDR0YzaDl6Mm5jOUtKUUQvc3U3d3hDc2RtQnM3WW9EaU0KdXpOUGxSQW1JMFFBRklMUENrNDh6L2xVUWszci9NenUwWXpSdjdmSTRXU3BJR0FlZlZQRHF5MXVYc0FURG9ESgphcmNFa05ENUxpYjg5THg3cjAyRWV2SkpUZGhUSk04bUJkUmw2d3BOVjN4QmR3aXM2N3VTeXVuRlpZcFNpTXc3CldXaklSaHpoTEl2cGdENzhVdk52dUppMFVHVkVqVHFueHZ1VzNZNnNMZklrODBLU1IyNFVTaW5UMjd0Ly94N3oKeXpOa283NWF2RjJobTFmOFkvRXBjSEhBYXg4TkFRRjV1dVY5eEJOdnYzRUNnZ0VBZFMvc1JqQ0syVU5wdmcvRwowRkx0V0FncmNzdUhNNEl6alZ2SnMzbWw2YVYzcC81dUtxQncwVlVVekdLTkNBQTRUbFhRa09jUnh6VnJTNkhICkZpTG4yT0NIeHkyNHExOUdhenowcDdmZkUzaHUvUE1PRlJlY04rVkNoZDBBbXRuVHRGVGZVMnNHWE1nalp0TG0KdUwzc2lpUmlVaEZKWE9FN05Vb2xuV0s1dTJZK3RXQlpwUVZKY0N4MGJ1c054NytBRXR6blpMQzU4M3hhS0p0RApzMUs3SlJRQjdqVTU1eHJDMEc5cGJrTXlzbTBOdHlGemd3bWZpcEJIVmxDcHl2ZzZEQ3hkOEZodmhOOVplYTFiCmZoa2MwU0pab3JIQzVoa3FweWRKRG1sVkNrMHZ6RUFlUU00Qzk0WlVPeXRibmpRbm1YcDE0Q05BU1lxTFh0ZVEKdWVSbzB3S0NBUUFHMEYxMEl4Rm0xV290alpxdlpKZ21RVkJYLzBmclVQY3hnNHZwQjVyQzdXUm03TUk2WVF2UgpMS0JqeldFYWtIdjRJZ2ZxM0IrZms1WmNHaVJkNnhTZG41cjN3S1djR2YzaC8xSkFKZEo2cXVGTld0VnVkK04zCnpZemZsMVllcUZDdlJ3RDhzc2hlTlkzQlYvVTdhU3ROZDJveTRTNSt3WmYyWW9wTFNSV1VWNC9tUXdkSGJNQUIKMXh0Mno1bEROQmdkdng4TEFBclpyY1pKYjZibGF4RjBibkF2WUF4UjNoQkV6eFovRGlPbW9GcGRZeVUwdEpRVQpkUG1lbWhGZUo1UHRyUnh0aW1vaHdnQ0VzVC9UQVlodVVKdVkyVnZ6bkVXcHhXdWNiaWNLYlQySkQwdDY3bUVCCnNWOSs4anFWYkNsaUJ0ZEJhZHRib2hqd2trb1IzZ0J4QW9JQkFHM2NadU5rSVdwRUxFYmVJQ0tvdVNPS04wNnIKRnMvVVhVOHJvTlRoUFI3dlB0amVEMU5ETW1VSEpyMUZHNFNKclNpZ2REOHFOQmc4dy9HM25JMEl3N2VGc2trNQo4bU5tMjFDcER6T04zNlpPN0lETWo1dXlCbGoydCtJeGwvdUpZaFlTcHVOWHlVVE1tK3JrRkowdmRTVjRmakxkCkoybTMwanVZbk1pQkJKZjdkejVNOTUrVDB4aWNHV3lWMjR6VllZQmJTbzBOSEVHeHFlUmhpa05xWk5Qa29kNmYKa2ZPSlpHYWxoMkthSzVSTXBacEZGaFova1c5eFJXTkpaeUNXZ2tJb1lrZGlsTXVJU0J1M2xDcms4cmRNcEFMMAp3SEVjcTh4d2NnWUNTMnFrOEh3anRtVmQzZ3BCMXk5VXNoTXIzcW51SDF3TXBVNUMrbk0yb3kzdlNrbz0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K +``` + +### Cert Base64 + +``` +LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZOekNDQXg4Q0NRQzY4N1hGeHREUlNqQU5CZ2txaGtpRzl3MEJBUXNGQURCL01Rc3dDUVlEVlFRR0V3SlYKVXpFTE1Ba0dBMVVFQ0F3Q1Rsa3hEekFOQmdOVkJBY01Ca2wwYUdGallURVFNQTRHQTFVRUNnd0hRWFpoYkdGaQpjekVPTUF3R0ExVUVDd3dGUjJWamEyOHhEREFLQmdOVkJBTU1BMkYyWVRFaU1DQUdDU3FHU0liM0RRRUpBUllUCmMzUmxjR2hsYmtCaGRtRnNZV0p6TG05eVp6QWdGdzB4T1RBM01ESXhOakV5TVRWYUdBOHpNREU1TURjeE1ERTIKTVRJeE5Wb3dPakVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrNVpNUkF3RGdZRFZRUUtEQWRCZG1GcwpZV0p6TVF3d0NnWURWUVFEREFOaGRtRXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDRHdBd2dnSUtBb0lDCkFRREtZU1J3L1cwWXBZSC9NVFFoaUZyUjBtODlsNnlUdXpMcER0anVkci81Um5oSVB2dHFrN1lJR20vbTlsMjkKeHdSNEo1cjdTWkdzKzcweUJldGtiUytoN1B3SjJybVdEd2JyZHlKS3ZWQmhxZjhrU24rVlUyTGVQU0ljSmoxOQozTER5V2hWMUg0bHFOa1VrY0FSNzZGaDlxak12QTJwMHZKNjYrZURMWGxwaC9SWWFwUXg5SGdPai8wQm1BS01yCllDeW81QmhSaWgrT3VnZzhhSzRHOVBRVElBNUcyd1RXVzJRa0h4TS9RcHBGalpkL1h3UWVKMkg2dWJXTUZjNWYKdHRmNkF6cEp2RklEQnUvSkRDS1dpQ3U1bTh0NEdMOHcyT3JJeDhKczE5bEY0WVlFMmVvakNyZXFnUGk2NFMzbwpjcXdLc0RveVNUdzYvNWlLUTVCVVl3VVhYM3o3RVhPcUQ4U01IZWZVS2Vjemo0V3ZBYVpMelIyN3FYbTU1RWdSCllRQUlYNGZobVk3TmZTb3AzV2gwRW82MitKSG9NLzFnK1VnT1hsYm5XcFk5NU1nZDcvZndEU1dMdTRJeEUwL3UKcThWdWZJYmZDNHlyWThxbFRWZkFmZkkxbGRSZHZKalBKQlBpUTBDTnJPbDYwTFZwdHBrR2M5c2hIN3daMmJQMApiRW5ZS1RnTEFmT3pEOFV0NzFPMkFPSWE4MEExR05GbDRZbGUvTVNOSk9jUU9TcGd0V2RSRXpJVW9lbkFqZnV6Ck00T2VUcjRjUmc0K1ZZVEFvOUtIS3JpTjFEdWV3TnpHZDhXaktBVkhtY0lNanFJU0xUbHpNaGRzZG0rT21mUTYKT3Z5WDd2MEdUT0JiaFAwOU5HY3d3NUEwZ0N6WE4xOEZTNW94bnhlNk9HOUQwd0lEQVFBQk1BMEdDU3FHU0liMwpEUUVCQ3dVQUE0SUNBUUFxTDFUV0kxUFRNbTNKYVhraGRUQmU4dHNrNytGc0hBRnpUY0JWQnNCOGRrSk5HaHhiCmRsdTdYSW0rQXlHVW4wajhzaXo4cW9qS2JPK3JFUFYvSW1USDVXN1EzNnJYU2Rndk5VV3BLcktJQzVTOFBVRjUKVDRwSCtscFlJbFFIblRhS011cUgzbk8zSTQwSWhFaFBhYTJ3QXd5MmtEbHo0NmZKY3I2YU16ajZaZzQzSjVVSwpaaWQrQlFzaVdBVWF1NVY3Q3BDN0dNQ3g0WWRPWldXc1QzZEFzdWc5aHZ3VGU4MWtLMUpvVEgwanV3UFRCSDB0CnhVZ1VWSVd5dXdlTTFVd1lGM244SG13cTZCNDZZbXVqaE1ES1QrM2xncVp0N2VaMVh2aWVMZEJSbFZRV3pPYS8KNlFZVGtycXdQWmlvS0lTdHJ4VkdZams0MHFFQ05vZENTQ0l3UkRnYm5RdWJSV3Jkc2x4aUl5YzVibEpOdU9WKwpqZ3Y1ZDJFZVVwd1VqdnBadUVWN0ZxUEtHUmdpRzBqZmw2UHNtczlnWVVYZCt5M3l0RzlIZW9ETm1MVFNUQkU0Cm5DUVhYOTM1UDIveE91b2s2Q3BpR3BQODlEWDd0OHlpd2s4TEZOblkzcnZ2NTBuVnk4a2VyVmRuZkhUbW9NWjkKL0lCZ29qU0lLb3Y0bG1QS2RnekZmaW16aGJzc1ZDYTRETy9MSWhURjdiUWJIMXV0L09xN25wZE9wTWpMWUlCRQo5bGFndlJWVFZGd1QvdXdyQ2NYSENiMjFiL3B1d1Y5NFNOWFZ3dDdCaGVGVEZCZHR4SnJSNGpqcjJUNW9kTGtYCjZuUWNZOFYyT1Q3S094bjBLVmM2cGwzc2FKVExtTCtILzNDdEFhbzlOdG11VURhcEtJTlJTVk55dmc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== +``` + +## Signer1 + +### Public Key + +``` +0xa75054f6d9ea8419775ca88730ff01f068a538dae1ccb9eeee4324c7928ee79b42746e2568809b6c7d8f124eb966629e +``` + +### Proof of Possession + +``` +0xa945d9805ce5f817c706e9765e8802ef422cfde97a046dbb2cf9279f773abedf66e56081dcbc813cce7d8fd36049f80a062946b94645c0f1a73b878c1c4a98a9d6824326e4633c764eb6f8f1d387ebdde3d60674fdab56fffae290f98b5bd8e2 +``` + +### Key Base64 + +``` +TG9jYWxGbGFyZU5ldHdvcmtUZXN0VmFsaWRhdG9yMDE= +``` + +## Staker2 + +### NodeID + +``` +NodeID-MFrZFVCXPv5iCn6M9K6XduxGTYp891xXZ +``` + +### Key Base64 + +``` +LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS2dJQkFBS0NBZ0VBM1U2RWV0SjJ1amJrZllrZ0ZETXN6MXlUVEZsVUd5OGsrZjRtUW9pZHdRZUdGakFZClg4YVNWaVlsbDNEUHM1dzRLU2JzZ2hIb2VRNmVtV054WHU1T2g0YmVoWmhhUTQ4SjQ2VUpuWkJDbElOTkJ2aEoKVDA2ZjFMSnJEeS9pUUxNRDZnV1ZxVEFVdDNiRjBEYnB6RExUQnBhMytVdEplbDErTXU1SFB4azhXSUwzbG1mUwpiVWY1dGlqWHEvc0k0QVI2aWVLejZweHdkOUxBMkQyWVBkUXM5UUpQQVc5c05pTmRQTTBGWUNpdW5pS2pTbHlTCmYrS1lzMG1YVk9QZHRNRE1XQWFaM3d3SElaOFhvdlZQNzkwRzArM2lsQ05ueHEvb1Rsa3lGbFBOUGxYamdiZkcKdWorTEV2UUVJZWYraHZLc0pnS2p0MkVQWGNUZk5vcnZ4OS9qZmhobXZ6TksrYXhGcTg4Q0xoWWNDcjhrSEtUMApRWnRza0p3WjRTL1J1WkRlcCtEZ1ROQ3JJR2R3bUpSMlVvT0hsbkRZQWJkVmdoYkVITzczMUJXaVo2L0VEOTZSCmVGZitBYUpPOEV6dVJ6ZEFOUkdCRldMbCtkelJwNnlsWHZoWVBmc1REb0Z6MXBGRGFuYjZTUFdXMWQyZEV1Zk8KdXFoZC83d1dhNFlBcXluT1JQN3pIc1FVWHZDb3VNQ3FqalZZWXZWaGxIRVI3eFJJcEhQdTFhejQ5R0dodXMxUwozZnJqZ0NUZk5YeWpqMktPRitMR2F6TWoySmR6b3hyVElYMWVDcndtaXhLOVhianBKYWp1bkFyeGtQSWdKUzFjCnQxU2NsdVJpZEE5Q3NvZ0lxK0ZUWFFDU3FOV1lJaG9yeVMxcnBVMzQxSW53amZhTmRlUUlxaWNZcXNNQ0F3RUEKQVFLQ0FnQU5HVU9nSFdybmxLNHJlLzFKRk1wWEw2eU1QVkZNRnB0Q3JMZEpBdHNMZk0yRDdLN1VwR1V1OGkwUgpiSnp1alpXSllnTm5vM1cyREpaNGo3azdIREhMdGNEZitXZUdUaVlRc2trQ2FYSjNaZG9lU24zVVV0d0U4OWFBClhKNHdwQ2ZjSng1M21CL3h4L2JuWHdpeGpHU1BKRWFaVzhwcWtyUVFnYWYzNVI5OFFhd3oyOHRKcXBQdUl6YTQKdURBTFNsaVNacmV0Y0RyNzdKNTdiaEhmdnZvMk9qL0EzdjV4cWVBdjVCYW9YV0FRZmc1YUxXYUNhVUFPaEpHUApkYmsrcEphenN4aFNhbHpWc1p2dGlrV0Q5Zm9jZXgwSkZadGoyQytReTVpNlY1VnpWaFFVTG5OMXZLTVhxUmZCCmhnQzdyZ3RnYUpHV0hnbVJ6RUJGOHkxRUVFMWZvaGJvMnNxa0c0b016M2pCWjRvNE1BRFFjcGZLMnFjaGdybmsKT3hJUy91VThzemR1ODRpSDhzNkYvSGwxKzg3am5xNk85UmUwaU1TdXZ5VWJqQUVlOENtOVAvYTVNMVg5ZXl6dwpXU1hTUFpCd0tTUm9QM3d1eWNiRW9uVFdRblFIZHd5U1krSXZkdGdsaUVEaEtyVmJaR25rczV6bWFhSXlkVy95CkxTMlM5SlJNNVkrWHAwdlYzbkdsRWVoQ1VkclhvUTFEei9BaUhuV0hqYnhvQ0ZHdDBxTDZDT0p6aUFHZlVYS2EKY1E1aURkN3pjMkozbTJaNmM4Vzh4a1BKZSsxZG1OV2ZHSHJqYThEU0h0VGNEWTZBcWQ5OFZ1MG5pdThQQzdieApBdncrKzZKMndHN0xOODlyZ1IwdVA3YXM5Q3g0a0hIc09Gd3ArbEtPRFZlMmR3MHZBUUtDQVFFQTdtb05DalA2CjVQa1NrU05QaS9qdzFZL0ZDd0JvSkV6bDNRNWZ0d0dya1laRlJMQlR2Q0NsaTJqaGV4YUMwRDkreWpzVmFMLzIKVmFwNDMveGk1NWlwbmlxdjhjMVdBYzV4RmgrNmhDeWRTNks5b3dEeGxITjc1TUdMcm1yWWpZKzNhTWRvMTVEbQp4NWJ6bk9MTHlNVVc0QWsrNzdNVHcxMmZhZC83TDBBTlh1bUZGajZ5ZGNTOFBIbWhKbG16NVZlZ1d6NWIxS0dRCksvL3BoY3VPbTM0OXhla3Q3SjVrS1JiREVxTE9sWnYvRUlBZENCUU00VTNkNlAvMnZVVXk1bktZRzBGMXhlYUMKbGVWcHIxRVBvRUkrWGtUeStqam9hQnM3aVVIcGNEMzU5WFFDV0xuaXdmMVlmdHRrOXpKcDdtNnRSL0dlYWJsawp1bm5INXp5Rmt3emxRd0tDQVFFQTdhRnROc2pMMFVFWGx5QllqQ1JPb1B1NmthL280UXlFYVd2TUhjaVh1K0d2Ck03VFFDRjJpOW9lUVhBQkl5VE5vUXIrcE5qQVJib1k4cDArOVpmVjhRR2x2SDZhd1cyTU56RDA3bGc5aHdzalkKSk9DSTY0WHhaajE4M0doSGdOOS9jRTRQWEJyUUNxUExQQ0tkVjY2eUFSOVdObTlWYTNZOVhmL1J2Y29MaU5CMQpGQWc1YmhiTlFNblIzOG5QSnM5K3N1U3FZQjh4QURLdndtS0Vkb255K1dJTS9HUXlZWmlEbFhFajhFZldRb3VNCndBb2s2VnVoczZjdUxpSEh6WEZSNFk2UkNXUmIybmYyVnJ6V29wejJCcDAySWVIWTBVWnNaZUtucWhhOWR0VXUKWkNJdDJNWlVFTHhpaDlKUyt3ekNYOEJKazN4ZWRpODl6T1pLUng0TWdRS0NBUUVBeHFuVUo5WmNrSVFEdHJFbgp6Y2tvVmF5eFVwT0tOQVZuM1NYbkdBWHFReDhSaFVVdzRTaUxDWG5odWNGdVM3MDlGNkxZR2lzclJ3TUFLaFNUCkRjMG1PY2YwU0pjRHZnbWFMZ2RPVW1raXdTM2d1MzFEMEtIU2NUSGVCUDYvYUdhRFBHbzlzTExydXhETCtzVDUKYmxqYzBONmpkUFZSMkkraEVJWTFOcEEzRkFtZWZvVE1ERnBkU0Q5Snl6MGdMRkV5TEJYd1MyUTlVSXkwdUdxQQpjSTFuU0EwZjJYVzZuSXA5RG9CZmlFY3U2VDczOGcxVEZrTGVVUk5KTlRuK1NnemZOb2I3Ym1iQUZjdk9udW43CkRWMWx2d1BSUERSRFpNeWNkYWxZcmREWEFuTWlxWEJyeFo0b0tiMERpd0NWU0xzczVUQXZBb1licTA5akJncG0KZTd4WkpRS0NBUUVBM2Y3bDBiMXFzNVdVM1VtSmozckh2aHNOWTljcnZ6cjdaS1VoTGwzY2F0aGUzZlk0TnVpTApPcmJReFRJNnpVUnFUWmxTRWw1N21uNXJvWDZjR09scVo1NVlBd0N0VnVMRjNCMEVVcDhTSEcrWGhYUUNWYzF2CkJLM0N2UUhxY3RuWTYyanhib0ZhQSthYkVoWGdXaTdJK3NWMHZDdnNhQlV4SldTOVpBbWlGdkZ2dndRajZ0WUEKY0Z0YTV5OVlpQkJtYytldHgxaThaVXYwNktzeXhxNy9QNzA3Rm5yZ21rNXA5eTJZZm53T0RXTGpYZkRjSk9uRwp1ZGdnQzFiaG11c1hySm1NbzNLUFlSeWJGTk1ielJUSHZzd1Y2emRiWDc3anU1Y3dQWFU3RVEzOVpleU1XaXlHCkVwQjdtQm1FRGljUVczVi9CdnEwSU1MbmdFbFA4UHFBZ1FLQ0FRRUFxNEJFMVBGTjZoUU9xZTBtY084ZzltcXUKenhsMk1NMEtiMkFCRThmeFEydzRGeTdnNDJOb3pEVVcxMy9NTjdxMUkrQXdNaGJsNEliMlFJbUVNVHVGYUhQWQpBM09abG5FOUwwb2k0Rkkra0cyZUpPQi8rNXBIU3VmL2pyWi80Z0FSSyt1Yy9DRGVhSWxqUC9ueHcwY1grc0YrCkhqWDRPYjQvQ3lFSWVJVUdkT0dzN2c5a2Yrb2lyWHJ5dURjWnhsLzJmUU94cXZhOWRoaEJMaFBYRzNvdFNwMFQKRDkweEMxbFNQTElIZitWVWlGOWJMTXRVcDRtZUdjZ3dwWFBWalJWNWNibExyUDlQeGJldmxoRzJEM3ZuT0s5QQo4aldJOVAxdU5CRUFVVFNtWFY4cmVNWU95TlhKSDhZYmJUNHlpYXJXbmFRTTBKMGlwV3dYR0VlV2Fndi9hQT09Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== +``` + +### Cert Base64 + +``` +LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZOekNDQXg4Q0NRQzY4N1hGeHREUlNqQU5CZ2txaGtpRzl3MEJBUXNGQURCL01Rc3dDUVlEVlFRR0V3SlYKVXpFTE1Ba0dBMVVFQ0F3Q1Rsa3hEekFOQmdOVkJBY01Ca2wwYUdGallURVFNQTRHQTFVRUNnd0hRWFpoYkdGaQpjekVPTUF3R0ExVUVDd3dGUjJWamEyOHhEREFLQmdOVkJBTU1BMkYyWVRFaU1DQUdDU3FHU0liM0RRRUpBUllUCmMzUmxjR2hsYmtCaGRtRnNZV0p6TG05eVp6QWdGdzB4T1RBM01ESXhOakV5TVRsYUdBOHpNREU1TURjeE1ERTIKTVRJeE9Wb3dPakVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrNVpNUkF3RGdZRFZRUUtEQWRCZG1GcwpZV0p6TVF3d0NnWURWUVFEREFOaGRtRXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDRHdBd2dnSUtBb0lDCkFRRGRUb1I2MG5hNk51UjlpU0FVTXl6UFhKTk1XVlFiTHlUNS9pWkNpSjNCQjRZV01CaGZ4cEpXSmlXWGNNK3oKbkRncEp1eUNFZWg1RHA2WlkzRmU3azZIaHQ2Rm1GcERqd25qcFFtZGtFS1VnMDBHK0VsUFRwL1VzbXNQTCtKQQpzd1BxQlpXcE1CUzNkc1hRTnVuTU10TUdscmY1UzBsNlhYNHk3a2MvR1R4WWd2ZVdaOUp0Ui9tMktOZXIrd2pnCkJIcUo0clBxbkhCMzBzRFlQWmc5MUN6MUFrOEJiMncySTEwOHpRVmdLSzZlSXFOS1hKSi80cGl6U1pkVTQ5MjAKd014WUJwbmZEQWNobnhlaTlVL3YzUWJUN2VLVUkyZkdyK2hPV1RJV1U4MCtWZU9CdDhhNlA0c1M5QVFoNS82Rwo4cXdtQXFPM1lROWR4TjgyaXUvSDMrTitHR2EvTTByNXJFV3J6d0l1Rmh3S3Z5UWNwUFJCbTJ5UW5CbmhMOUc1CmtONm40T0JNMEtzZ1ozQ1lsSFpTZzRlV2NOZ0J0MVdDRnNRYzd2ZlVGYUpucjhRUDNwRjRWLzRCb2s3d1RPNUgKTjBBMUVZRVZZdVg1M05HbnJLVmUrRmc5K3hNT2dYUFdrVU5xZHZwSTlaYlYzWjBTNTg2NnFGMy92QlpyaGdDcgpLYzVFL3ZNZXhCUmU4S2k0d0txT05WaGk5V0dVY1JIdkZFaWtjKzdWclBqMFlhRzZ6VkxkK3VPQUpOODFmS09QCllvNFg0c1pyTXlQWWwzT2pHdE1oZlY0S3ZDYUxFcjFkdU9rbHFPNmNDdkdROGlBbExWeTNWSnlXNUdKMEQwS3kKaUFpcjRWTmRBSktvMVpnaUdpdkpMV3VsVGZqVWlmQ045bzExNUFpcUp4aXF3d0lEQVFBQk1BMEdDU3FHU0liMwpEUUVCQ3dVQUE0SUNBUUNRT2R3RDdlUkl4QnZiUUhVYyttMFRSekVhMTdCQ2ZjazFZMld3TjNUWlhER1NrUFZFCjB1dWpBOFNMM3FpOC9DVExHUnFJOVUzZ1JaSmYrdEpQQkYvUDAyMVBFbXlhRlRTNGh0eGNEeFR4dVp2MmpDbzkKK1hoVUV5dlJXaXRUbW95MWVzcTNta290VlFIZVRtUXZ3Q3NRSkFoY3RWQS9oUmRKd21NUHMxQjhReE9VSTZCcQpTT0JIYTlDc1hJelZPRnY4RnFFOTFQWkEybnMzMHNLUVlycm5iSDk5YXBmRjVXZ2xMVW95UHd4ZjJlM0FBQ2g3CmJlRWRrNDVpdnZLd2k1Sms4bnI4NUtESFlQbHFrcjBiZDlFaGw4eHBsYU5CZE1QZVJ1ZnFCRGx6dGpjTEozd28KbW5ydDk1Z1FNZVNvTEhZM1VOc0lSamJqNDN6SW11N3E5di9ERDlwcFFwdTI2YVJEUm1CTmdMWkE5R001WG5iWgpSRmkzVnhMeXFhc0djU3phSHd6NWM3dk9CT2tPZGxxY1F6SVNSdldEeGlOMUhrQUwraGtpUUN1TWNoZ09SQWdNCnd6UG9vYThyZld0TElwT1hNcHd1VkdiLzhyR05MRVBvdm9DSzl6NmMrV1oremtSbzQrM1RRa09NWTY2WGh0N3IKQWhseTNsZXIrVHlnNmE1alhUOTJXS0MvTVhCWUF5MlpRTm95MjA0a05LZXZjSDdSMmNTa3hJVGQzbjVFYWNOeQo1TUF0Q05JazdKd2VMQ2g5ckxyTFVCdCtpNG40NHNQK0xWaGZXSGVtbmdBOENvRjRuNmVRMHBwMGl4WlRlbjBqCjR1TjBHMk5mK0plR01scW9PYkxXZElPZEgvcGJEcHBYR29aYUtLRGQ3K2JBNzRGbGU1VWg3KzFlM0E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== +``` + +## Signer2 + +### Public Key + +``` +0xadb0203ebc76627d28fb9440272a2701b85f5c5d4266352686ea42666f5026f1fdabab59529932f1eddb317a6f7435f9 +``` + +### Proof of Possession + +``` +0x926a1c21953babb12189ec88bfab5cb0060b385efa04c51abe4a3bc42266f80eebb84bcfaacfc8f075560ab444bda8de17bf9013ec20a80dfdfda4ae047a0b39669d153dcee94eb964fe194f5d55c5bba5939ff9ba07b728b6733d696c33ac5a +``` + +### Key Base64 + +``` +TG9jYWxGbGFyZU5ldHdvcmtUZXN0VmFsaWRhdG9yMDI= +``` + +## Staker3 + +### NodeID + +``` +NodeID-NFBbbJ4qCmNaCzeW7sxErhvWqvEQMnYcN +``` + +### Key Base64 + +``` +LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKSndJQkFBS0NBZ0VBdkpsUTA2QjI1RkJkb0VYVlg2Y25XU3pTbmtOL0hlaDJSWkZETjFOWFZpMlJVbVRiCjJvRGZJWlVMMlA3VUx0d09OS2RmVm9DbTgxbWEzdDdZTlFKTVBDYzdYYy9ZSmVFVEo5ZU5pQ0R0d29zQ0tiWkIKTkNhS1cyWVpodVh3QXRwL3pGMExYNG5pTjRjM2tkT0MxdUNZQ3lzMzl6Wi9NV1haUVlDWHdaaSthbEZKeE96dQp5R3lSaHBtdW9FZzkzc0hyUk9pa0diVGJYOU1VU0w5UEhhR1NtTUU1ZWlMWkhSaXUrcm42cXRQZkJsY0Zyd05jCkVycDN1UGpCWUxQVGVIaFNZK2ljcmZPWDRpdjNNRDhleWtYWUdJQWM2MlVPQkQ3SVVaOEZxd1U0bmpqbWlid0EKTmtUTVRCR015a2F5UUFMR0NOYzhiVGNxaUZHZ1UzTVptcy9qVjFCbHZzQjhDRTRuTkJxZjEwSnRpOEsvNWNTRgplanB3SlM5d29HMGw0cTBwNkJ2bmgwUHJ3OHdRSEVRNUV3SUwveGxDTTVYNms5a3JVeXROcmlBWUNXTTd4VXh1CktaUjRyWEhFWDh2TU1iSWVXZk1SdmZaWmowRUxTN0o1VUFGZmk0dE9nbS9BaCtJVkZhSWxvbGJPVUFHb2FscHIKK1g2YUY5QkxOdWtjbEIxeG00eE50TnFjakRBTzQxSHExRlNqSW96cUJtYlhCK3N3ZUtmM1NkRTdVNkVjRG9LdwpPelFpUFFHRmJhbFY2MXh2N25ZL0xxOXE5VzAxY0NHeDdEbmt4RDk0R05KaDBWcHRoUDJ5NDdmYjIyQ0VWTVVmCk1WcHk1RzNnYzRqVThJUHQrbzNReGZ1b0x0M0ZNLzUyMHAvR0dQbU5ZNDE3ak41YWhmSWpHZ0tHNGJzQ0F3RUEKQVFLQ0FnQSt1SElUM3lLSzdWc2xxUE83K3Rmd0pTTHFOU0k2TFF2Z09OMzBzVWV6UmpZMUE0dkdEK09reEcrTApPN3dPMVduNEFzMkc5QVFSbS9RUU9HWUl3dm5kYTJLbjRTNU44cHN2UGRVNHQxSzZ4d1h5SDBWeDlYcy95Q1duCklpTCtuL0d1WWljZEg3cldvcVpOWGR6K1h2VFJpZzd6clBFQjJaQTE0M0VVbGhxRk93RmdkemMxK2owdldUNmsKMlVHU0trVjJ4ak9FeFF2THcyUFVpYUxqQk0rKzgwdU5IYmU4b0cvWXZDN3J6c2cxMEl6NFZoS3h1OGVEQVY4MgpMTGVnTWN1Y3BFZ3U1WHJXWWE2MElkbTRoUi9Iamh1UUFTeDNKdlh4aHdRWWl3VDRRWTRSc2k4VDNTOWdBTm9rCmp2eEtvMkYrb1MzY1dHTlJzR3UwTk93SCt5anNWeU1ZYXpjTE9VZXNBQWU4NXR0WGdZcjAyK1ovdU1ueHF0T0YKZ2pJSFkzWDVRWmJENGw0Z2J3eCtQTGJqc2o0S0M2cjN5WnJyNTFQZExVckJ2b3FCaHF3dUNrc2RhTW50V0dNRQp1MFYvb29KaTQrdXpDWXpOMDZqRmZBRlhhMnBXelZCNXlLdzFkNnlZaTlVL2JQZDR4bjFwaExVTUhyQzJidmRNCkg4UDE4Z0FTNnJrV24rYWdlaVdSSG1rZjR1b0tndjNQck1qaWprQmFHcGY2eGp2NiswUTM5M2pkVklDN3dnSlYKOFcwaTFmMUF3djY4MDg5bUhCRWFyUFR2M2d6MzkyNTFXRkNQTlFoRXVTeTc5TGk1emp3T3ByWlhTME1uSlhibQpCMDBJUFRJdzUxS3Vhb3VlV3pZMXBBMklGUS8wc0gzZm8ySmhEMHBwMWdJMERkZTdFUUtDQVFFQTdSVmdOZWxrCjNIM1pKc29PZk9URmEwM2xuWHVFZlRBeXhoRUVDUno2NGs1NHBTYkVXVjczUEllWUJ5WnZuc3JLUnlkcFlXVVAKQ3AvbUtoQUpINFVDZjJoelkwR3lPNy9ENitIRUtaZENnNmEwRE5LY2tBb0ZrQmZlT2xMSkxqTFZBVzJlRVZ4egp0bEZ0Ky9XQkU5MEdDdkU1b3ZYdUVoWEdhUHhDUHA1Z2lJTjVwaFN6U0Q1NTdid3dPeVB3TktGWjdBbzc3VU5LCmt6NkV6Y3ZRZ3FiMjA1U1JSS0dwUzgvVC85TGNMc1VZVmtCZllRL0JheWpmZk8rY1FGNHZINXJCNHgvOC9UN3QKdVVhNzl1WStMZUdIZ1RTRklBdWk5TEVLNXJ5Ly8yaERKSU5zSXRZTWtzMVFvNFN1dTIzcE91R2VyamlGVEtXbAptT0lvRm1QbWJlYkFjd0tDQVFFQXk2V2FKY3pQY0tRUS9ocWdsUXR4VTZWZlA0OVpqVUtrb2krT0NtdHZLRHRzCjdwSmZJa3VsdWhuWUdlcUZmam5vcHc1YmdaSE5GbTZHY2NjNUN3QnRONldrMUtubk9nRElnM2tZQ3JmanRLeS8KQlNTVjNrTEVCdmhlOUVKQTU2bUZnUDdSdWZNYkhUWGhYUEdMa2dFN0pCWmoyRUt4cDFxR1lZVlplc1RNRndETQpLRUh3eklHY0ZreVpzZDJqcHR5TFlxY2ZES3pUSG1GR2N3MW1kdExXQVVkcHYzeHJTM0d2ckNiVU1xSW9kalJkCnFrcmcvZC9rUXBLN0Ezb0xPV2ZhNmVCUTJCWHFhV0IxeDEzYnpKMldsc2h4SkFaMXAxb3pLaWk1QlE5cnZ3V28KbXVJNXZkN282QTlYc2w4UXpsdVNTU1BpK05oalo2NGdNQnJYY2lSdm1RS0NBUUIvZEI1azNUUDcxU3dJVGxlNwpqTUVWRHF1Q0hnVDd5QTJEcldJZUJCWmIweFBJdFM2WlhSUk0xaGhFdjhVQitNTUZ2WXBKY2FyRWEzR3c2eTM4ClkrVVQyWE11eVFLb1hFOVhYK2UwOUR3dHlsREJFL2hXOXd4R2lvNU5qSFBiQWpqQXE4MXVSK1ZzL2huQ2Voa0sKTktncStjT2lkOU9rcFZBazRIZzhjYWd6dTNxS2JsWnpZQ0xzUzE4aWJBK1dPNmU3M1VTYUtMTE90YTF2ZFVLQworbjkyLzBlWlBjOWxralRHTXZWcnIwbUdGTlV4dU9haVZUYlFVNEFNbXBWNnlCZXpvbDYvUmpWR2hXQkhPei95CktteE9hWTJuekptdU1mOUtTKzVyd0FGWWY4NkNhOUFXbTRuZVhsWVJMT1ZWWWpXTU01WjF2aGRvT1N5VDNPRGoKOUVsQkFvSUJBR0NSUGFCeEYyajlrOFU3RVN5OENWZzEwZzNNeHhWU0pjbDJyVzlKZEtOcVVvUnF5a3Z6L1RsYgphZnNZRjRjOHBKTWJIczg1T1R4SzJ0djNNWmlDOGtkeDk5Q1VaTDQvZ3RXOVJXWkh2dVY5Q1BQQ1hvTFB2QzdsCjlmanp0ZDFrcUpiN3ZxM2psdGJxSnR5dytaTVpuRmJIZXo4Z21TZVhxS056M1hOM0FLUmp6MnZEb1JFSTRPQSsKSUorVVR6Y2YyOFRESk5rWTF0L1FGdDBWM0tHNTVwc2lwd1dUVlRtb1JqcG5DemFiYUg1czVJR05FbFd3cG9mZgpGbWxXcFIzcW5vZEt4R3RETVM0WS9LQzJaRFVLQVUrczZ1Ry9ZbWtpUDZMZFBxY2tvZDRxSzhLT1JmMUFSOGRMCkJ6WGhHSklTSURNb25rZU1MTThNWmQwSnpXSWwzdmtDZ2dFQVBCa0V4ZDJqNFZZNXMrd1FKZGlNdG81RERvY2kKa0FFSXZJa0pZOUkrUHQybHBpblFLQWNBQVhidnVlYUprSnBxMzFmNlk2NnVvazhRbkQwOWJJUUNBQmpqbEl2ZQpvN3FRK0g4L2lxSFFYMW5iSER6SW5hRGRhZDNqWXRrV1VIakhQYUtnMi9rdHlOa0Z0bFNIc2t2dkNFVnc1YWp1CjgwUTN0UnBRRzlQZTRaUmpLRXpOSXBNWGZRa3NGSDBLd2p3QVZLd1lKTHFaeHRORVlvazRkcGVmU0lzbkgvclgKcHdLL3B5QnJGcXhVNlBVUlVMVUp1THFSbGFJUlhBVTMxUm1Kc1ZzMkpibUk3Q2J0ajJUbXFBT3hzTHNpNVVlSgpjWnhjVEF1WUNOWU11ODhrdEh1bDhZSmRCRjNyUUtVT25zZ1cxY3g3SDZMR2J1UFpUcGc4U2J5bHR3PT0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K +``` + +### Cert Base64 + +``` +LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZOekNDQXg4Q0NRQzY4N1hGeHREUlNqQU5CZ2txaGtpRzl3MEJBUXNGQURCL01Rc3dDUVlEVlFRR0V3SlYKVXpFTE1Ba0dBMVVFQ0F3Q1Rsa3hEekFOQmdOVkJBY01Ca2wwYUdGallURVFNQTRHQTFVRUNnd0hRWFpoYkdGaQpjekVPTUF3R0ExVUVDd3dGUjJWamEyOHhEREFLQmdOVkJBTU1BMkYyWVRFaU1DQUdDU3FHU0liM0RRRUpBUllUCmMzUmxjR2hsYmtCaGRtRnNZV0p6TG05eVp6QWdGdzB4T1RBM01ESXhOakV5TWpKYUdBOHpNREU1TURjeE1ERTIKTVRJeU1sb3dPakVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrNVpNUkF3RGdZRFZRUUtEQWRCZG1GcwpZV0p6TVF3d0NnWURWUVFEREFOaGRtRXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDRHdBd2dnSUtBb0lDCkFRQzhtVkRUb0hia1VGMmdSZFZmcHlkWkxOS2VRMzhkNkhaRmtVTTNVMWRXTFpGU1pOdmFnTjhobFF2WS90UXUKM0E0MHAxOVdnS2J6V1pyZTN0ZzFBa3c4Snp0ZHo5Z2w0Uk1uMTQySUlPM0Npd0lwdGtFMEpvcGJaaG1HNWZBQwoybi9NWFF0ZmllSTNoemVSMDRMVzRKZ0xLemYzTm44eFpkbEJnSmZCbUw1cVVVbkU3TzdJYkpHR21hNmdTRDNlCndldEU2S1FadE50ZjB4Ukl2MDhkb1pLWXdUbDZJdGtkR0s3NnVmcXEwOThHVndXdkExd1N1bmU0K01GZ3M5TjQKZUZKajZKeXQ4NWZpSy9jd1B4N0tSZGdZZ0J6clpRNEVQc2hSbndXckJUaWVPT2FKdkFBMlJNeE1FWXpLUnJKQQpBc1lJMXp4dE55cUlVYUJUY3htYXorTlhVR1crd0h3SVRpYzBHcC9YUW0yTHdyL2x4SVY2T25BbEwzQ2diU1hpCnJTbm9HK2VIUSt2RHpCQWNSRGtUQWd2L0dVSXpsZnFUMlN0VEswMnVJQmdKWXp2RlRHNHBsSGl0Y2NSZnk4d3gKc2g1Wjh4Rzk5bG1QUVF0THNubFFBVitMaTA2Q2I4Q0g0aFVWb2lXaVZzNVFBYWhxV212NWZwb1gwRXMyNlJ5VQpIWEdiakUyMDJweU1NQTdqVWVyVVZLTWlqT29HWnRjSDZ6QjRwL2RKMFR0VG9Sd09nckE3TkNJOUFZVnRxVlhyClhHL3Vkajh1cjJyMWJUVndJYkhzT2VURVAzZ1kwbUhSV20yRS9iTGp0OXZiWUlSVXhSOHhXbkxrYmVCemlOVHcKZyszNmpkREYrNmd1M2NVei9uYlNuOFlZK1kxampYdU0zbHFGOGlNYUFvYmh1d0lEQVFBQk1BMEdDU3FHU0liMwpEUUVCQ3dVQUE0SUNBUUFlMmtDMEhqS1pVK2RsblUyUmxmQnBCNFFnenpyRkU1TjlBOEYxTWxFNHZWM0F6Q2cxClJWZEhQdm5pWHpkTmhEaWlmbEswbC9jbnJGdjJYMVR6WU1yckE2NzcvdXNIZjJCdzB4am0vaXBIT3Q1Vis0VE4KbVpBSUE0SVBsMDlnUDI4SVpMYzl4U3VxNEZvSGVNOE9UeGh0dE9sSU5ocXBHOVA1ZDZiUGV6VzZaekkzQ2RQUApDRjY5eEs0R0Zsai9OUW5Bb0ZvZ2lkNG9qWVlOVGovY000UFlRVTJLYnJsekx5UHVVay9DZ3dlZlhMTUg4Ny9ICmUza1BEZXY4MFRqdjJQbTVuRDkzN2ZaZmdyRW95b2xLeGlSVmNmWlZNeFI3cWhQaGl6anVlRDBEQWtmUUlzN0wKWVZTeXgvcWpFdjJiQllhaW01UlFha1VlSFIxWHU1WGovazV6cjMzdDk3OWVkZTUwYnlRcmNXbTRINUp4bkVwRApKeEpuRmZET1U2bzE0U0tHSFNyYW81WjRDM2RJNTVETTg0V0xBU25sTUk1Qks0WHRTM25vdExOekc4ZGZXV2hUCjltMEhjcnkrd1BORGNHcjhNdGoxbG9zLzBiTURxTUhDNGpjRlcxaHJYQ1VVczlSWXpFK04veG9xd0NRU2dOMVAKRTczdVhUeVNXajVvdk1SNVRQRjZQaGNmdExCL096aXFPN0Z2ZXJFQnB2R0dIVUFuVVQ2MUp0am9kalhQYkVkagowVmd5TU9CWTJ5NTNIVFhueDNkeGVGWmtVZFJYL1ZaWXk4dE1LM01UWSs3VUlVNWNXWW5DWkFvNUxOY2MwdWtSClM2V1M5KzZlYVE2WFJqaGZOVWp4OWE3RnpxYXBXZHRUZWRwaXBtQlAxTmphcDNnMjlpVXVWbkxRZWc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== +``` + +## Signer3 + +### Public Key + +``` +0x83a2f6ccaae3ab909f68cdd2da7d41843aa6723fb24202f65a4b3048eb20e065c30aad5b290e8238a7155a440fde780b +``` + +### Proof of Possession + +``` +0x99e3394f0a161167879218a180d1efe3f571a119b02ecc44334fd8aefd561e7c938d85e7b288c3d92ddb94b7983b56a811ce43a876985278e08039865c952aeba3f1ff31fa8642d4ed2e94cda4e6696c5bc411f8f2bf93928e5b9c7e55ad6cb7 +``` + +### Key Base64 + +``` +TG9jYWxGbGFyZU5ldHdvcmtUZXN0VmFsaWRhdG9yMDM= +``` + +## Staker4 + +### NodeID + +``` +NodeID-GWPcbFJZFfZreETSoWjPimr846mXEKCtu +``` + +### Key Base64 + +``` +LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS1FJQkFBS0NBZ0VBMlp3NkF4eE5wNC9Oc1E0eDlFMit6bHpLa0F4OC9zMnk0bmJlakVpTlZ3ZDd6Z0NCCklqNnE3WHB4cDZrVzFSWEp1Um1jYjhZZUdFQS9Bb3lrcE5hb0dTWE03TkF6MG9oa1BCSDVlRHhqdU1aeEc1WlkKQzl6MUVUMXFlNWhGZDZJZW44U0FvRUNrd1pVK3U5Ukp4Y3dlWmNpVnFicGtjN3dOYjhvUVRMR1BHanZzWGF1UwoxQlpiV013WGI2WUMxMVdnOEIyVU5qMEJGclJkSGRDVHRqZEZoUlF4alpzZXAvS1NrbmlBRlE3RThHUW1TdmhRCm5uaE1xQm4xN0NEYkhOTWdqRUxKV3NKSHpqS2dQY2dWZHlZdWtKaFdDc2htTGV1VEZoamFqd09GNnlNcFlpc0EKZ2w4TS9abzJsc2plNndNc1BYRnBTekRnelpnemtWc3hrV2VkdmREQkFCQ0diS28yQjFZclVVelAwWnpTR25VNgpHUmp0TXViZ0pETmVISms3NTdDdFFqL2Rxclgwb3JLdStWdnlQZFhhMDRhQkxVeXUvRkJqcGYrVU5XQVVkUEJmCnJVbEZtVnNoM2lPbERnVkFoZ3Rtd3Jac3lqSmU3MU51WlJ1cG13a2RTYmtJdWtXS1ozekkxeDkrcFZlRXUzMDcKMHNNTGtkTjRkaGR4OVhoWkZqUnRwV3hhMXRIY3d3ajRONVY4SmhVT3VZVkhFSVhMYzU2QWMrUXRaUmRubHROOApMbFlNWXA1N2xaU1I0OHZ4cTZwbHJBRXA5VHc3bGJPSk81T2d5WDhNaHdJZ05Mc1Y2eG44V0w2ZXgvSStQdG1ECnBsb2xwZWNXSUt4QTQ3cStaL0djZU9qeDBIcTlnTDZ0N0d4QmhIdFRBclZ5Wlk5M2EzZVI0cHQ1dmpVQ0F3RUEKQVFLQ0FnQk1vQk5aWnd6OUZNa0VNSkJzaXpmRjZLeTNQbjZCSnFOMzFRMldiakcrMUhiRzJpeWVoMXllMUwvUwpudHJZVzV5MW5nd1UyN2xiSnJ4SlJJYnhPRmpteWdXMzJiUjF6T3NtcjltZGVmNVBZU2tRNHNiTUhwajQ0aHh0CnV2ZXpJWllSQWh1YzBrWnhtQUVJR0wrRmM5TzhXWDVCenMxeVoyUi8yYklWbjJ4WmU0SkdsWlRWTTY0a3ZYRC8KTW9ETG5HNVlQc0lpdXlaMy9UalF0OUpibG1qWGJIM3FkQlcrWTg4eTNsV1RsS2pLVVNtZXVvT0EyYkY4ZSsrNQpudlFvMlRzYnlLU29YY0wxRzZTTFBMbzZRMnFnSmRRZVplUjlCUGU5RHpGZXJJbnFlMjRtRUNoVXYrMk9HMUJmCmxnblF6VVExdW9xdUhGNzhaank2VVZkSjhTZDh1ZnZLQzlyejhKWXNJeW5mdzBnUUMzRjgvZW1tMVFTYWJGdlkKdEc0K3gwSzhGZ3JpampFMDhSdnFnSW5keDlmdENOb040dTNsWHhQckpoS3ByMnh1WFNhNFZaYnVtZ043ZnFXeApVQkM4bG1QUWk1VlptajNuSmZqNGRhdG1CVHZzMWRPTFJNZGZkdFRGeitjQWRXTlp4WDNIT0xaVVNxTVZXZ1hZCmtYMHM3SVY5R255VW50Qmt0WCtJRWJXbEF0dHpsZHlxRjltZDRhdmpLWFErWTRQSy9zUjF5V3N1dnRpWmRZVUwKL1FyUUhYMENzVnYxaFJjWDB5ZWtBMGE4cXdhR214RWNuZEVLdjd3RjFpNjI2amMyZkRSNnFJMXlwMjBYbDNTaQprWUJTTmg3VksyMTBYSWhkZFN1VnhXNS9neU5uRkFCRGZwMWJTZFRoNVpKUmZOdnRRUUtDQVFFQTlaaXBueXU4CkpLbEx0V3JoMmlQOXBtRkJhZVVnb0Y2OElWZmQ2SVhkVjduQUhTV3lzaUM0M1NDVW1JNjN4bHVNWUVKRmRBWisKbS9pUmMvbjZWRktFQlc0K1VqazlWVzFFMWlxSGdTQUJnN250RXNCMk1EY1lZMHFLc040Q1lqQzJmTllPOTd6Sgo1b2p1ODRVM1FuOFRXTmtNc3JVVTdjcm0yb0FRZDA4QWl6VkZxTG8xZDhhSXpScSt0bDk1MlMvbGhmWEtjL1A5CmtmaGwrUktqaVlDMnpiV25HaW54YzJOYmY1cFd3bm10U3JjZW5nK1prZ1ZmU0IzSHZTY2txekVOeWU5WWtwVk0KR0UrS2pFZHNzK1FuR1FSV00ySlBseW9ZRG1oVDZycmFzUlQ2VEtzZWN3bzFyUlhCaTRDMWVUWlFTblpmMjRPZwpRdXJTLy9Yekh6Ym5rUUtDQVFFQTR0UVNtYUpQWk5XWXhPSG5saXZ6c2VsZkpZeWc1SnlKVk1RV3cvN0N2VGNWCkdPcG9ENGhDMjVwdUFuaVQxVS9SeWFZYVVGWitJcDJGaEsyUWNlK3Vza05ndHFGTjlwaGgvRGVPMWc4Q1NhSWUKNkVidGc4TjhHTGMwWWhkaUp0ZDJYR3JrdGoyWHRoTUw3T0pQWUlpZGQ0OHRHdVFpemZpam80RmUxUzByU1c1NgpCNFJIVGgvTzZhMHRhTmVGYm5aUUpENTJoYTl3bG5jL1BaU0NVTWI5QzBkMDhkU3hkQlFWK1NWZEdybC9JUmZDCnFISG9DODZHWURjbW52aUQ1Q0ZPeHB4N0FKL2hRQXdQRlFSQ25XR0h3RGpwY29NT3RrdHlvN3BqOU1EdXpCVWIKa3I0cjFlaThmN1BDOWRtU1ltWXpKTVF4TGZ6K1RpMlN5eU9tZE0xQ1pRS0NBUUVBc1ZyNGl6Q0xJcko3TU55cAprdDFRekRrSmd3NXEvRVROZVFxNS9yUEUveGZ0eTE2dzUvL0hZRENwL20xNSt5MmJkdHdFeWQveXlIRzlvRklTClc1aG5MSURMVXBkeFdtS1pSa3ZhSlA1VythaG5zcFg0QTZPVjRnWXZsOEFMV3Bzdy9YK2J1WDNGRTgwcE9nU20KdmtlRVVqSVVBRzNTV2xLZldZVUgzeERYSkxCb3lJc0lGNkh3b3FWQXVmVEN5bnZUTldVbE9ZMG1QYVp6QldaWApZUEhwa1M0d0tTM0c1bndHMUdSQmFSbHpjalJCVVFXVThpVWRCTGcweUwwZXR0MnF4bndvcTFwVFpHNzBiNDhZCnllUGw5Q1AwbUJEVHh5Y256aWU3Q2hTNzN3dDJJYTJsUkpCSDZPR0FMbHpaTUZwdnF3WkcvUC9WMk4wNVdJeGwKY05JMmNRS0NBUUVBb3lzN1ZobFVVNHp6b0cyQlVwMjdhRGdnb2JwUDR5UllCZ29vOWtURmdhZW1IWTVCM1NxQQpMY2toYWRXalFzZHdla1pxbDNBZ3ZIWGtIbFZjbXhsMzZmUmVGZ0pqT3dqVE04UWpsQWluOUtBUzY3UmFGM2NBClJpZEVIMndDeHo0bmZzUEdVdkpydUNaclpiUkd0WUtSQS9pUzBjMWEzQ0FJVnc0eFVkaDBVeGFONGVwZUFPMFEKd3pnNGVqclBXVzd5cDUvblVyT3BvaE9XQW81YVVCRlU1bEE0NTkzQTZXZXBodGhCNlgrVzNBOWprQmlnZkIzTQp2Rm53Qmx0dlJTUlFycjdTSE5qbUNGU2taTkh6dVpMM1BHZTBSeFBQK1lLOHJOcmdIS2pOSHpIdjY5ZXhZT2RTCjhlbzJUUFIrUVJxVG45Y2lLWnJjdFJCRGtLM01pQ2svb1FLQ0FRQVpJWmRrT0NsVVBIZlNrNHg1bkJYYXNoS1kKZ0R6ZXlZSFlMd05hQmRFS2dITnVmNmpDbHRLV29EelpzcXJ2MU55YS8xNDhzVGdTVGc5MzFiYmNoK2xuSEtKZApjWHJDUVpXQm51MlVxdWlzRk1lTk92cHAwY1B0NHRJWURaVkNSTVJyd0lsWnFJSnhiMm5Bd0Z2YjBmRWZMays0CmdtdSszY0NhTi92UzNvSkE5RUZremp4RzBYaUxPeW55QVpiNWZZMDRObUZPSXNxM3JnVDREZUN1ckhUS3RPSjIKdDE0b1ROcTA2TEQ1NjZPblQ2cGxMN3ZhTHRUUi85L3FKYzAwN1dqdzhRZGJUdVFBTHFDaldXZzJiN0JWa095UgpvOUdyaFB6U2VUNm5CSEk4RW9KdjBueGVRV05EWDlwWmlXLzFuc3l1QUFGSjlJU2JEV2p6L1R3QjE3VUwKLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K +``` + +### Cert Base64 + +``` +LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZOekNDQXg4Q0NRQzY4N1hGeHREUlNqQU5CZ2txaGtpRzl3MEJBUXNGQURCL01Rc3dDUVlEVlFRR0V3SlYKVXpFTE1Ba0dBMVVFQ0F3Q1Rsa3hEekFOQmdOVkJBY01Ca2wwYUdGallURVFNQTRHQTFVRUNnd0hRWFpoYkdGaQpjekVPTUF3R0ExVUVDd3dGUjJWamEyOHhEREFLQmdOVkJBTU1BMkYyWVRFaU1DQUdDU3FHU0liM0RRRUpBUllUCmMzUmxjR2hsYmtCaGRtRnNZV0p6TG05eVp6QWdGdzB4T1RBM01ESXhOakV5TWpWYUdBOHpNREU1TURjeE1ERTIKTVRJeU5Wb3dPakVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrNVpNUkF3RGdZRFZRUUtEQWRCZG1GcwpZV0p6TVF3d0NnWURWUVFEREFOaGRtRXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDRHdBd2dnSUtBb0lDCkFRRFpuRG9ESEUybmo4MnhEakgwVGI3T1hNcVFESHoremJMaWR0Nk1TSTFYQjN2T0FJRWlQcXJ0ZW5HbnFSYlYKRmNtNUdaeHZ4aDRZUUQ4Q2pLU2sxcWdaSmN6czBEUFNpR1E4RWZsNFBHTzR4bkVibGxnTDNQVVJQV3A3bUVWMwpvaDZmeElDZ1FLVEJsVDY3MUVuRnpCNWx5SldwdW1SenZBMXZ5aEJNc1k4YU8reGRxNUxVRmx0WXpCZHZwZ0xYClZhRHdIWlEyUFFFV3RGMGQwSk8yTjBXRkZER05teDZuOHBLU2VJQVZEc1R3WkNaSytGQ2VlRXlvR2ZYc0lOc2MKMHlDTVFzbGF3a2ZPTXFBOXlCVjNKaTZRbUZZS3lHWXQ2NU1XR05xUEE0WHJJeWxpS3dDQ1h3ejltamFXeU43cgpBeXc5Y1dsTE1PRE5tRE9SV3pHUlo1MjkwTUVBRUlac3FqWUhWaXRSVE0vUm5OSWFkVG9aR08weTV1QWtNMTRjCm1Udm5zSzFDUDkycXRmU2lzcTc1Vy9JOTFkclRob0V0VEs3OFVHT2wvNVExWUJSMDhGK3RTVVdaV3lIZUk2VU8KQlVDR0MyYkN0bXpLTWw3dlUyNWxHNm1iQ1IxSnVRaTZSWXBuZk1qWEgzNmxWNFM3ZlR2U3d3dVIwM2gyRjNIMQplRmtXTkcybGJGclcwZHpEQ1BnM2xYd21GUTY1aFVjUWhjdHpub0J6NUMxbEYyZVcwM3d1Vmd4aW5udVZsSkhqCnkvR3JxbVdzQVNuMVBEdVZzNGs3azZESmZ3eUhBaUEwdXhYckdmeFl2cDdIOGo0KzJZT21XaVdsNXhZZ3JFRGoKdXI1bjhaeDQ2UEhRZXIyQXZxM3NiRUdFZTFNQ3RYSmxqM2RyZDVIaW0zbStOUUlEQVFBQk1BMEdDU3FHU0liMwpEUUVCQ3dVQUE0SUNBUUE0MGF4MGRBTXJiV2lrYUo1czZramFHa1BrWXV4SE5KYncwNDdEbzBoancrbmNYc3hjClFESG1XY29ISHBnTVFDeDArdnA4eStvS1o0cG5xTmZHU3VPVG83L2wwNW9RVy9OYld3OW1Id1RpTE1lSTE4L3gKQXkrNUxwT2FzdytvbXFXTGJkYmJXcUwwby9SdnRCZEsycmtjSHpUVnpFQ2dHU294VUZmWkQrY2syb2RwSCthUgpzUVZ1ODZBWlZmY2xOMm1qTXlGU3FNSXRxUmNWdzdycXIzWHk2RmNnUlFQeWtVbnBndUNFZ2NjOWM1NGMxbFE5ClpwZGR0NGV6WTdjVGRrODZvaDd5QThRRmNodnRFOVpiNWRKNVZ1OWJkeTlpZzFreXNjUFRtK1NleWhYUmNoVW8KcWw0SC9jekdCVk1IVVk0MXdZMlZGejdIaXRFQ2NUQUlwUzZRdmN4eGdZZXZHTmpaWnh5WnZFQThTWXBMTVp5YgpvbWs0ZW5EVExkL3hLMXlGN1ZGb2RUREV5cTYzSUFtME5UUVpVVnZJRGZKZXV6dU56NTV1eGdkVXEyUkxwYUplCjBidnJ0OU9ieitmNWoyam9uYjJlMEJ1dWN3U2RUeUZYa1VDeE1XK3BpSVVHa3lyZ3VBaGxjSG9oRExFbzJ1Qi8KaVE0Zm9zR3Fxc2w0N2IrVGV6VDVwU1NibGtnVWppd3o2ZURwTTRsUXB4MjJNeHNIVmx4RkhyY0JObTBUZDkydgpGaXhybWxsYW1BWmJFejF0Qi8vMGJpcEthT09adWhBTkpmcmdOOEJDNnYyYWhsNC9TQnV1dDA5YTBBenl4cXBwCnVDc3lUbmZORWQxVzZjNm5vYXEyNHMrN1c3S0tMSWVrdU5uMU51bm5IcUtxcmlFdUgxeGx4eFBqWUE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== +``` + +## Signer4 + +### Public Key + +``` +0xa926858ebfba247e6d759e40bc82f92c20f359769e3538d58562bf30077f5e4c62b3b149f11b8e68a2f5599acd782de2 +``` + +### Proof of Possession + +``` +0xb48df13f6e0167e2f40cc7c02cf81bf4a2c0b1b686395a56690deb6b5876c0a654ac41253455f41a6c65ac2fbf712d980d9012375edf73efb341d0961ac09197bf5f807192b6cd9f44c9e9032b0e035e4d07ba7eebccf434688610a2f51bc492 +``` + +### Key Base64 + +``` +TG9jYWxGbGFyZU5ldHdvcmtUZXN0VmFsaWRhdG9yMDQ= +``` + +## Staker5 + +### NodeID + +``` +NodeID-P7oB2McjBGgW2NXXWVYjV8JEDFoW9xDE5 +``` + +### Key Base64 + +``` +LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS1FJQkFBS0NBZ0VBNEN1YStiM1I3U1JSSU1PNFJoUDVjeW1oM0w4TTY5OW1xNXF0SlBiV0hJU1YzMDk1Ck5ZOVpWN1FRSFhGRGQ0RWtTbzRNeC83NDI1OUhtdHdJZ3dsSGRqSEpQNTlDbkNXZU4wdVNPMll6KzYyMnI5dmUKbXlnbWRSNis2K1lGMkJZWlB0MmxMRlh3MUtnRHdjTWovQ25sZDlRRVZRUDdQbGozbHFyT3ZVRjAzY2liZ1dCTwphYStOTDlVbi9ubTFDSjJkKzdobU41SWp0Qk1meVlIS0VMQmljNjFnLzg3VzBRaFNMNjNUU1dYZEtScnpKWmt5CnFlMTdERWQzS2I2cE8rOVAvekN3bmNobHY5U0FLMDByZzliM2U1Z1Y5U2NsVnlzbkdEZEx5RHJXNE1UemhiclYKTXFnem5qKzZPZGF1SWowdDNRa1lINXAvNFZpOE12Z3JTWHRBUmlyRytMWk5MU3NhTkxIdkwvU1FQSmxMWjVzNQpqTGVHUFk0b3pxd2pnZTE4RmlwaVdCSnZEZVZHM011bktkcUMvTDBGYmM4OXoxNjVCYmZ0M0JiZFk2NDNhVUdJCkIrSThwSkZnM2pveEg4dFJRc003RFViblMwN3k0WHk1L092MlUrSEMyZlNTTy8xbHRDRHd2QlhPNVljNXlmbXkKRUZIaEozV2huWUZaTWJoSU5ETVhheks5STFOQ0JhRjhra1Z6VWMzQTFtVW9SSXdVNys3WnhjUzVxRFZQcmlXLwp5cVRCK3JFV0l0ODdQT1huR2pDamNodml3NWdXb1ZKUjhlWVNnQitQVHZQT2VmZ0RJYklQZFRqV1NaWFkwRjlLCkdheUNzeXVRYWJLbms1ZUhObUxmM3dDNG1XbmhaTmFRUkxMMVhqRGdRaUxsaFBtK0ZDQkdpd0RvY1g4Q0F3RUEKQVFLQ0FnRUFwdU1QcnhtSDdYbjZBK0J4a1lwUlRWRVROWm50N3JRVVpYRHpzZThwbTNXQmRneGVlbWRMNWlVaApVaW4rUmp1WVh3Qzl0eTYwNmh2OFhPZXVWbzlUNmtSS1JOazE1N1dCd2p5Nmt3b1ZiU3I0TkpnRmM1RkNnREx4CmhBRnRIRi9uVDR3RzZhalpjQmZkSkNVNDV3UHgxM0c1LytqRTVMZXJLem5pUzdjdFgrZDNEYXc2OUNkRGZ2YTcKblpIU0dxWHM5WGRrY2I2VVlmMVN6dHVYS1RHSE9nTTdrWFhWS3kxOHNnNUFuQVgvemhoSUtCZVRSanFNUHFuOQpwdEJRZ1ZRNlJBdGxrVEdkdm1CZlF0MWlwZllsckplZTBUSGhkTEdsbXp1ZmFXT1VrU1ZPL3FJSEVuMXlZRCtsClRtWHFvWWJXWEJYbkpiQUp3Q1FsaC9TRmxXRHlpV1dPeHN6eGR3d1QyeWJ3N09SM2EwREVWME1iS0prVWV4eUYKOTJMcjNxb0JTWlJGUW5YVnZCZ2pRT3duekVGcGgxQU51R1kzb2RMOEpTTTF0SG5pSXNDczRXaERQT3NiQWoraAprd1M1MWNvbE1rM2JOQ1ozeGVBcmpNTEJWTGdUN3hMWC83WlljNy9vVEVGV2lrKzIwVHZTRVd6ZEUxTi80Z2ZKCmpFVS9WcXJuTmp5ZXYydzlBazZiRWt3WkZMUzZWWjlyVFdURjlqazhDMWFYai9SaGZhYUMzM3hYQmJobjlIdVgKbFR1L0phTE1wMFFjNGFDbHFVWU02TGx4SWVqSDViOGZJeENOSEppc2xYSkRhNmE2YVFsODVCaVFPRFBGeFZUNQpXQ3BRRDQ4NThFdUxkWDRCUlcyZklHUlk2RGl2UjZ1SlJBbXhMZitFd0FnL3JnVHpVc0VDZ2dFQkFQU2tIWDVGCkJoUmd1ZEYwTW53Titlbmo0U29YSGhSRytEVG9yeE8xWmgycU45bG5YTzluTUtNQ1hWSkxJVnZHRnVpTVJTSjAKVktmMXUwVXFhQkYwMk1iSXZiZWk3bXpra1cwLzc0bTA0WDM3aXlNbXRubW9vUTBHRVY4NG9PTndBdDNEZWVUZwp2SXBPdHE5VjI2WEhHYVFEeGNSRk1GQnVEMDJhMnlmM0pZa1hqNzRpMnNjTVA0eHhNSE1rSnhHSzlGU0JPaG5wCmsvcDBoTWwzRlZHZm81TnM1VDFSbDNwTXVlRUYzQjUrQnZyVjF6MTRJTi8wbHd1aHVqclVVWVM0RXcrUGs1ekMKRlN1YmZJUU1xU1QxanZYWFRhR2dYMEdQZmZhNGx4Z2FERUFUTGV3dkwzRmp5MjdYemw1N2k5WnZUTkM0eUZhZAo0b2tqci9lSXRIdEtWSEVDZ2dFQkFPcVVLd3cvNnVpSk1OdmMyTnhMVU14dXpCMDdycU9aS1QyVk1Ca0c1R3prCnY4MWZEdGxuZEQ4Y3dIU3FPTEtzY0gvUUtYRDdXSzNGQ3V2WlN2TXdDakVCNFBwMXpnd0pvQmV4dVh2RkREYnMKMFQ3N1Fpd2UrMldtUklpWWV2NWFSRzNsbkJNTThSRFMvUVB6RWRveEhkenJGVVJZVmwwcnY1bC83cndCMlpkNgp4QVlIY1VwWmM0WmF5c0VncVFDdVpRcUM3TXJxN3FmQnlVdGhIMjhZaWN6MTk3OGZwRTNkeDE1Y2VxalU5akJRCnhVVXdiZUtUL1VrUVF2bVlIZHRnd0VqaHpWUUwxT0FBV2tUNlJzc01xeDJSQWRpMFNxV1BGRWh4TlBIQnBHOUIKbEtVREJCSU02ZHU5MTZPbjBCamdoaDNXaHhRS3BUSXp2ZU5BaWV4YlhPOENnZ0VCQU52Sm9oR3ljMzdWVTd3ZwoxOFpxVEEvY3dvc3REOElKN0s2a0tiN2NKeTBabzJsM21xQWZKaXdkVUxoQmRXdmRNUEdtSytxRGR4Y2JCeTloCnBQT2g5YXZKNStCV3lqd2NzYWJrWFJGcjUzWm5DcDcvQmN1Uk8zZlc3cjZNd3NieStEQkNrWDJXaHV6L1FOT1AKb0hGMHljMTM4aktlTW9UZ0RIR2RZYTJyTmhiUGl6MjRWTE9saG1abnZxNkRXWEpDVTdha0R3Mytzd3E5cWhyUwpHTjRuUFMrVEV2VWZHNmN0ellXajNSbXNBaHRUQ1RoWmQ3ZWRLQ0swSHZzQmkyZGdkUWR5NTV4YkplZnlubENJCmkySUFGM3M0L3E3cHhRckNudG1OQjNvSTFONndISDduK1lpMnJxc2J5WFZMSzl2d1RLUHNqMWg2S204cEY4dWQKRHdFQlM1RUNnZ0VBTW5xMkZNbkFiRS94Z3E2d3dCODVBUFVxMlhPWmJqMHNZY016K1g3Qk15bTZtS0JIR3NPbgpnVmxYbFFONGRnS2pwdTJOclhGNU1OUEJPT1dtdWxSeExRQ2hnR1JQZGNtd2VNalhDR3ByNlhubXdXM2lYSXBDClFTcVpmdWVKT0NrR3BydU5iWkFRWkRWekd5RjRpd0tjMFlpSktBNzJidEJXUjlyKzdkaGNFYnZxYVAyN0JHdmgKYjEwa1dwRURyVkRhRDN3REp0dU5oZTR1dWhqcFljZmZCNHM2eUJjd0RVMlhkSmZrRVdiYW42VVIvb1NnY095MQp5YjVGRzE3L3RkREpNQ1hmUUtIWEtta0pBK1R6elFncDNvL3czTWhYYys4cFJ6bU5VaVVBbEt5QkowMVIxK3lOCmVxc010M3dLVFFBci9FbkpBYWdVeW92VjVneGlZY2w3WXdLQ0FRQWRPWWNaeC9sLy9PMEZ0bTZ3cFhHUmpha04KSUhGY1AyaTdtVVc3NkV3bmoxaUZhOVl2N3BnYmJCRDlTMVNNdWV0ZklXY3FTakRpVWF5bW5EZEExOE5WWVVZdgpsaGxVSjZrd2RWdXNlanFmY24rNzVKZjg3QnZXZElWR3JOeFBkQjdaL2xtYld4RnF5WmkwMFI5MFVHQm50YU11CnpnL2lickxnYXR6QTlTS2dvV1htMmJMdDZiYlhlZm1PZ25aWHl3OFFrbzcwWHh0eDVlQlIxQkRBUWpEaXM4MW4KTGc5NnNKM0xPbjdTWEhmeEozQnRYc2hUSkFvQkZ4NkVwbXVsZ05vUFdJa0p0ZDdYV1lQNll5MjJEK2tLN09oSApScTNDaVlNdERtWm91Yi9rVkJMME1WZFNtN2huMVRTVlRIakZvVzZjd1EzN2lLSGprWlZSd1gxS3p0MEIKLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K +``` + +### Cert Base64 + +``` +LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZOekNDQXg4Q0NRQzY4N1hGeHREUlNqQU5CZ2txaGtpRzl3MEJBUXNGQURCL01Rc3dDUVlEVlFRR0V3SlYKVXpFTE1Ba0dBMVVFQ0F3Q1Rsa3hEekFOQmdOVkJBY01Ca2wwYUdGallURVFNQTRHQTFVRUNnd0hRWFpoYkdGaQpjekVPTUF3R0ExVUVDd3dGUjJWamEyOHhEREFLQmdOVkJBTU1BMkYyWVRFaU1DQUdDU3FHU0liM0RRRUpBUllUCmMzUmxjR2hsYmtCaGRtRnNZV0p6TG05eVp6QWdGdzB4T1RBM01ESXhOakV5TWpsYUdBOHpNREU1TURjeE1ERTIKTVRJeU9Wb3dPakVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrNVpNUkF3RGdZRFZRUUtEQWRCZG1GcwpZV0p6TVF3d0NnWURWUVFEREFOaGRtRXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDRHdBd2dnSUtBb0lDCkFRRGdLNXI1dmRIdEpGRWd3N2hHRS9sekthSGN2d3pyMzJhcm1xMGs5dFljaEpYZlQzazFqMWxYdEJBZGNVTjMKZ1NSS2pnekgvdmpibjBlYTNBaURDVWQyTWNrL24wS2NKWjQzUzVJN1pqUDdyYmF2Mjk2YktDWjFIcjdyNWdYWQpGaGsrM2FVc1ZmRFVxQVBCd3lQOEtlVjMxQVJWQS9zK1dQZVdxczY5UVhUZHlKdUJZRTVwcjQwdjFTZitlYlVJCm5aMzd1R1kza2lPMEV4L0pnY29Rc0dKenJXRC96dGJSQ0ZJdnJkTkpaZDBwR3ZNbG1US3A3WHNNUjNjcHZxazcKNzAvL01MQ2R5R1cvMUlBclRTdUQxdmQ3bUJYMUp5VlhLeWNZTjB2SU90Ymd4UE9GdXRVeXFET2VQN281MXE0aQpQUzNkQ1JnZm1uL2hXTHd5K0N0SmUwQkdLc2I0dGswdEt4bzBzZTh2OUpBOG1VdG5tem1NdDRZOWppak9yQ09CCjdYd1dLbUpZRW04TjVVYmN5NmNwMm9MOHZRVnR6ejNQWHJrRnQrM2NGdDFqcmpkcFFZZ0g0anlra1dEZU9qRWYKeTFGQ3d6c05SdWRMVHZMaGZMbjg2L1pUNGNMWjlKSTcvV1cwSVBDOEZjN2xoem5KK2JJUVVlRW5kYUdkZ1ZreAp1RWcwTXhkck1yMGpVMElGb1h5U1JYTlJ6Y0RXWlNoRWpCVHY3dG5GeExtb05VK3VKYi9LcE1INnNSWWkzenM4CjVlY2FNS055RytMRG1CYWhVbEh4NWhLQUg0OU84ODU1K0FNaHNnOTFPTlpKbGRqUVgwb1pySUt6SzVCcHNxZVQKbDRjMll0L2ZBTGlaYWVGazFwQkVzdlZlTU9CQ0l1V0UrYjRVSUVhTEFPaHhmd0lEQVFBQk1BMEdDU3FHU0liMwpEUUVCQ3dVQUE0SUNBUUIrMlZYbnFScWZHN0gyL0swbGd6eFQrWDlyMXUrWURuMEVhVUdBRzcxczcwUW5xYnBuClg3dEJtQ0tMTjZYZ1BMMEhyTjkzM253aVlybWZiOFMzM3paN2t3OEdKRHZhVGFtTE55ZW00LzhxVEJRbW5Sd2UKNnJRN1NZMmw3M0lnODdtUjBXVGkrclRuVFR0YzY2Ky9qTHRGZWFqMFljbDloQlpYSEtpVUxTR2hzYlVid3RregppdU5sQU5ob05LWE5JQUJSSW1VcTZPd1loRVFOMER3SFhqNzl3a3B5RFlqS1p3SHVFWlVrbmM4UGwyb1FQQmtlCm1pbDN0c3J2R1Jrd2hpc25YWDd0cWg2cldLVlpOSmtPNjhoeTdYTzlhVFhqYmNCLzdZMUs4M0lTTkV5R1BzSC8KcHdGeWQvajhPNG1vZHdoN1Vsd3cxL2h3Y3FucWlFRkUzS3p4WDJwTWg3VnhlQW1YMnQ1ZVhGWk9sUngxbGVjTQpYUmtWdTE5bFlES1FIR1NyR3huZytCRmxTT0I5NmU1a1hJYnVJWEtwUEFBQ29CUS9KWllidEhrczlIOE90TllPClAyam9xbW5ROXdHa0U1Y28xSWkvL2oydHVvQ1JDcEs4Nm1tYlRseU5ZdksrMS9ra0tjc2FpaVdYTnJRc3JJRFoKQkZzMEZ3WDVnMjRPUDUrYnJ4VGxSWkUwMVI2U3Q4bFFqNElVd0FjSXpHOGZGbU1DV2FZYXZyQ1pUZVlhRWl5RgpBMFgyVkEvdlo3eDlENVA5WjVPYWtNaHJNVytoSlRZcnBIMXJtNktSN0IyNmlVMmtKUnhUWDd4UTlscmtzcWZCCjdsWCtxMGloZWVZQTRjSGJHSk5Xd1dnZCtGUXNLL1BUZWl5cjRyZnF1dHV0ZFdBMEl4b0xSYzNYRnc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== +``` + +## Signer5 + +### Public Key + +``` +0xa09f0233b2682fc9ae699aae49c9e95b86a0b39b6b0617987691a64aeca5a376f71dbdf7bb3d51c684cfe0971b882adc +``` + +### Proof of Possession + +``` +0xb9a3b3cf09f987cd314551bde2c7a1182210d91af36689e5ac31a213e5c5e9c66dc2abd83959b8204264919e2456652f17fc3eed88469a06212787f12ea400977c6319ce2157b5d816000fd19c64dfd55a94e64b49f993664e69f23cf1acf51d +``` + +### Key Base64 + +``` +TG9jYWxGbGFyZU5ldHdvcmtUZXN0VmFsaWRhdG9yMDU= +``` diff --git a/server/docker/localflare/signer1.key b/server/docker/localflare/signer1.key new file mode 100644 index 0000000..09a795a --- /dev/null +++ b/server/docker/localflare/signer1.key @@ -0,0 +1 @@ +LocalFlareNetworkTestValidator01 \ No newline at end of file diff --git a/server/docker/localflare/signer2.key b/server/docker/localflare/signer2.key new file mode 100644 index 0000000..6986189 --- /dev/null +++ b/server/docker/localflare/signer2.key @@ -0,0 +1 @@ +LocalFlareNetworkTestValidator02 \ No newline at end of file diff --git a/server/docker/localflare/signer3.key b/server/docker/localflare/signer3.key new file mode 100644 index 0000000..a8d6879 --- /dev/null +++ b/server/docker/localflare/signer3.key @@ -0,0 +1 @@ +LocalFlareNetworkTestValidator03 \ No newline at end of file diff --git a/server/docker/localflare/signer4.key b/server/docker/localflare/signer4.key new file mode 100644 index 0000000..20d5335 --- /dev/null +++ b/server/docker/localflare/signer4.key @@ -0,0 +1 @@ +LocalFlareNetworkTestValidator04 \ No newline at end of file diff --git a/server/docker/localflare/signer5.key b/server/docker/localflare/signer5.key new file mode 100644 index 0000000..ac58001 --- /dev/null +++ b/server/docker/localflare/signer5.key @@ -0,0 +1 @@ +LocalFlareNetworkTestValidator05 \ No newline at end of file diff --git a/server/docker/localflare/staker1.crt b/server/docker/localflare/staker1.crt new file mode 100644 index 0000000..b97df69 --- /dev/null +++ b/server/docker/localflare/staker1.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFNzCCAx8CCQC687XFxtDRSjANBgkqhkiG9w0BAQsFADB/MQswCQYDVQQGEwJV +UzELMAkGA1UECAwCTlkxDzANBgNVBAcMBkl0aGFjYTEQMA4GA1UECgwHQXZhbGFi +czEOMAwGA1UECwwFR2Vja28xDDAKBgNVBAMMA2F2YTEiMCAGCSqGSIb3DQEJARYT +c3RlcGhlbkBhdmFsYWJzLm9yZzAgFw0xOTA3MDIxNjEyMTVaGA8zMDE5MDcxMDE2 +MTIxNVowOjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMRAwDgYDVQQKDAdBdmFs +YWJzMQwwCgYDVQQDDANhdmEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDKYSRw/W0YpYH/MTQhiFrR0m89l6yTuzLpDtjudr/5RnhIPvtqk7YIGm/m9l29 +xwR4J5r7SZGs+70yBetkbS+h7PwJ2rmWDwbrdyJKvVBhqf8kSn+VU2LePSIcJj19 +3LDyWhV1H4lqNkUkcAR76Fh9qjMvA2p0vJ66+eDLXlph/RYapQx9HgOj/0BmAKMr +YCyo5BhRih+Ougg8aK4G9PQTIA5G2wTWW2QkHxM/QppFjZd/XwQeJ2H6ubWMFc5f +ttf6AzpJvFIDBu/JDCKWiCu5m8t4GL8w2OrIx8Js19lF4YYE2eojCreqgPi64S3o +cqwKsDoySTw6/5iKQ5BUYwUXX3z7EXOqD8SMHefUKeczj4WvAaZLzR27qXm55EgR +YQAIX4fhmY7NfSop3Wh0Eo62+JHoM/1g+UgOXlbnWpY95Mgd7/fwDSWLu4IxE0/u +q8VufIbfC4yrY8qlTVfAffI1ldRdvJjPJBPiQ0CNrOl60LVptpkGc9shH7wZ2bP0 +bEnYKTgLAfOzD8Ut71O2AOIa80A1GNFl4Yle/MSNJOcQOSpgtWdREzIUoenAjfuz +M4OeTr4cRg4+VYTAo9KHKriN1DuewNzGd8WjKAVHmcIMjqISLTlzMhdsdm+OmfQ6 +OvyX7v0GTOBbhP09NGcww5A0gCzXN18FS5oxnxe6OG9D0wIDAQABMA0GCSqGSIb3 +DQEBCwUAA4ICAQAqL1TWI1PTMm3JaXkhdTBe8tsk7+FsHAFzTcBVBsB8dkJNGhxb +dlu7XIm+AyGUn0j8siz8qojKbO+rEPV/ImTH5W7Q36rXSdgvNUWpKrKIC5S8PUF5 +T4pH+lpYIlQHnTaKMuqH3nO3I40IhEhPaa2wAwy2kDlz46fJcr6aMzj6Zg43J5UK +Zid+BQsiWAUau5V7CpC7GMCx4YdOZWWsT3dAsug9hvwTe81kK1JoTH0juwPTBH0t +xUgUVIWyuweM1UwYF3n8Hmwq6B46YmujhMDKT+3lgqZt7eZ1XvieLdBRlVQWzOa/ +6QYTkrqwPZioKIStrxVGYjk40qECNodCSCIwRDgbnQubRWrdslxiIyc5blJNuOV+ +jgv5d2EeUpwUjvpZuEV7FqPKGRgiG0jfl6Psms9gYUXd+y3ytG9HeoDNmLTSTBE4 +nCQXX935P2/xOuok6CpiGpP89DX7t8yiwk8LFNnY3rvv50nVy8kerVdnfHTmoMZ9 +/IBgojSIKov4lmPKdgzFfimzhbssVCa4DO/LIhTF7bQbH1ut/Oq7npdOpMjLYIBE +9lagvRVTVFwT/uwrCcXHCb21b/puwV94SNXVwt7BheFTFBdtxJrR4jjr2T5odLkX +6nQcY8V2OT7KOxn0KVc6pl3saJTLmL+H/3CtAao9NtmuUDapKINRSVNyvg== +-----END CERTIFICATE----- diff --git a/server/docker/localflare/staker1.key b/server/docker/localflare/staker1.key new file mode 100644 index 0000000..f4747a5 --- /dev/null +++ b/server/docker/localflare/staker1.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKAIBAAKCAgEAymEkcP1tGKWB/zE0IYha0dJvPZesk7sy6Q7Y7na/+UZ4SD77 +apO2CBpv5vZdvccEeCea+0mRrPu9MgXrZG0voez8Cdq5lg8G63ciSr1QYan/JEp/ +lVNi3j0iHCY9fdyw8loVdR+JajZFJHAEe+hYfaozLwNqdLyeuvngy15aYf0WGqUM +fR4Do/9AZgCjK2AsqOQYUYofjroIPGiuBvT0EyAORtsE1ltkJB8TP0KaRY2Xf18E +Hidh+rm1jBXOX7bX+gM6SbxSAwbvyQwilogruZvLeBi/MNjqyMfCbNfZReGGBNnq +Iwq3qoD4uuEt6HKsCrA6Mkk8Ov+YikOQVGMFF198+xFzqg/EjB3n1CnnM4+FrwGm +S80du6l5ueRIEWEACF+H4ZmOzX0qKd1odBKOtviR6DP9YPlIDl5W51qWPeTIHe/3 +8A0li7uCMRNP7qvFbnyG3wuMq2PKpU1XwH3yNZXUXbyYzyQT4kNAjazpetC1abaZ +BnPbIR+8Gdmz9GxJ2Ck4CwHzsw/FLe9TtgDiGvNANRjRZeGJXvzEjSTnEDkqYLVn +URMyFKHpwI37szODnk6+HEYOPlWEwKPShyq4jdQ7nsDcxnfFoygFR5nCDI6iEi05 +czIXbHZvjpn0Ojr8l+79BkzgW4T9PTRnMMOQNIAs1zdfBUuaMZ8XujhvQ9MCAwEA +AQKCAgEAuUM4Mt8r8bYBTPVj/ZZvXUjAYKfqacqijkrzN0kp8C4cijZtvWC+8KgS +7GF36vS3GK9Y5tSwMKS6y4IzvFlfk2H4T6UU41OaSA9lKvonDWCrmjNAnBgbl8pq +4U34WLGgohrpLbDTAJHxtat9z1ghOdiGxnDgEUFiJVP9/u2+25jtlTKmPhstxgEy +mK3YsSp3d5xmzq4cuXF/fJ1vQhsXHDLqHt78jKZZA+AWpIB57VXy67y1bk0rGnTK +xxRnOaOODubJgxqMEQ1WkLs1Jow9Sspd9vDghPzt4SNMzorB8YDESMib17xF6iXq +jFj6x6HB8H7mp4X3RyMYJuo2w6lpzBsEncUYpKhqMabF0I/giI5VdpSDvkCCOFen +nWZLV9Ai/x7tTq/0F+cVM69Mgfe8iYymqlfd6WRZITKfViNHALlG/Pq9yHJsz7Ng +S8BKODt/sj4Q0xLtFDT/DmpP50iq7SiS14obcKcQr8FAjM/sOY/Ulg4M8MA7EugS +pDJwLl6XDoIMMCNwZ1HGsDstzmx5Mf50bS4tbK4iZzcpPX5RBTlVdo9MTSgnFizp +Ii1NjHLuVVCSLb1OjoTgu0cQFiWEBCkC1XuoR8RCY6iWVrUH4Gezni7ckt2mJaNA +pd6/87dFKE3jh5T6jZeJMJg5skTZHSozJDuaj9pMK/JONSD06sECggEBAPq2lEmd +g1hpMIqa7ey1uoLd1zFFzlWrxTJLlu38N69mYDOHrV/zqRGOpZB+1nH7tQJIT/L1 +xLN33mFVqCrN8yUmZ+iUWioaI5JZ1jzCgemVGeBgodwP9MOZfxxrDp17oTdabaEq +7ZaBYnY8xK/4bCxu/B4mFiF3Za8ZTd/+2yev7JM+E3MorWc7rrKm1ApflfxytdhO +JLBiqOcqobI3dgHyzesVb8cT4XCpoRhdrFwort0JI7ryfddd49vMJ3ElRbnN/h4F +f24cWY/sQPq/nfDmec28Z7nVza1D4rszNylYDvzdjF0Q1mL5dFVntWbZA1CNurVw +nTfwuyQ8RF9YnYMCggEBAM6lpNeqaiG9ixKSr65pYOKtByUI3/eTT4vBnrDtYF+8 +ohiKgIymG/vJsSdrynKfwJObEy2dBYhCGF3h9z2nc9KJQD/su7wxCsdmBs7YoDiM +uzNPlRAmI0QAFILPCk48z/lUQk3r/Mzu0YzRv7fI4WSpIGAefVPDqy1uXsATDoDJ +arcEkND5Lib89Lx7r02EevJJTdhTJM8mBdRl6wpNV3xBdwis67uSyunFZYpSiMw7 +WWjIRhzhLIvpgD78UvNvuJi0UGVEjTqnxvuW3Y6sLfIk80KSR24USinT27t//x7z +yzNko75avF2hm1f8Y/EpcHHAax8NAQF5uuV9xBNvv3ECggEAdS/sRjCK2UNpvg/G +0FLtWAgrcsuHM4IzjVvJs3ml6aV3p/5uKqBw0VUUzGKNCAA4TlXQkOcRxzVrS6HH +FiLn2OCHxy24q19Gazz0p7ffE3hu/PMOFRecN+VChd0AmtnTtFTfU2sGXMgjZtLm +uL3siiRiUhFJXOE7NUolnWK5u2Y+tWBZpQVJcCx0busNx7+AEtznZLC583xaKJtD +s1K7JRQB7jU55xrC0G9pbkMysm0NtyFzgwmfipBHVlCpyvg6DCxd8FhvhN9Zea1b +fhkc0SJZorHC5hkqpydJDmlVCk0vzEAeQM4C94ZUOytbnjQnmXp14CNASYqLXteQ +ueRo0wKCAQAG0F10IxFm1WotjZqvZJgmQVBX/0frUPcxg4vpB5rC7WRm7MI6YQvR +LKBjzWEakHv4Igfq3B+fk5ZcGiRd6xSdn5r3wKWcGf3h/1JAJdJ6quFNWtVud+N3 +zYzfl1YeqFCvRwD8ssheNY3BV/U7aStNd2oy4S5+wZf2YopLSRWUV4/mQwdHbMAB +1xt2z5lDNBgdvx8LAArZrcZJb6blaxF0bnAvYAxR3hBEzxZ/DiOmoFpdYyU0tJQU +dPmemhFeJ5PtrRxtimohwgCEsT/TAYhuUJuY2VvznEWpxWucbicKbT2JD0t67mEB +sV9+8jqVbCliBtdBadtbohjwkkoR3gBxAoIBAG3cZuNkIWpELEbeICKouSOKN06r +Fs/UXU8roNThPR7vPtjeD1NDMmUHJr1FG4SJrSigdD8qNBg8w/G3nI0Iw7eFskk5 +8mNm21CpDzON36ZO7IDMj5uyBlj2t+Ixl/uJYhYSpuNXyUTMm+rkFJ0vdSV4fjLd +J2m30juYnMiBBJf7dz5M95+T0xicGWyV24zVYYBbSo0NHEGxqeRhikNqZNPkod6f +kfOJZGalh2KaK5RMpZpFFhZ/kW9xRWNJZyCWgkIoYkdilMuISBu3lCrk8rdMpAL0 +wHEcq8xwcgYCS2qk8HwjtmVd3gpB1y9UshMr3qnuH1wMpU5C+nM2oy3vSko= +-----END RSA PRIVATE KEY----- diff --git a/server/docker/localflare/staker2.crt b/server/docker/localflare/staker2.crt new file mode 100644 index 0000000..a572af1 --- /dev/null +++ b/server/docker/localflare/staker2.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFNzCCAx8CCQC687XFxtDRSjANBgkqhkiG9w0BAQsFADB/MQswCQYDVQQGEwJV +UzELMAkGA1UECAwCTlkxDzANBgNVBAcMBkl0aGFjYTEQMA4GA1UECgwHQXZhbGFi +czEOMAwGA1UECwwFR2Vja28xDDAKBgNVBAMMA2F2YTEiMCAGCSqGSIb3DQEJARYT +c3RlcGhlbkBhdmFsYWJzLm9yZzAgFw0xOTA3MDIxNjEyMTlaGA8zMDE5MDcxMDE2 +MTIxOVowOjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMRAwDgYDVQQKDAdBdmFs +YWJzMQwwCgYDVQQDDANhdmEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDdToR60na6NuR9iSAUMyzPXJNMWVQbLyT5/iZCiJ3BB4YWMBhfxpJWJiWXcM+z +nDgpJuyCEeh5Dp6ZY3Fe7k6Hht6FmFpDjwnjpQmdkEKUg00G+ElPTp/UsmsPL+JA +swPqBZWpMBS3dsXQNunMMtMGlrf5S0l6XX4y7kc/GTxYgveWZ9JtR/m2KNer+wjg +BHqJ4rPqnHB30sDYPZg91Cz1Ak8Bb2w2I108zQVgKK6eIqNKXJJ/4pizSZdU4920 +wMxYBpnfDAchnxei9U/v3QbT7eKUI2fGr+hOWTIWU80+VeOBt8a6P4sS9AQh5/6G +8qwmAqO3YQ9dxN82iu/H3+N+GGa/M0r5rEWrzwIuFhwKvyQcpPRBm2yQnBnhL9G5 +kN6n4OBM0KsgZ3CYlHZSg4eWcNgBt1WCFsQc7vfUFaJnr8QP3pF4V/4Bok7wTO5H +N0A1EYEVYuX53NGnrKVe+Fg9+xMOgXPWkUNqdvpI9ZbV3Z0S5866qF3/vBZrhgCr +Kc5E/vMexBRe8Ki4wKqONVhi9WGUcRHvFEikc+7VrPj0YaG6zVLd+uOAJN81fKOP +Yo4X4sZrMyPYl3OjGtMhfV4KvCaLEr1duOklqO6cCvGQ8iAlLVy3VJyW5GJ0D0Ky +iAir4VNdAJKo1ZgiGivJLWulTfjUifCN9o115AiqJxiqwwIDAQABMA0GCSqGSIb3 +DQEBCwUAA4ICAQCQOdwD7eRIxBvbQHUc+m0TRzEa17BCfck1Y2WwN3TZXDGSkPVE +0uujA8SL3qi8/CTLGRqI9U3gRZJf+tJPBF/P021PEmyaFTS4htxcDxTxuZv2jCo9 ++XhUEyvRWitTmoy1esq3mkotVQHeTmQvwCsQJAhctVA/hRdJwmMPs1B8QxOUI6Bq +SOBHa9CsXIzVOFv8FqE91PZA2ns30sKQYrrnbH99apfF5WglLUoyPwxf2e3AACh7 +beEdk45ivvKwi5Jk8nr85KDHYPlqkr0bd9Ehl8xplaNBdMPeRufqBDlztjcLJ3wo +mnrt95gQMeSoLHY3UNsIRjbj43zImu7q9v/DD9ppQpu26aRDRmBNgLZA9GM5XnbZ +RFi3VxLyqasGcSzaHwz5c7vOBOkOdlqcQzISRvWDxiN1HkAL+hkiQCuMchgORAgM +wzPooa8rfWtLIpOXMpwuVGb/8rGNLEPovoCK9z6c+WZ+zkRo4+3TQkOMY66Xht7r +Ahly3ler+Tyg6a5jXT92WKC/MXBYAy2ZQNoy204kNKevcH7R2cSkxITd3n5EacNy +5MAtCNIk7JweLCh9rLrLUBt+i4n44sP+LVhfWHemngA8CoF4n6eQ0pp0ixZTen0j +4uN0G2Nf+JeGMlqoObLWdIOdH/pbDppXGoZaKKDd7+bA74Fle5Uh7+1e3A== +-----END CERTIFICATE----- diff --git a/server/docker/localflare/staker2.key b/server/docker/localflare/staker2.key new file mode 100644 index 0000000..c31bc80 --- /dev/null +++ b/server/docker/localflare/staker2.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKgIBAAKCAgEA3U6EetJ2ujbkfYkgFDMsz1yTTFlUGy8k+f4mQoidwQeGFjAY +X8aSViYll3DPs5w4KSbsghHoeQ6emWNxXu5Oh4behZhaQ48J46UJnZBClINNBvhJ +T06f1LJrDy/iQLMD6gWVqTAUt3bF0DbpzDLTBpa3+UtJel1+Mu5HPxk8WIL3lmfS +bUf5tijXq/sI4AR6ieKz6pxwd9LA2D2YPdQs9QJPAW9sNiNdPM0FYCiuniKjSlyS +f+KYs0mXVOPdtMDMWAaZ3wwHIZ8XovVP790G0+3ilCNnxq/oTlkyFlPNPlXjgbfG +uj+LEvQEIef+hvKsJgKjt2EPXcTfNorvx9/jfhhmvzNK+axFq88CLhYcCr8kHKT0 +QZtskJwZ4S/RuZDep+DgTNCrIGdwmJR2UoOHlnDYAbdVghbEHO731BWiZ6/ED96R +eFf+AaJO8EzuRzdANRGBFWLl+dzRp6ylXvhYPfsTDoFz1pFDanb6SPWW1d2dEufO +uqhd/7wWa4YAqynORP7zHsQUXvCouMCqjjVYYvVhlHER7xRIpHPu1az49GGhus1S +3frjgCTfNXyjj2KOF+LGazMj2JdzoxrTIX1eCrwmixK9XbjpJajunArxkPIgJS1c +t1ScluRidA9CsogIq+FTXQCSqNWYIhoryS1rpU341InwjfaNdeQIqicYqsMCAwEA +AQKCAgANGUOgHWrnlK4re/1JFMpXL6yMPVFMFptCrLdJAtsLfM2D7K7UpGUu8i0R +bJzujZWJYgNno3W2DJZ4j7k7HDHLtcDf+WeGTiYQskkCaXJ3ZdoeSn3UUtwE89aA +XJ4wpCfcJx53mB/xx/bnXwixjGSPJEaZW8pqkrQQgaf35R98Qawz28tJqpPuIza4 +uDALSliSZretcDr77J57bhHfvvo2Oj/A3v5xqeAv5BaoXWAQfg5aLWaCaUAOhJGP +dbk+pJazsxhSalzVsZvtikWD9focex0JFZtj2C+Qy5i6V5VzVhQULnN1vKMXqRfB +hgC7rgtgaJGWHgmRzEBF8y1EEE1fohbo2sqkG4oMz3jBZ4o4MADQcpfK2qchgrnk +OxIS/uU8szdu84iH8s6F/Hl1+87jnq6O9Re0iMSuvyUbjAEe8Cm9P/a5M1X9eyzw +WSXSPZBwKSRoP3wuycbEonTWQnQHdwySY+IvdtgliEDhKrVbZGnks5zmaaIydW/y +LS2S9JRM5Y+Xp0vV3nGlEehCUdrXoQ1Dz/AiHnWHjbxoCFGt0qL6COJziAGfUXKa +cQ5iDd7zc2J3m2Z6c8W8xkPJe+1dmNWfGHrja8DSHtTcDY6Aqd98Vu0niu8PC7bx +Avw++6J2wG7LN89rgR0uP7as9Cx4kHHsOFwp+lKODVe2dw0vAQKCAQEA7moNCjP6 +5PkSkSNPi/jw1Y/FCwBoJEzl3Q5ftwGrkYZFRLBTvCCli2jhexaC0D9+yjsVaL/2 +Vap43/xi55ipniqv8c1WAc5xFh+6hCydS6K9owDxlHN75MGLrmrYjY+3aMdo15Dm +x5bznOLLyMUW4Ak+77MTw12fad/7L0ANXumFFj6ydcS8PHmhJlmz5VegWz5b1KGQ +K//phcuOm349xekt7J5kKRbDEqLOlZv/EIAdCBQM4U3d6P/2vUUy5nKYG0F1xeaC +leVpr1EPoEI+XkTy+jjoaBs7iUHpcD359XQCWLniwf1Yfttk9zJp7m6tR/Geablk +unnH5zyFkwzlQwKCAQEA7aFtNsjL0UEXlyBYjCROoPu6ka/o4QyEaWvMHciXu+Gv +M7TQCF2i9oeQXABIyTNoQr+pNjARboY8p0+9ZfV8QGlvH6awW2MNzD07lg9hwsjY +JOCI64XxZj183GhHgN9/cE4PXBrQCqPLPCKdV66yAR9WNm9Va3Y9Xf/RvcoLiNB1 +FAg5bhbNQMnR38nPJs9+suSqYB8xADKvwmKEdony+WIM/GQyYZiDlXEj8EfWQouM +wAok6Vuhs6cuLiHHzXFR4Y6RCWRb2nf2VrzWopz2Bp02IeHY0UZsZeKnqha9dtUu +ZCIt2MZUELxih9JS+wzCX8BJk3xedi89zOZKRx4MgQKCAQEAxqnUJ9ZckIQDtrEn +zckoVayxUpOKNAVn3SXnGAXqQx8RhUUw4SiLCXnhucFuS709F6LYGisrRwMAKhST +Dc0mOcf0SJcDvgmaLgdOUmkiwS3gu31D0KHScTHeBP6/aGaDPGo9sLLruxDL+sT5 +bljc0N6jdPVR2I+hEIY1NpA3FAmefoTMDFpdSD9Jyz0gLFEyLBXwS2Q9UIy0uGqA +cI1nSA0f2XW6nIp9DoBfiEcu6T738g1TFkLeURNJNTn+SgzfNob7bmbAFcvOnun7 +DV1lvwPRPDRDZMycdalYrdDXAnMiqXBrxZ4oKb0DiwCVSLss5TAvAoYbq09jBgpm +e7xZJQKCAQEA3f7l0b1qs5WU3UmJj3rHvhsNY9crvzr7ZKUhLl3cathe3fY4NuiL +OrbQxTI6zURqTZlSEl57mn5roX6cGOlqZ55YAwCtVuLF3B0EUp8SHG+XhXQCVc1v +BK3CvQHqctnY62jxboFaA+abEhXgWi7I+sV0vCvsaBUxJWS9ZAmiFvFvvwQj6tYA +cFta5y9YiBBmc+etx1i8ZUv06Ksyxq7/P707Fnrgmk5p9y2YfnwODWLjXfDcJOnG +udggC1bhmusXrJmMo3KPYRybFNMbzRTHvswV6zdbX77ju5cwPXU7EQ39ZeyMWiyG +EpB7mBmEDicQW3V/Bvq0IMLngElP8PqAgQKCAQEAq4BE1PFN6hQOqe0mcO8g9mqu +zxl2MM0Kb2ABE8fxQ2w4Fy7g42NozDUW13/MN7q1I+AwMhbl4Ib2QImEMTuFaHPY +A3OZlnE9L0oi4FI+kG2eJOB/+5pHSuf/jrZ/4gARK+uc/CDeaIljP/nxw0cX+sF+ +HjX4Ob4/CyEIeIUGdOGs7g9kf+oirXryuDcZxl/2fQOxqva9dhhBLhPXG3otSp0T +D90xC1lSPLIHf+VUiF9bLMtUp4meGcgwpXPVjRV5cblLrP9PxbevlhG2D3vnOK9A +8jWI9P1uNBEAUTSmXV8reMYOyNXJH8YbbT4yiarWnaQM0J0ipWwXGEeWagv/aA== +-----END RSA PRIVATE KEY----- diff --git a/server/docker/localflare/staker3.crt b/server/docker/localflare/staker3.crt new file mode 100644 index 0000000..65781b0 --- /dev/null +++ b/server/docker/localflare/staker3.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFNzCCAx8CCQC687XFxtDRSjANBgkqhkiG9w0BAQsFADB/MQswCQYDVQQGEwJV +UzELMAkGA1UECAwCTlkxDzANBgNVBAcMBkl0aGFjYTEQMA4GA1UECgwHQXZhbGFi +czEOMAwGA1UECwwFR2Vja28xDDAKBgNVBAMMA2F2YTEiMCAGCSqGSIb3DQEJARYT +c3RlcGhlbkBhdmFsYWJzLm9yZzAgFw0xOTA3MDIxNjEyMjJaGA8zMDE5MDcxMDE2 +MTIyMlowOjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMRAwDgYDVQQKDAdBdmFs +YWJzMQwwCgYDVQQDDANhdmEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQC8mVDToHbkUF2gRdVfpydZLNKeQ38d6HZFkUM3U1dWLZFSZNvagN8hlQvY/tQu +3A40p19WgKbzWZre3tg1Akw8Jztdz9gl4RMn142IIO3CiwIptkE0JopbZhmG5fAC +2n/MXQtfieI3hzeR04LW4JgLKzf3Nn8xZdlBgJfBmL5qUUnE7O7IbJGGma6gSD3e +wetE6KQZtNtf0xRIv08doZKYwTl6ItkdGK76ufqq098GVwWvA1wSune4+MFgs9N4 +eFJj6Jyt85fiK/cwPx7KRdgYgBzrZQ4EPshRnwWrBTieOOaJvAA2RMxMEYzKRrJA +AsYI1zxtNyqIUaBTcxmaz+NXUGW+wHwITic0Gp/XQm2Lwr/lxIV6OnAlL3CgbSXi +rSnoG+eHQ+vDzBAcRDkTAgv/GUIzlfqT2StTK02uIBgJYzvFTG4plHitccRfy8wx +sh5Z8xG99lmPQQtLsnlQAV+Li06Cb8CH4hUVoiWiVs5QAahqWmv5fpoX0Es26RyU +HXGbjE202pyMMA7jUerUVKMijOoGZtcH6zB4p/dJ0TtToRwOgrA7NCI9AYVtqVXr +XG/udj8ur2r1bTVwIbHsOeTEP3gY0mHRWm2E/bLjt9vbYIRUxR8xWnLkbeBziNTw +g+36jdDF+6gu3cUz/nbSn8YY+Y1jjXuM3lqF8iMaAobhuwIDAQABMA0GCSqGSIb3 +DQEBCwUAA4ICAQAe2kC0HjKZU+dlnU2RlfBpB4QgzzrFE5N9A8F1MlE4vV3AzCg1 +RVdHPvniXzdNhDiiflK0l/cnrFv2X1TzYMrrA677/usHf2Bw0xjm/ipHOt5V+4TN +mZAIA4IPl09gP28IZLc9xSuq4FoHeM8OTxhttOlINhqpG9P5d6bPezW6ZzI3CdPP +CF69xK4GFlj/NQnAoFogid4ojYYNTj/cM4PYQU2KbrlzLyPuUk/CgwefXLMH87/H +e3kPDev80Tjv2Pm5nD937fZfgrEoyolKxiRVcfZVMxR7qhPhizjueD0DAkfQIs7L +YVSyx/qjEv2bBYaim5RQakUeHR1Xu5Xj/k5zr33t979ede50byQrcWm4H5JxnEpD +JxJnFfDOU6o14SKGHSrao5Z4C3dI55DM84WLASnlMI5BK4XtS3notLNzG8dfWWhT +9m0Hcry+wPNDcGr8Mtj1los/0bMDqMHC4jcFW1hrXCUUs9RYzE+N/xoqwCQSgN1P +E73uXTySWj5ovMR5TPF6PhcftLB/OziqO7FverEBpvGGHUAnUT61JtjodjXPbEdj +0VgyMOBY2y53HTXnx3dxeFZkUdRX/VZYy8tMK3MTY+7UIU5cWYnCZAo5LNcc0ukR +S6WS9+6eaQ6XRjhfNUjx9a7FzqapWdtTedpipmBP1Njap3g29iUuVnLQeg== +-----END CERTIFICATE----- diff --git a/server/docker/localflare/staker3.key b/server/docker/localflare/staker3.key new file mode 100644 index 0000000..504cdfe --- /dev/null +++ b/server/docker/localflare/staker3.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJJwIBAAKCAgEAvJlQ06B25FBdoEXVX6cnWSzSnkN/Heh2RZFDN1NXVi2RUmTb +2oDfIZUL2P7ULtwONKdfVoCm81ma3t7YNQJMPCc7Xc/YJeETJ9eNiCDtwosCKbZB +NCaKW2YZhuXwAtp/zF0LX4niN4c3kdOC1uCYCys39zZ/MWXZQYCXwZi+alFJxOzu +yGyRhpmuoEg93sHrROikGbTbX9MUSL9PHaGSmME5eiLZHRiu+rn6qtPfBlcFrwNc +Erp3uPjBYLPTeHhSY+icrfOX4iv3MD8eykXYGIAc62UOBD7IUZ8FqwU4njjmibwA +NkTMTBGMykayQALGCNc8bTcqiFGgU3MZms/jV1BlvsB8CE4nNBqf10Jti8K/5cSF +ejpwJS9woG0l4q0p6Bvnh0Prw8wQHEQ5EwIL/xlCM5X6k9krUytNriAYCWM7xUxu +KZR4rXHEX8vMMbIeWfMRvfZZj0ELS7J5UAFfi4tOgm/Ah+IVFaIlolbOUAGoalpr ++X6aF9BLNukclB1xm4xNtNqcjDAO41Hq1FSjIozqBmbXB+sweKf3SdE7U6EcDoKw +OzQiPQGFbalV61xv7nY/Lq9q9W01cCGx7DnkxD94GNJh0VpthP2y47fb22CEVMUf +MVpy5G3gc4jU8IPt+o3QxfuoLt3FM/520p/GGPmNY417jN5ahfIjGgKG4bsCAwEA +AQKCAgA+uHIT3yKK7VslqPO7+tfwJSLqNSI6LQvgON30sUezRjY1A4vGD+OkxG+L +O7wO1Wn4As2G9AQRm/QQOGYIwvnda2Kn4S5N8psvPdU4t1K6xwXyH0Vx9Xs/yCWn +IiL+n/GuYicdH7rWoqZNXdz+XvTRig7zrPEB2ZA143EUlhqFOwFgdzc1+j0vWT6k +2UGSKkV2xjOExQvLw2PUiaLjBM++80uNHbe8oG/YvC7rzsg10Iz4VhKxu8eDAV82 +LLegMcucpEgu5XrWYa60Idm4hR/HjhuQASx3JvXxhwQYiwT4QY4Rsi8T3S9gANok +jvxKo2F+oS3cWGNRsGu0NOwH+yjsVyMYazcLOUesAAe85ttXgYr02+Z/uMnxqtOF +gjIHY3X5QZbD4l4gbwx+PLbjsj4KC6r3yZrr51PdLUrBvoqBhqwuCksdaMntWGME +u0V/ooJi4+uzCYzN06jFfAFXa2pWzVB5yKw1d6yYi9U/bPd4xn1phLUMHrC2bvdM +H8P18gAS6rkWn+ageiWRHmkf4uoKgv3PrMjijkBaGpf6xjv6+0Q393jdVIC7wgJV +8W0i1f1Awv68089mHBEarPTv3gz39251WFCPNQhEuSy79Li5zjwOprZXS0MnJXbm +B00IPTIw51KuaoueWzY1pA2IFQ/0sH3fo2JhD0pp1gI0Dde7EQKCAQEA7RVgNelk +3H3ZJsoOfOTFa03lnXuEfTAyxhEECRz64k54pSbEWV73PIeYByZvnsrKRydpYWUP +Cp/mKhAJH4UCf2hzY0GyO7/D6+HEKZdCg6a0DNKckAoFkBfeOlLJLjLVAW2eEVxz +tlFt+/WBE90GCvE5ovXuEhXGaPxCPp5giIN5phSzSD557bwwOyPwNKFZ7Ao77UNK +kz6EzcvQgqb205SRRKGpS8/T/9LcLsUYVkBfYQ/BayjffO+cQF4vH5rB4x/8/T7t +uUa79uY+LeGHgTSFIAui9LEK5ry//2hDJINsItYMks1Qo4Suu23pOuGerjiFTKWl +mOIoFmPmbebAcwKCAQEAy6WaJczPcKQQ/hqglQtxU6VfP49ZjUKkoi+OCmtvKDts +7pJfIkuluhnYGeqFfjnopw5bgZHNFm6Gccc5CwBtN6Wk1KnnOgDIg3kYCrfjtKy/ +BSSV3kLEBvhe9EJA56mFgP7RufMbHTXhXPGLkgE7JBZj2EKxp1qGYYVZesTMFwDM +KEHwzIGcFkyZsd2jptyLYqcfDKzTHmFGcw1mdtLWAUdpv3xrS3GvrCbUMqIodjRd +qkrg/d/kQpK7A3oLOWfa6eBQ2BXqaWB1x13bzJ2WlshxJAZ1p1ozKii5BQ9rvwWo +muI5vd7o6A9Xsl8QzluSSSPi+NhjZ64gMBrXciRvmQKCAQB/dB5k3TP71SwITle7 +jMEVDquCHgT7yA2DrWIeBBZb0xPItS6ZXRRM1hhEv8UB+MMFvYpJcarEa3Gw6y38 +Y+UT2XMuyQKoXE9XX+e09DwtylDBE/hW9wxGio5NjHPbAjjAq81uR+Vs/hnCehkK +NKgq+cOid9OkpVAk4Hg8cagzu3qKblZzYCLsS18ibA+WO6e73USaKLLOta1vdUKC ++n92/0eZPc9lkjTGMvVrr0mGFNUxuOaiVTbQU4AMmpV6yBezol6/RjVGhWBHOz/y +KmxOaY2nzJmuMf9KS+5rwAFYf86Ca9AWm4neXlYRLOVVYjWMM5Z1vhdoOSyT3ODj +9ElBAoIBAGCRPaBxF2j9k8U7ESy8CVg10g3MxxVSJcl2rW9JdKNqUoRqykvz/Tlb +afsYF4c8pJMbHs85OTxK2tv3MZiC8kdx99CUZL4/gtW9RWZHvuV9CPPCXoLPvC7l +9fjztd1kqJb7vq3jltbqJtyw+ZMZnFbHez8gmSeXqKNz3XN3AKRjz2vDoREI4OA+ +IJ+UTzcf28TDJNkY1t/QFt0V3KG55psipwWTVTmoRjpnCzabaH5s5IGNElWwpoff +FmlWpR3qnodKxGtDMS4Y/KC2ZDUKAU+s6uG/YmkiP6LdPqckod4qK8KORf1AR8dL +BzXhGJISIDMonkeMLM8MZd0JzWIl3vkCggEAPBkExd2j4VY5s+wQJdiMto5DDoci +kAEIvIkJY9I+Pt2lpinQKAcAAXbvueaJkJpq31f6Y66uok8QnD09bIQCABjjlIve +o7qQ+H8/iqHQX1nbHDzInaDdad3jYtkWUHjHPaKg2/ktyNkFtlSHskvvCEVw5aju +80Q3tRpQG9Pe4ZRjKEzNIpMXfQksFH0KwjwAVKwYJLqZxtNEYok4dpefSIsnH/rX +pwK/pyBrFqxU6PURULUJuLqRlaIRXAU31RmJsVs2JbmI7Cbtj2TmqAOxsLsi5UeJ +cZxcTAuYCNYMu88ktHul8YJdBF3rQKUOnsgW1cx7H6LGbuPZTpg8Sbyltw== +-----END RSA PRIVATE KEY----- diff --git a/server/docker/localflare/staker4.crt b/server/docker/localflare/staker4.crt new file mode 100644 index 0000000..92128d0 --- /dev/null +++ b/server/docker/localflare/staker4.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFNzCCAx8CCQC687XFxtDRSjANBgkqhkiG9w0BAQsFADB/MQswCQYDVQQGEwJV +UzELMAkGA1UECAwCTlkxDzANBgNVBAcMBkl0aGFjYTEQMA4GA1UECgwHQXZhbGFi +czEOMAwGA1UECwwFR2Vja28xDDAKBgNVBAMMA2F2YTEiMCAGCSqGSIb3DQEJARYT +c3RlcGhlbkBhdmFsYWJzLm9yZzAgFw0xOTA3MDIxNjEyMjVaGA8zMDE5MDcxMDE2 +MTIyNVowOjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMRAwDgYDVQQKDAdBdmFs +YWJzMQwwCgYDVQQDDANhdmEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDZnDoDHE2nj82xDjH0Tb7OXMqQDHz+zbLidt6MSI1XB3vOAIEiPqrtenGnqRbV +Fcm5GZxvxh4YQD8CjKSk1qgZJczs0DPSiGQ8Efl4PGO4xnEbllgL3PURPWp7mEV3 +oh6fxICgQKTBlT671EnFzB5lyJWpumRzvA1vyhBMsY8aO+xdq5LUFltYzBdvpgLX +VaDwHZQ2PQEWtF0d0JO2N0WFFDGNmx6n8pKSeIAVDsTwZCZK+FCeeEyoGfXsINsc +0yCMQslawkfOMqA9yBV3Ji6QmFYKyGYt65MWGNqPA4XrIyliKwCCXwz9mjaWyN7r +Ayw9cWlLMODNmDORWzGRZ5290MEAEIZsqjYHVitRTM/RnNIadToZGO0y5uAkM14c +mTvnsK1CP92qtfSisq75W/I91drThoEtTK78UGOl/5Q1YBR08F+tSUWZWyHeI6UO +BUCGC2bCtmzKMl7vU25lG6mbCR1JuQi6RYpnfMjXH36lV4S7fTvSwwuR03h2F3H1 +eFkWNG2lbFrW0dzDCPg3lXwmFQ65hUcQhctznoBz5C1lF2eW03wuVgxinnuVlJHj +y/GrqmWsASn1PDuVs4k7k6DJfwyHAiA0uxXrGfxYvp7H8j4+2YOmWiWl5xYgrEDj +ur5n8Zx46PHQer2Avq3sbEGEe1MCtXJlj3drd5Him3m+NQIDAQABMA0GCSqGSIb3 +DQEBCwUAA4ICAQA40ax0dAMrbWikaJ5s6kjaGkPkYuxHNJbw047Do0hjw+ncXsxc +QDHmWcoHHpgMQCx0+vp8y+oKZ4pnqNfGSuOTo7/l05oQW/NbWw9mHwTiLMeI18/x +Ay+5LpOasw+omqWLbdbbWqL0o/RvtBdK2rkcHzTVzECgGSoxUFfZD+ck2odpH+aR +sQVu86AZVfclN2mjMyFSqMItqRcVw7rqr3Xy6FcgRQPykUnpguCEgcc9c54c1lQ9 +Zpddt4ezY7cTdk86oh7yA8QFchvtE9Zb5dJ5Vu9bdy9ig1kyscPTm+SeyhXRchUo +ql4H/czGBVMHUY41wY2VFz7HitECcTAIpS6QvcxxgYevGNjZZxyZvEA8SYpLMZyb +omk4enDTLd/xK1yF7VFodTDEyq63IAm0NTQZUVvIDfJeuzuNz55uxgdUq2RLpaJe +0bvrt9Obz+f5j2jonb2e0BuucwSdTyFXkUCxMW+piIUGkyrguAhlcHohDLEo2uB/ +iQ4fosGqqsl47b+TezT5pSSblkgUjiwz6eDpM4lQpx22MxsHVlxFHrcBNm0Td92v +FixrmllamAZbEz1tB//0bipKaOOZuhANJfrgN8BC6v2ahl4/SBuut09a0Azyxqpp +uCsyTnfNEd1W6c6noaq24s+7W7KKLIekuNn1NunnHqKqriEuH1xlxxPjYA== +-----END CERTIFICATE----- diff --git a/server/docker/localflare/staker4.key b/server/docker/localflare/staker4.key new file mode 100644 index 0000000..d51233e --- /dev/null +++ b/server/docker/localflare/staker4.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEA2Zw6AxxNp4/NsQ4x9E2+zlzKkAx8/s2y4nbejEiNVwd7zgCB +Ij6q7Xpxp6kW1RXJuRmcb8YeGEA/AoykpNaoGSXM7NAz0ohkPBH5eDxjuMZxG5ZY +C9z1ET1qe5hFd6Ien8SAoECkwZU+u9RJxcweZciVqbpkc7wNb8oQTLGPGjvsXauS +1BZbWMwXb6YC11Wg8B2UNj0BFrRdHdCTtjdFhRQxjZsep/KSkniAFQ7E8GQmSvhQ +nnhMqBn17CDbHNMgjELJWsJHzjKgPcgVdyYukJhWCshmLeuTFhjajwOF6yMpYisA +gl8M/Zo2lsje6wMsPXFpSzDgzZgzkVsxkWedvdDBABCGbKo2B1YrUUzP0ZzSGnU6 +GRjtMubgJDNeHJk757CtQj/dqrX0orKu+VvyPdXa04aBLUyu/FBjpf+UNWAUdPBf +rUlFmVsh3iOlDgVAhgtmwrZsyjJe71NuZRupmwkdSbkIukWKZ3zI1x9+pVeEu307 +0sMLkdN4dhdx9XhZFjRtpWxa1tHcwwj4N5V8JhUOuYVHEIXLc56Ac+QtZRdnltN8 +LlYMYp57lZSR48vxq6plrAEp9Tw7lbOJO5OgyX8MhwIgNLsV6xn8WL6ex/I+PtmD +plolpecWIKxA47q+Z/GceOjx0Hq9gL6t7GxBhHtTArVyZY93a3eR4pt5vjUCAwEA +AQKCAgBMoBNZZwz9FMkEMJBsizfF6Ky3Pn6BJqN31Q2WbjG+1HbG2iyeh1ye1L/S +ntrYW5y1ngwU27lbJrxJRIbxOFjmygW32bR1zOsmr9mdef5PYSkQ4sbMHpj44hxt +uvezIZYRAhuc0kZxmAEIGL+Fc9O8WX5Bzs1yZ2R/2bIVn2xZe4JGlZTVM64kvXD/ +MoDLnG5YPsIiuyZ3/TjQt9JblmjXbH3qdBW+Y88y3lWTlKjKUSmeuoOA2bF8e++5 +nvQo2TsbyKSoXcL1G6SLPLo6Q2qgJdQeZeR9BPe9DzFerInqe24mEChUv+2OG1Bf +lgnQzUQ1uoquHF78Zjy6UVdJ8Sd8ufvKC9rz8JYsIynfw0gQC3F8/emm1QSabFvY +tG4+x0K8FgrijjE08RvqgIndx9ftCNoN4u3lXxPrJhKpr2xuXSa4VZbumgN7fqWx +UBC8lmPQi5VZmj3nJfj4datmBTvs1dOLRMdfdtTFz+cAdWNZxX3HOLZUSqMVWgXY +kX0s7IV9GnyUntBktX+IEbWlAttzldyqF9md4avjKXQ+Y4PK/sR1yWsuvtiZdYUL +/QrQHX0CsVv1hRcX0yekA0a8qwaGmxEcndEKv7wF1i626jc2fDR6qI1yp20Xl3Si +kYBSNh7VK210XIhddSuVxW5/gyNnFABDfp1bSdTh5ZJRfNvtQQKCAQEA9Zipnyu8 +JKlLtWrh2iP9pmFBaeUgoF68IVfd6IXdV7nAHSWysiC43SCUmI63xluMYEJFdAZ+ +m/iRc/n6VFKEBW4+Ujk9VW1E1iqHgSABg7ntEsB2MDcYY0qKsN4CYjC2fNYO97zJ +5oju84U3Qn8TWNkMsrUU7crm2oAQd08AizVFqLo1d8aIzRq+tl952S/lhfXKc/P9 +kfhl+RKjiYC2zbWnGinxc2Nbf5pWwnmtSrceng+ZkgVfSB3HvSckqzENye9YkpVM +GE+KjEdss+QnGQRWM2JPlyoYDmhT6rrasRT6TKsecwo1rRXBi4C1eTZQSnZf24Og +QurS//XzHzbnkQKCAQEA4tQSmaJPZNWYxOHnlivzselfJYyg5JyJVMQWw/7CvTcV +GOpoD4hC25puAniT1U/RyaYaUFZ+Ip2FhK2Qce+uskNgtqFN9phh/DeO1g8CSaIe +6Ebtg8N8GLc0YhdiJtd2XGrktj2XthML7OJPYIidd48tGuQizfijo4Fe1S0rSW56 +B4RHTh/O6a0taNeFbnZQJD52ha9wlnc/PZSCUMb9C0d08dSxdBQV+SVdGrl/IRfC +qHHoC86GYDcmnviD5CFOxpx7AJ/hQAwPFQRCnWGHwDjpcoMOtktyo7pj9MDuzBUb +kr4r1ei8f7PC9dmSYmYzJMQxLfz+Ti2SyyOmdM1CZQKCAQEAsVr4izCLIrJ7MNyp +kt1QzDkJgw5q/ETNeQq5/rPE/xfty16w5//HYDCp/m15+y2bdtwEyd/yyHG9oFIS +W5hnLIDLUpdxWmKZRkvaJP5W+ahnspX4A6OV4gYvl8ALWpsw/X+buX3FE80pOgSm +vkeEUjIUAG3SWlKfWYUH3xDXJLBoyIsIF6HwoqVAufTCynvTNWUlOY0mPaZzBWZX +YPHpkS4wKS3G5nwG1GRBaRlzcjRBUQWU8iUdBLg0yL0ett2qxnwoq1pTZG70b48Y +yePl9CP0mBDTxycnzie7ChS73wt2Ia2lRJBH6OGALlzZMFpvqwZG/P/V2N05WIxl +cNI2cQKCAQEAoys7VhlUU4zzoG2BUp27aDggobpP4yRYBgoo9kTFgaemHY5B3SqA +LckhadWjQsdwekZql3AgvHXkHlVcmxl36fReFgJjOwjTM8QjlAin9KAS67RaF3cA +RidEH2wCxz4nfsPGUvJruCZrZbRGtYKRA/iS0c1a3CAIVw4xUdh0UxaN4epeAO0Q +wzg4ejrPWW7yp5/nUrOpohOWAo5aUBFU5lA4593A6WephthB6X+W3A9jkBigfB3M +vFnwBltvRSRQrr7SHNjmCFSkZNHzuZL3PGe0RxPP+YK8rNrgHKjNHzHv69exYOdS +8eo2TPR+QRqTn9ciKZrctRBDkK3MiCk/oQKCAQAZIZdkOClUPHfSk4x5nBXashKY +gDzeyYHYLwNaBdEKgHNuf6jCltKWoDzZsqrv1Nya/148sTgSTg931bbch+lnHKJd +cXrCQZWBnu2UquisFMeNOvpp0cPt4tIYDZVCRMRrwIlZqIJxb2nAwFvb0fEfLk+4 +gmu+3cCaN/vS3oJA9EFkzjxG0XiLOynyAZb5fY04NmFOIsq3rgT4DeCurHTKtOJ2 +t14oTNq06LD566OnT6plL7vaLtTR/9/qJc007Wjw8QdbTuQALqCjWWg2b7BVkOyR +o9GrhPzSeT6nBHI8EoJv0nxeQWNDX9pZiW/1nsyuAAFJ9ISbDWjz/TwB17UL +-----END RSA PRIVATE KEY----- diff --git a/server/docker/localflare/staker5.crt b/server/docker/localflare/staker5.crt new file mode 100644 index 0000000..e50294d --- /dev/null +++ b/server/docker/localflare/staker5.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFNzCCAx8CCQC687XFxtDRSjANBgkqhkiG9w0BAQsFADB/MQswCQYDVQQGEwJV +UzELMAkGA1UECAwCTlkxDzANBgNVBAcMBkl0aGFjYTEQMA4GA1UECgwHQXZhbGFi +czEOMAwGA1UECwwFR2Vja28xDDAKBgNVBAMMA2F2YTEiMCAGCSqGSIb3DQEJARYT +c3RlcGhlbkBhdmFsYWJzLm9yZzAgFw0xOTA3MDIxNjEyMjlaGA8zMDE5MDcxMDE2 +MTIyOVowOjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMRAwDgYDVQQKDAdBdmFs +YWJzMQwwCgYDVQQDDANhdmEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDgK5r5vdHtJFEgw7hGE/lzKaHcvwzr32armq0k9tYchJXfT3k1j1lXtBAdcUN3 +gSRKjgzH/vjbn0ea3AiDCUd2Mck/n0KcJZ43S5I7ZjP7rbav296bKCZ1Hr7r5gXY +Fhk+3aUsVfDUqAPBwyP8KeV31ARVA/s+WPeWqs69QXTdyJuBYE5pr40v1Sf+ebUI +nZ37uGY3kiO0Ex/JgcoQsGJzrWD/ztbRCFIvrdNJZd0pGvMlmTKp7XsMR3cpvqk7 +70//MLCdyGW/1IArTSuD1vd7mBX1JyVXKycYN0vIOtbgxPOFutUyqDOeP7o51q4i +PS3dCRgfmn/hWLwy+CtJe0BGKsb4tk0tKxo0se8v9JA8mUtnmzmMt4Y9jijOrCOB +7XwWKmJYEm8N5Ubcy6cp2oL8vQVtzz3PXrkFt+3cFt1jrjdpQYgH4jykkWDeOjEf +y1FCwzsNRudLTvLhfLn86/ZT4cLZ9JI7/WW0IPC8Fc7lhznJ+bIQUeEndaGdgVkx +uEg0MxdrMr0jU0IFoXySRXNRzcDWZShEjBTv7tnFxLmoNU+uJb/KpMH6sRYi3zs8 +5ecaMKNyG+LDmBahUlHx5hKAH49O8855+AMhsg91ONZJldjQX0oZrIKzK5BpsqeT +l4c2Yt/fALiZaeFk1pBEsvVeMOBCIuWE+b4UIEaLAOhxfwIDAQABMA0GCSqGSIb3 +DQEBCwUAA4ICAQB+2VXnqRqfG7H2/K0lgzxT+X9r1u+YDn0EaUGAG71s70Qnqbpn +X7tBmCKLN6XgPL0HrN933nwiYrmfb8S33zZ7kw8GJDvaTamLNyem4/8qTBQmnRwe +6rQ7SY2l73Ig87mR0WTi+rTnTTtc66+/jLtFeaj0Ycl9hBZXHKiULSGhsbUbwtkz +iuNlANhoNKXNIABRImUq6OwYhEQN0DwHXj79wkpyDYjKZwHuEZUknc8Pl2oQPBke +mil3tsrvGRkwhisnXX7tqh6rWKVZNJkO68hy7XO9aTXjbcB/7Y1K83ISNEyGPsH/ +pwFyd/j8O4modwh7Ulww1/hwcqnqiEFE3KzxX2pMh7VxeAmX2t5eXFZOlRx1lecM +XRkVu19lYDKQHGSrGxng+BFlSOB96e5kXIbuIXKpPAACoBQ/JZYbtHks9H8OtNYO +P2joqmnQ9wGkE5co1Ii//j2tuoCRCpK86mmbTlyNYvK+1/kkKcsaiiWXNrQsrIDZ +BFs0FwX5g24OP5+brxTlRZE01R6St8lQj4IUwAcIzG8fFmMCWaYavrCZTeYaEiyF +A0X2VA/vZ7x9D5P9Z5OakMhrMW+hJTYrpH1rm6KR7B26iU2kJRxTX7xQ9lrksqfB +7lX+q0iheeYA4cHbGJNWwWgd+FQsK/PTeiyr4rfqututdWA0IxoLRc3XFw== +-----END CERTIFICATE----- diff --git a/server/docker/localflare/staker5.key b/server/docker/localflare/staker5.key new file mode 100644 index 0000000..82c668b --- /dev/null +++ b/server/docker/localflare/staker5.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEA4Cua+b3R7SRRIMO4RhP5cymh3L8M699mq5qtJPbWHISV3095 +NY9ZV7QQHXFDd4EkSo4Mx/74259HmtwIgwlHdjHJP59CnCWeN0uSO2Yz+622r9ve +mygmdR6+6+YF2BYZPt2lLFXw1KgDwcMj/Cnld9QEVQP7Plj3lqrOvUF03cibgWBO +aa+NL9Un/nm1CJ2d+7hmN5IjtBMfyYHKELBic61g/87W0QhSL63TSWXdKRrzJZky +qe17DEd3Kb6pO+9P/zCwnchlv9SAK00rg9b3e5gV9SclVysnGDdLyDrW4MTzhbrV +Mqgznj+6OdauIj0t3QkYH5p/4Vi8MvgrSXtARirG+LZNLSsaNLHvL/SQPJlLZ5s5 +jLeGPY4ozqwjge18FipiWBJvDeVG3MunKdqC/L0Fbc89z165Bbft3BbdY643aUGI +B+I8pJFg3joxH8tRQsM7DUbnS07y4Xy5/Ov2U+HC2fSSO/1ltCDwvBXO5Yc5yfmy +EFHhJ3WhnYFZMbhINDMXazK9I1NCBaF8kkVzUc3A1mUoRIwU7+7ZxcS5qDVPriW/ +yqTB+rEWIt87POXnGjCjchviw5gWoVJR8eYSgB+PTvPOefgDIbIPdTjWSZXY0F9K +GayCsyuQabKnk5eHNmLf3wC4mWnhZNaQRLL1XjDgQiLlhPm+FCBGiwDocX8CAwEA +AQKCAgEApuMPrxmH7Xn6A+BxkYpRTVETNZnt7rQUZXDzse8pm3WBdgxeemdL5iUh +Uin+RjuYXwC9ty606hv8XOeuVo9T6kRKRNk157WBwjy6kwoVbSr4NJgFc5FCgDLx +hAFtHF/nT4wG6ajZcBfdJCU45wPx13G5/+jE5LerKzniS7ctX+d3Daw69CdDfva7 +nZHSGqXs9Xdkcb6UYf1SztuXKTGHOgM7kXXVKy18sg5AnAX/zhhIKBeTRjqMPqn9 +ptBQgVQ6RAtlkTGdvmBfQt1ipfYlrJee0THhdLGlmzufaWOUkSVO/qIHEn1yYD+l +TmXqoYbWXBXnJbAJwCQlh/SFlWDyiWWOxszxdwwT2ybw7OR3a0DEV0MbKJkUexyF +92Lr3qoBSZRFQnXVvBgjQOwnzEFph1ANuGY3odL8JSM1tHniIsCs4WhDPOsbAj+h +kwS51colMk3bNCZ3xeArjMLBVLgT7xLX/7ZYc7/oTEFWik+20TvSEWzdE1N/4gfJ +jEU/VqrnNjyev2w9Ak6bEkwZFLS6VZ9rTWTF9jk8C1aXj/RhfaaC33xXBbhn9HuX +lTu/JaLMp0Qc4aClqUYM6LlxIejH5b8fIxCNHJislXJDa6a6aQl85BiQODPFxVT5 +WCpQD4858EuLdX4BRW2fIGRY6DivR6uJRAmxLf+EwAg/rgTzUsECggEBAPSkHX5F +BhRgudF0MnwN+enj4SoXHhRG+DTorxO1Zh2qN9lnXO9nMKMCXVJLIVvGFuiMRSJ0 +VKf1u0UqaBF02MbIvbei7mzkkW0/74m04X37iyMmtnmooQ0GEV84oONwAt3DeeTg +vIpOtq9V26XHGaQDxcRFMFBuD02a2yf3JYkXj74i2scMP4xxMHMkJxGK9FSBOhnp +k/p0hMl3FVGfo5Ns5T1Rl3pMueEF3B5+BvrV1z14IN/0lwuhujrUUYS4Ew+Pk5zC +FSubfIQMqST1jvXXTaGgX0GPffa4lxgaDEATLewvL3Fjy27Xzl57i9ZvTNC4yFad +4okjr/eItHtKVHECggEBAOqUKww/6uiJMNvc2NxLUMxuzB07rqOZKT2VMBkG5Gzk +v81fDtlndD8cwHSqOLKscH/QKXD7WK3FCuvZSvMwCjEB4Pp1zgwJoBexuXvFDDbs +0T77Qiwe+2WmRIiYev5aRG3lnBMM8RDS/QPzEdoxHdzrFURYVl0rv5l/7rwB2Zd6 +xAYHcUpZc4ZaysEgqQCuZQqC7Mrq7qfByUthH28Yicz1978fpE3dx15ceqjU9jBQ +xUUwbeKT/UkQQvmYHdtgwEjhzVQL1OAAWkT6RssMqx2RAdi0SqWPFEhxNPHBpG9B +lKUDBBIM6du916On0Bjghh3WhxQKpTIzveNAiexbXO8CggEBANvJohGyc37VU7wg +18ZqTA/cwostD8IJ7K6kKb7cJy0Zo2l3mqAfJiwdULhBdWvdMPGmK+qDdxcbBy9h +pPOh9avJ5+BWyjwcsabkXRFr53ZnCp7/BcuRO3fW7r6Mwsby+DBCkX2Whuz/QNOP +oHF0yc138jKeMoTgDHGdYa2rNhbPiz24VLOlhmZnvq6DWXJCU7akDw3+swq9qhrS +GN4nPS+TEvUfG6ctzYWj3RmsAhtTCThZd7edKCK0HvsBi2dgdQdy55xbJefynlCI +i2IAF3s4/q7pxQrCntmNB3oI1N6wHH7n+Yi2rqsbyXVLK9vwTKPsj1h6Km8pF8ud +DwEBS5ECggEAMnq2FMnAbE/xgq6wwB85APUq2XOZbj0sYcMz+X7BMym6mKBHGsOn +gVlXlQN4dgKjpu2NrXF5MNPBOOWmulRxLQChgGRPdcmweMjXCGpr6XnmwW3iXIpC +QSqZfueJOCkGpruNbZAQZDVzGyF4iwKc0YiJKA72btBWR9r+7dhcEbvqaP27BGvh +b10kWpEDrVDaD3wDJtuNhe4uuhjpYcffB4s6yBcwDU2XdJfkEWban6UR/oSgcOy1 +yb5FG17/tdDJMCXfQKHXKmkJA+TzzQgp3o/w3MhXc+8pRzmNUiUAlKyBJ01R1+yN +eqsMt3wKTQAr/EnJAagUyovV5gxiYcl7YwKCAQAdOYcZx/l//O0Ftm6wpXGRjakN +IHFcP2i7mUW76Ewnj1iFa9Yv7pgbbBD9S1SMuetfIWcqSjDiUaymnDdA18NVYUYv +lhlUJ6kwdVusejqfcn+75Jf87BvWdIVGrNxPdB7Z/lmbWxFqyZi00R90UGBntaMu +zg/ibrLgatzA9SKgoWXm2bLt6bbXefmOgnZXyw8Qko70Xxtx5eBR1BDAQjDis81n +Lg96sJ3LOn7SXHfxJ3BtXshTJAoBFx6EpmulgNoPWIkJtd7XWYP6Yy22D+kK7OhH +Rq3CiYMtDmZoub/kVBL0MVdSm7hn1TSVTHjFoW6cwQ37iKHjkZVRwX1Kzt0B +-----END RSA PRIVATE KEY----- diff --git a/server/go.mod b/server/go.mod index a7cb3af..6a4981e 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,99 +1,118 @@ module github.com/ava-labs/avalanche-rosetta -go 1.18 +go 1.21 require ( - github.com/ava-labs/avalanchego v1.7.18 - github.com/ava-labs/coreth v0.8.16-rc.2 + github.com/ava-labs/avalanchego v1.11.2 + github.com/ava-labs/coreth v0.13.1-rc.5 github.com/coinbase/rosetta-sdk-go v0.6.5 - github.com/ethereum/go-ethereum v1.10.21 - github.com/stretchr/testify v1.7.2 - golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d - golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 + github.com/ethereum/go-ethereum v1.12.0 + github.com/stretchr/testify v1.8.4 + go.uber.org/mock v0.4.0 + golang.org/x/crypto v0.18.0 + golang.org/x/sync v0.6.0 ) require ( - github.com/BurntSushi/toml v1.1.0 // indirect + github.com/DataDog/zstd v1.5.2 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/VictoriaMetrics/fastcache v1.10.0 // indirect - github.com/aead/siphash v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/btcsuite/btcd v0.23.1 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect - github.com/btcsuite/btcd/btcutil v1.1.1 // indirect - github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect - github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect - github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect - github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect - github.com/btcsuite/winsvc v1.0.0 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/btcsuite/btcd/btcutil v1.1.3 // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cockroachdb/errors v1.9.1 // indirect + github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811 // indirect + github.com/cockroachdb/redact v1.1.3 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/deckarep/golang-set v1.8.0 // indirect - github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0-20200627015759-01fd2de07837 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect - github.com/decred/dcrd/lru v1.1.1 // indirect + github.com/deckarep/golang-set/v2 v2.1.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/dlclark/regexp2 v1.7.0 // indirect + github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3 // indirect github.com/fatih/color v1.13.0 // indirect github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect + github.com/getsentry/sentry-go v0.18.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-stack/stack v1.8.0 // indirect - github.com/golang/mock v1.6.0 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/google/go-cmp v0.5.8 // indirect - github.com/google/uuid v1.2.0 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect + github.com/go-stack/stack v1.8.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect + github.com/google/renameio/v2 v2.0.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/rpc v1.2.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/hashicorp/go-bexpr v0.1.10 // indirect github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect - github.com/holiman/uint256 v1.2.0 // indirect - github.com/jessevdk/go-flags v1.5.0 // indirect - github.com/jrick/logrotate v1.0.0 // indirect - github.com/kkdai/bstream v1.0.0 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect + github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c // indirect + github.com/klauspost/compress v1.15.15 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/pointerstructure v1.2.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pires/go-proxyproto v0.6.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.12.2 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.32.1 // indirect - github.com/prometheus/procfs v0.7.3 // indirect - github.com/prometheus/tsdb v0.10.0 // indirect - github.com/rjeczalik/notify v0.9.2 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.39.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rs/cors v1.7.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/segmentio/fasthash v1.0.3 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/spf13/cast v1.5.0 // indirect - github.com/status-im/keycard-go v0.0.0-20200402102358-957c09536969 // indirect - github.com/stretchr/objx v0.2.0 // indirect + github.com/status-im/keycard-go v0.2.0 // indirect + github.com/supranational/blst v0.3.11 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect github.com/tklauser/go-sysconf v0.3.5 // indirect github.com/tklauser/numcpus v0.2.2 // indirect - github.com/tyler-smith/go-bip39 v1.0.2 // indirect + github.com/tyler-smith/go-bip39 v1.1.0 // indirect + github.com/urfave/cli/v2 v2.17.2-0.20221006022127-8f469abc00aa // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.8.0 // indirect - go.uber.org/zap v1.21.0 // indirect - golang.org/x/net v0.0.0-20220708220712-1185a9018129 // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect - golang.org/x/text v0.3.7 // indirect - golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect + go.opentelemetry.io/otel v1.22.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect + go.opentelemetry.io/otel/metric v1.22.0 // indirect + go.opentelemetry.io/otel/sdk v1.22.0 // indirect + go.opentelemetry.io/otel/trace v1.22.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect gonum.org/v1/gonum v0.11.0 // indirect - google.golang.org/protobuf v1.28.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/grpc v1.62.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect - gopkg.in/urfave/cli.v1 v1.20.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/server/go.sum b/server/go.sum index 01b0a7d..5d7880f 100644 --- a/server/go.sum +++ b/server/go.sum @@ -1,36 +1,5 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc= github.com/Azure/azure-storage-blob-go v0.7.0/go.mod h1:f9YQKtsG1nMisotuTPpO0tjNuEjKRYAcJU8/ydDI++4= @@ -45,39 +14,40 @@ github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= -github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= +github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= +github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/VictoriaMetrics/fastcache v1.5.7/go.mod h1:ptDBkNMQI4RtmVo8VS/XwRY6RoTu1dAWCbrk+6WsEM8= github.com/VictoriaMetrics/fastcache v1.10.0 h1:5hDJnLsKLpnUEToub7ETuRu8RCkb40woBZAUiKonXzY= github.com/VictoriaMetrics/fastcache v1.10.0/go.mod h1:tjiYeEfYXCqacuvYw/7UoDIeJaNxq6132xHICNP77w8= github.com/Zilliqa/gozilliqa-sdk v1.2.1-0.20201201074141-dd0ecada1be6/go.mod h1:eSYp2T6f0apnuW8TzhV3f6Aff2SE8Dwio++U4ha4yEM= -github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/aristanetworks/goarista v0.0.0-20170210015632-ea17b1a17847/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/ava-labs/avalanchego v1.7.18 h1:x4PrTKIyUZkxhPjjf/a4hQLZDUB8netA6oFMw6Z1M+Q= -github.com/ava-labs/avalanchego v1.7.18/go.mod h1:Jo21X9sMxvkZNoo8B7GJbbelGrIJbwFPcMhwanink68= -github.com/ava-labs/coreth v0.8.16-rc.2 h1:Yo6J5faqK9sdnp7yzTIIEGodZqPctvy9l4SLRTVTWck= -github.com/ava-labs/coreth v0.8.16-rc.2/go.mod h1:iOB8EcOy/9yY1+/MAUqI2UM5GUIuIbX2MVKK1gCHqHs= +github.com/ava-labs/avalanchego v1.11.2 h1:8iodZ+RjqpRwHdiXPPtvaNt72qravge7voGzw3yPRzg= +github.com/ava-labs/avalanchego v1.11.2/go.mod h1:oTVnF9idL57J4LM/6RByTmKhI4QvV6OCnF99ysyBljE= +github.com/ava-labs/coreth v0.13.1-rc.5 h1:YcTs9nryZLkf4gPmMyFx1TREFpDTPdg/VCNGGHSF2TY= +github.com/ava-labs/coreth v0.13.1-rc.5/go.mod h1:4y1igTe/sFOIrpAtXoY+AdmfftNHrmrhBBRVfGCAPcw= github.com/aws/aws-sdk-go v1.25.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/btcsuite/btcd v0.0.0-20171128150713-2e60448ffcc6/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ= @@ -85,47 +55,63 @@ github.com/btcsuite/btcd v0.0.0-20190315201642-aa6e0f35703c/go.mod h1:DrZx5ec/dm github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.21.0-beta/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= -github.com/btcsuite/btcd v0.23.1 h1:IB8cVQcC2X5mHbnfirLG5IZnkWYNTPlLZVrxUYSotbE= -github.com/btcsuite/btcd v0.23.1/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= -github.com/btcsuite/btcd/btcec/v2 v2.1.1/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= -github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= -github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= +github.com/btcsuite/btcd v0.23.0 h1:V2/ZgjfDFIygAX3ZapeigkVBoVUtOJKSwrhZdlpSvaA= +github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= -github.com/btcsuite/btcd/btcutil v1.1.1 h1:hDcDaXiP0uEzR8Biqo2weECKqEw0uHDZ9ixIWevVQqY= -github.com/btcsuite/btcd/btcutil v1.1.1/go.mod h1:nbKlBMNm9FGsdvKvu0essceubPiAcI57pYBNnsLAa34= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ= +github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= -github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190207003914-4c204d697803/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= -github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= -github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= -github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cloudflare-go v0.10.2-0.20190916151808-a80f83b9add9/go.mod h1:1MxXX1Ux4x6mqPmjkUgTP1CdXIBXKX7T+Jk9Gxrmx+U= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= +github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8= +github.com/cockroachdb/errors v1.9.1/go.mod h1:2sxOtL2WIc096WSZqZ5h8fa17rdDq9HZOZLBCor4mBk= +github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811 h1:ytcWPaNPhNoGMWEhDvS3zToKcDpRsLuRolQJBVGdozk= +github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811/go.mod h1:Nb5lgvnQ2+oGlE/EyZy4+2/CxRh9KfvCXnag1vtpxVM= +github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ= +github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= github.com/coinbase/rosetta-sdk-go v0.6.5 h1:RytFDCPXS64vEYwIOsxsoQGlZZyP9RQvzyYikxymI4w= github.com/coinbase/rosetta-sdk-go v0.6.5/go.mod h1:MvQfsL2KlJ5786OdDviRIJE3agui2YcvS1CaQPDl1Yo= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -133,24 +119,23 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= -github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= -github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= -github.com/decred/dcrd/chaincfg/chainhash v1.0.2 h1:rt5Vlq/jM3ZawwiacWjPa+smINyLRN07EO0cNBV6DGU= -github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= +github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI= +github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0-20200627015759-01fd2de07837 h1:g2cyFTu5FKWhCo7L4hVJ797Q506B4EywA7L9I6OebgA= -github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0-20200627015759-01fd2de07837/go.mod h1:J70FGZSbzsjecRTiTzER+3f1KZLNaXkuv+yeFTKoxM8= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/decred/dcrd/lru v1.1.1 h1:kWFDaW0OWx6AD6Ki342c+JPmHbiVdE6rK81pT3fuo/Y= -github.com/decred/dcrd/lru v1.1.1/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/dgraph-io/badger/v2 v2.2007.2/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE= github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= @@ -159,70 +144,95 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/dop251/goja v0.0.0-20200721192441-a695b0cdd498/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3 h1:+3HCtB74++ClLy8GgjUQYeC8R4ILzVcIe8+5edAJJnE= +github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/ethereum/go-ethereum v1.9.24/go.mod h1:JIfVb6esrqALTExdz9hRYvrP0xBDf6wCncIu1hNwHpM= -github.com/ethereum/go-ethereum v1.10.21 h1:5lqsEx92ZaZzRyOqBEXux4/UR06m296RGzN3ol3teJY= -github.com/ethereum/go-ethereum v1.10.21/go.mod h1:EYFyF19u3ezGLD4RqOkLq+ZCXzYbLoNDdZlMt7kyKFg= +github.com/ethereum/go-ethereum v1.12.0 h1:bdnhLPtqETd4m3mS8BGMNvBTf36bO5bx/hxE2zljOa0= +github.com/ethereum/go-ethereum v1.12.0/go.mod h1:/oo2X/dZLJjf2mJ6YT9wcWxa4nNJDBKDBU6sFIpx1Gs= +github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/color v1.3.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= +github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0= +github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= -github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= +github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -231,46 +241,40 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.2-0.20200707131729-196ae77b8a26/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= +github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= @@ -280,10 +284,11 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/graph-gophers/graphql-go v0.0.0-20191115155744-f33e81362277/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= @@ -291,69 +296,92 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.1.1/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= -github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM= -github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= +github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c h1:DZfsyhDK1hnSS5lH8l+JggqzEleHteTYfutAiVlSUM8= +github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc= github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb v1.2.3-0.20180221223340-01288bdb0883/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY= +github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= +github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= +github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= +github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= +github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= -github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.1.1-0.20170430222011-975b5c4c7c21/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= +github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= +github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= +github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= +github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= +github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= -github.com/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8= -github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= +github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= github.com/lucasjones/reggen v0.0.0-20180717132126-cdb49ff09d77/go.mod h1:5ELEyG+X8f+meRWHuqUOewBOhvHkl7M76pdGEansxW4= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= +github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -366,13 +394,15 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= github.com/neilotoole/errgroup v0.1.5/go.mod h1:Q2nLGf+594h0CLBs/Mbg6qOr7GtqDK7C2S41udRnToE= @@ -386,6 +416,7 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= @@ -397,12 +428,18 @@ github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pborman/uuid v0.0.0-20170112150404-1b00554d8222/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8= +github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -410,51 +447,46 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= -github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI= +github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/prometheus/tsdb v0.6.2-0.20190402121629-4f204dcbc150/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/prometheus/tsdb v0.10.0 h1:If5rVCMTp6W2SiRAQFlbpJNgVlgMEd+U2GZckwK38ic= -github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4= github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= -github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8= -github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521/go.mod h1:RvLn4FgxWubrpZHtQLnOf6EwhN2hEMusxZOhcW9H3UQ= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sanity-io/litter v1.5.1 h1:dwnrSypP6q56o3lFxTU+t2fwQ9A+U5qrXVO4Qg9KwVU= +github.com/sanity-io/litter v1.5.1/go.mod h1:5Z71SvaYy5kcGtyglXOC9rrUi3c1E8CamFWjQsazTh0= +github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil v2.20.5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -466,25 +498,31 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= -github.com/status-im/keycard-go v0.0.0-20200402102358-957c09536969 h1:Oo2KZNP70KE0+IUJSidPj/BFS/RXNHmKIJOdckzml2E= -github.com/status-im/keycard-go v0.0.0-20200402102358-957c09536969/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= +github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= +github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570/go.mod h1:8OR4w3TdeIHIh1g6EMY5p0gVNOovcWC+1vpc7naMuAw= github.com/steakknife/hamming v0.0.0-20180906055917-c99c65617cd3/go.mod h1:hpGUWaI9xL8pRQCTXQgocU38Qw1g0Us7n5PxxTwTCYU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= +github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/thepudds/fzgen v0.4.2 h1:HlEHl5hk2/cqEomf2uK5SA/FeJc12s/vIHmOG+FbACw= +github.com/thepudds/fzgen v0.4.2/go.mod h1:kHCWdsv5tdnt32NIHYDdgq083m6bMtaY0M+ipiO9xWE= github.com/tidwall/gjson v1.6.1/go.mod h1:BaHyNc5bjzYkPqgLq7mdVzeiRtULKULXLgZFKsxEHI0= github.com/tidwall/gjson v1.6.3/go.mod h1:BaHyNc5bjzYkPqgLq7mdVzeiRtULKULXLgZFKsxEHI0= github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= @@ -495,272 +533,221 @@ github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITn github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA= github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= -github.com/tyler-smith/go-bip39 v1.0.2 h1:+t3w+KwLXO6154GNJY+qUtIxLTmFjfUmpguQT1OlOT8= github.com/tyler-smith/go-bip39 v1.0.2/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.17.2-0.20221006022127-8f469abc00aa h1:5SqCsI/2Qya2bCzK15ozrqo2sZxkh0FHynJZOTVoV6Q= +github.com/urfave/cli/v2 v2.17.2-0.20221006022127-8f469abc00aa/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/vmihailenco/msgpack/v5 v5.1.0/go.mod h1:C5gboKD0TJPqWDTVTtrQNfRbiBwHZGo8UTqP/9/XvLI= github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208/go.mod h1:IotVbo4F+mw0EzQ08zFqg7pK3FebNXpaMsRy2RT+Ees= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/ybbus/jsonrpc v2.1.2+incompatible/go.mod h1:XJrh1eMSzdIYFbM08flv0wp5G35eRniyeGut1z+LSiE= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= -go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= +go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0 h1:H2JFgRcGiyHg7H7bwcwaQJYrNFqCqrbTQ8K4p1OvDu8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0/go.mod h1:WfCWp1bGoYK8MeULtI15MmQVczfR+bFkk0DF3h06QmQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY= +go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= +go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= +go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= +go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= +go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= +go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5 h1:rxKZ2gOnYxjfmakvUUqh9Gyb6KXfrj7JWTxORTYqb0E= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220708220712-1185a9018129 h1:vucSRfWwTsoXro7P+3Cjlr6flUMtzCwzlvkxEQtHHB0= -golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4= -golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200824131525-c12d262b63d8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -768,69 +755,27 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= +google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU= +google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk= +google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -839,12 +784,11 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -853,34 +797,28 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/server/mapper/account.go b/server/mapper/account.go index 9e475fd..8b84d47 100644 --- a/server/mapper/account.go +++ b/server/mapper/account.go @@ -2,10 +2,10 @@ package mapper import ( "github.com/coinbase/rosetta-sdk-go/types" - ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common" ) -func Account(address *ethcommon.Address) *types.AccountIdentifier { +func Account(address *common.Address) *types.AccountIdentifier { if address == nil { return nil } diff --git a/server/mapper/amount.go b/server/mapper/amount.go index b28720c..fb6215a 100644 --- a/server/mapper/amount.go +++ b/server/mapper/amount.go @@ -22,6 +22,11 @@ func FlareAmount(value *big.Int) *types.Amount { return Amount(value, FlareCurrency) } +// AtomicAvaxAmount creates a Rosetta Amount representing AVAX amount in nAVAXs with given quantity +func AtomicAvaxAmount(value *big.Int) *types.Amount { + return Amount(value, AtomicAvaxCurrency) +} + func Erc20Amount( bytes []byte, currency *types.Currency, diff --git a/server/mapper/block.go b/server/mapper/block.go index e43dd44..cf8cb47 100644 --- a/server/mapper/block.go +++ b/server/mapper/block.go @@ -1,18 +1,18 @@ package mapper import ( - corethTypes "github.com/ava-labs/coreth/core/types" + "github.com/ava-labs/coreth/core/types" "github.com/ethereum/go-ethereum/common/hexutil" ) // BlockMetadata returns meta data for a block -func BlockMetadata(block *corethTypes.Block) map[string]interface{} { +func BlockMetadata(block *types.Block) map[string]interface{} { meta := map[string]interface{}{ "gas_limit": hexutil.EncodeUint64(block.GasLimit()), "gas_used": hexutil.EncodeUint64(block.GasUsed()), "difficulty": block.Difficulty(), "nonce": block.Nonce(), - "size": hexutil.EncodeUint64(uint64(block.Size())), + "size": hexutil.EncodeUint64(block.Size()), } if block.BaseFee() != nil { meta["base_fee"] = hexutil.EncodeBig(block.BaseFee()) diff --git a/server/mapper/cchainatomictx/helper.go b/server/mapper/cchainatomictx/helper.go new file mode 100644 index 0000000..9338ff9 --- /dev/null +++ b/server/mapper/cchainatomictx/helper.go @@ -0,0 +1,22 @@ +package cchainatomictx + +import ( + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/mapper" +) + +// IsCChainBech32Address checks whether a given account identifier contains a C-chain Bech32 type address +func IsCChainBech32Address(accountIdentifier *types.AccountIdentifier) bool { + if chainID, _, _, err := address.Parse(accountIdentifier.Address); err == nil { + return chainID == constants.CChain.String() + } + return false +} + +// IsAtomicOpType determines whether a given C-chain operation is an atomic one +func IsAtomicOpType(t string) bool { + return t == mapper.OpExport || t == mapper.OpImport +} diff --git a/server/mapper/cchainatomictx/tx_builder.go b/server/mapper/cchainatomictx/tx_builder.go new file mode 100644 index 0000000..05ac7a5 --- /dev/null +++ b/server/mapper/cchainatomictx/tx_builder.go @@ -0,0 +1,181 @@ +package cchainatomictx + +import ( + "errors" + "fmt" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/coreth/plugin/evm" + "github.com/coinbase/rosetta-sdk-go/parser" + "github.com/coinbase/rosetta-sdk-go/types" + "github.com/ethereum/go-ethereum/common" + + "github.com/ava-labs/avalanche-rosetta/mapper" +) + +var errMissingCoinIdentifier = errors.New("input operation does not have coin identifier") + +// BuildTx constructs an evm tx based on the provided operation type, Rosetta matches and metadata +// This method is only used during construction. +func BuildTx(opType string, matches []*parser.Match, metadata Metadata, codec codec.Manager, avaxAssetID ids.ID) (*evm.Tx, []*types.AccountIdentifier, error) { + switch opType { + case mapper.OpExport: + return buildExportTx(matches, metadata, codec, avaxAssetID) + case mapper.OpImport: + return buildImportTx(matches, metadata, codec, avaxAssetID) + default: + return nil, nil, fmt.Errorf("unsupported atomic operation type %s", opType) + } +} + +// [buildExportTx] returns a duly initialized tx if it does not err +func buildExportTx( + matches []*parser.Match, + metadata Metadata, + codec codec.Manager, + avaxAssetID ids.ID, +) (*evm.Tx, []*types.AccountIdentifier, error) { + ins, signers := buildIns(matches, metadata, avaxAssetID) + + exportedOutputs, err := buildExportedOutputs(matches, codec, avaxAssetID) + if err != nil { + return nil, nil, err + } + + tx := &evm.Tx{UnsignedAtomicTx: &evm.UnsignedExportTx{ + NetworkID: metadata.NetworkID, + BlockchainID: metadata.CChainID, + DestinationChain: *metadata.DestinationChainID, + Ins: ins, + ExportedOutputs: exportedOutputs, + }} + return tx, signers, tx.Sign(codec, nil) +} + +// [buildImportTx] returns a duly initialized tx if it does not err +func buildImportTx( + matches []*parser.Match, + metadata Metadata, + codec codec.Manager, + avaxAssetID ids.ID, +) (*evm.Tx, []*types.AccountIdentifier, error) { + importedInputs, signers, err := buildImportedInputs(matches, avaxAssetID) + if err != nil { + return nil, nil, err + } + + outs := buildOuts(matches, avaxAssetID) + + tx := &evm.Tx{UnsignedAtomicTx: &evm.UnsignedImportTx{ + NetworkID: metadata.NetworkID, + BlockchainID: metadata.CChainID, + SourceChain: *metadata.SourceChainID, + ImportedInputs: importedInputs, + Outs: outs, + }} + return tx, signers, tx.Sign(codec, nil) +} + +func buildIns(matches []*parser.Match, metadata Metadata, avaxAssetID ids.ID) ([]evm.EVMInput, []*types.AccountIdentifier) { + inputMatch := matches[0] + + ins := []evm.EVMInput{} + signers := []*types.AccountIdentifier{} + for i, op := range inputMatch.Operations { + ins = append(ins, evm.EVMInput{ + Address: common.HexToAddress(op.Account.Address), + Amount: inputMatch.Amounts[i].Uint64(), + AssetID: avaxAssetID, + Nonce: metadata.Nonce, + }) + signers = append(signers, op.Account) + } + + // we do not use the signers as signing is performed externally to Rosetta + // instead we are using a dummy array with the same length as ins + evmSigners := make([][]*secp256k1.PrivateKey, len(ins)) + evm.SortEVMInputsAndSigners(ins, evmSigners) + + return ins, signers +} + +func buildImportedInputs(matches []*parser.Match, avaxAssetID ids.ID) ([]*avax.TransferableInput, []*types.AccountIdentifier, error) { + inputMatch := matches[0] + + importedInputs := []*avax.TransferableInput{} + signers := []*types.AccountIdentifier{} + for i, op := range inputMatch.Operations { + if op.CoinChange == nil || op.CoinChange.CoinIdentifier == nil { + return nil, nil, errMissingCoinIdentifier + } + utxoID, err := mapper.DecodeUTXOID(op.CoinChange.CoinIdentifier.Identifier) + if err != nil { + return nil, nil, err + } + + importedInputs = append(importedInputs, &avax.TransferableInput{ + UTXOID: *utxoID, + Asset: avax.Asset{ID: avaxAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: inputMatch.Amounts[i].Uint64(), + Input: secp256k1fx.Input{ + SigIndices: []uint32{0}, + }, + }, + }) + signers = append(signers, op.Account) + } + utils.Sort(importedInputs) + + return importedInputs, signers, nil +} + +func buildOuts(matches []*parser.Match, avaxAssetID ids.ID) []evm.EVMOutput { + outputMatch := matches[1] + + outs := []evm.EVMOutput{} + for i, op := range outputMatch.Operations { + outs = append(outs, evm.EVMOutput{ + Address: common.HexToAddress(op.Account.Address), + Amount: outputMatch.Amounts[i].Uint64(), + AssetID: avaxAssetID, + }) + } + utils.Sort(outs) + + return outs +} + +func buildExportedOutputs(matches []*parser.Match, codec codec.Manager, avaxAssetID ids.ID) ([]*avax.TransferableOutput, error) { + outputMatch := matches[1] + + outs := []*avax.TransferableOutput{} + for i, op := range outputMatch.Operations { + destinationAddress, err := address.ParseToID(op.Account.Address) + if err != nil { + return nil, err + } + + outs = append(outs, &avax.TransferableOutput{ + Asset: avax.Asset{ID: avaxAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: outputMatch.Amounts[i].Uint64(), + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{destinationAddress}, + }, + }, + }) + } + + avax.SortTransferableOutputs(outs, codec) + + return outs, nil +} diff --git a/server/mapper/cchainatomictx/tx_parser.go b/server/mapper/cchainatomictx/tx_parser.go new file mode 100644 index 0000000..15ec60b --- /dev/null +++ b/server/mapper/cchainatomictx/tx_parser.go @@ -0,0 +1,199 @@ +package cchainatomictx + +import ( + "errors" + "math/big" + "strconv" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/coreth/plugin/evm" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/ava-labs/avalanche-rosetta/mapper" +) + +var ( + errUnknownDestinationChain = errors.New("unknown destination chain") + errNoMatchingInputAddresses = errors.New("no matching input addresses") +) + +// TxParser parses C-chain atomic transactions and generate corresponding Rosetta operations +type TxParser struct { + // hrp used for address formatting + hrp string + // chainIDs maps chain ids to chain id aliases + chainIDs map[ids.ID]string + // inputTxAccounts contain utxo id to account identifier mappings + inputTxAccounts map[string]*types.AccountIdentifier +} + +// NewTxParser returns a new transaction parser +func NewTxParser(hrp string, chainIDs map[ids.ID]string, inputTxAccounts map[string]*types.AccountIdentifier) *TxParser { + return &TxParser{hrp: hrp, chainIDs: chainIDs, inputTxAccounts: inputTxAccounts} +} + +// Parse converts the given atomic evm tx to corresponding Rosetta operations +// This method is only used during construction. +func (t *TxParser) Parse(tx evm.Tx) ([]*types.Operation, error) { + switch unsignedTx := tx.UnsignedAtomicTx.(type) { + case *evm.UnsignedExportTx: + return t.parseExportTx(unsignedTx) + case *evm.UnsignedImportTx: + return t.parseImportTx(unsignedTx) + default: + return nil, errors.New("unsupported tx type") + } +} + +func (t *TxParser) parseExportTx(exportTx *evm.UnsignedExportTx) ([]*types.Operation, error) { + operations := []*types.Operation{} + ins := insToOperations(0, mapper.OpExport, exportTx.Ins) + + destinationChainID := exportTx.DestinationChain + chainAlias, ok := t.chainIDs[destinationChainID] + if !ok { + return nil, errUnknownDestinationChain + } + + operations = append(operations, ins...) + outs, err := t.exportedOutputsToOperations(len(ins), mapper.OpExport, chainAlias, exportTx.ExportedOutputs) + if err != nil { + return nil, err + } + operations = append(operations, outs...) + + return operations, nil +} + +func (t *TxParser) parseImportTx(importTx *evm.UnsignedImportTx) ([]*types.Operation, error) { + operations := []*types.Operation{} + ins, err := t.importedInToOperations(0, mapper.OpImport, importTx.ImportedInputs) + if err != nil { + return nil, err + } + + operations = append(operations, ins...) + outs := outsToOperations(len(ins), mapper.OpImport, importTx.Outs) + operations = append(operations, outs...) + + return operations, nil +} + +func insToOperations(startIdx int64, opType string, ins []evm.EVMInput) []*types.Operation { + idx := startIdx + operations := []*types.Operation{} + for _, in := range ins { + inputAmount := new(big.Int).SetUint64(in.Amount) + operations = append(operations, &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: idx, + }, + Type: opType, + Account: &types.AccountIdentifier{Address: in.Address.Hex()}, + // Negating input amount + Amount: mapper.AtomicAvaxAmount(new(big.Int).Neg(inputAmount)), + }) + idx++ + } + return operations +} + +func (t *TxParser) importedInToOperations(startIdx int64, opType string, ins []*avax.TransferableInput) ([]*types.Operation, error) { + idx := startIdx + operations := []*types.Operation{} + for _, in := range ins { + inputAmount := new(big.Int).SetUint64(in.In.Amount()) + + utxoID := in.UTXOID.String() + account, ok := t.inputTxAccounts[utxoID] + if !ok { + return nil, errNoMatchingInputAddresses + } + + operations = append(operations, &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: idx, + }, + Type: opType, + Account: account, + // Negating input amount + Amount: mapper.AtomicAvaxAmount(new(big.Int).Neg(inputAmount)), + CoinChange: &types.CoinChange{ + CoinIdentifier: &types.CoinIdentifier{Identifier: utxoID}, + CoinAction: types.CoinSpent, + }, + }) + idx++ + } + return operations, nil +} + +func outsToOperations(startIdx int, opType string, outs []evm.EVMOutput) []*types.Operation { + idx := startIdx + operations := []*types.Operation{} + for _, out := range outs { + operations = append(operations, &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(idx), + }, + Account: &types.AccountIdentifier{Address: out.Address.Hex()}, + RelatedOperations: buildRelatedOperations(startIdx), + Type: opType, + Amount: &types.Amount{ + Value: strconv.FormatUint(out.Amount, 10), + Currency: mapper.AtomicAvaxCurrency, + }, + }) + idx++ + } + return operations +} + +func (t *TxParser) exportedOutputsToOperations( + startIdx int, + opType string, + chainAlias string, + outs []*avax.TransferableOutput, +) ([]*types.Operation, error) { + idx := startIdx + operations := []*types.Operation{} + for _, out := range outs { + var addr string + transferOutput := out.Output().(*secp256k1fx.TransferOutput) + if transferOutput != nil && len(transferOutput.Addrs) > 0 { + var err error + addr, err = address.Format(chainAlias, t.hrp, transferOutput.Addrs[0][:]) + if err != nil { + return nil, err + } + } + + operations = append(operations, &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(idx), + }, + Account: &types.AccountIdentifier{Address: addr}, + RelatedOperations: buildRelatedOperations(startIdx), + Type: opType, + Amount: &types.Amount{ + Value: strconv.FormatUint(out.Out.Amount(), 10), + Currency: mapper.AtomicAvaxCurrency, + }, + }) + idx++ + } + return operations, nil +} + +func buildRelatedOperations(idx int) []*types.OperationIdentifier { + var identifiers []*types.OperationIdentifier + for i := 0; i < idx; i++ { + identifiers = append(identifiers, &types.OperationIdentifier{ + Index: int64(i), + }) + } + return identifiers +} diff --git a/server/mapper/cchainatomictx/types.go b/server/mapper/cchainatomictx/types.go new file mode 100644 index 0000000..c9bf03f --- /dev/null +++ b/server/mapper/cchainatomictx/types.go @@ -0,0 +1,32 @@ +package cchainatomictx + +import ( + "math/big" + + "github.com/ava-labs/avalanchego/ids" +) + +const ( + MetadataAtomicTxGas = "atomic_tx_gas" + MetadataNonce = "nonce" + MetadataSourceChain = "source_chain" +) + +// Metadata contains metadata values returned by /construction/metadata for C-chain atomic transactions +type Metadata struct { + NetworkID uint32 `json:"network_id,omitempty"` + CChainID ids.ID `json:"c_chain_id,omitempty"` + SourceChainID *ids.ID `json:"source_chain_id,omitempty"` + DestinationChain string `json:"destination_chain,omitempty"` + DestinationChainID *ids.ID `json:"destination_chain_id,omitempty"` + Nonce uint64 `json:"nonce"` +} + +// Options contains response values returned by /construction/preprocess for C-chain atomic transactions +type Options struct { + AtomicTxGas *big.Int `json:"atomic_tx_gas"` + From string `json:"from,omitempty"` + SourceChain string `json:"source_chain,omitempty"` + DestinationChain string `json:"destination_chain,omitempty"` + Nonce *big.Int `json:"nonce,omitempty"` +} diff --git a/server/mapper/helper.go b/server/mapper/helper.go index ef8a2e7..0da1f6e 100644 --- a/server/mapper/helper.go +++ b/server/mapper/helper.go @@ -1,6 +1,19 @@ package mapper -import "strings" +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/formatting" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/coinbase/rosetta-sdk-go/types" +) + +var errUnrecognizedNetwork = errors.New("can't recognize network") // EqualFoldContains checks if the array contains the string regardless of casing func EqualFoldContains(arr []string, str string) bool { @@ -11,3 +24,69 @@ func EqualFoldContains(arr []string, str string) bool { } return false } + +// GetHRP fetches hrp for address formatting. +func GetHRP(networkIdentifier *types.NetworkIdentifier) (string, error) { + if IsSupportedHRP(networkIdentifier.Network) { + return networkIdentifier.Network, nil + } + return "", errUnrecognizedNetwork +} + +// UnmarshalJSONMap converts map[string]interface{} into a interface{}. +func UnmarshalJSONMap(m map[string]interface{}, i interface{}) error { + b, err := json.Marshal(m) + if err != nil { + return err + } + + return json.Unmarshal(b, i) +} + +// MarshalJSONMap converts an interface into a map[string]interface{}. +func MarshalJSONMap(i interface{}) (map[string]interface{}, error) { + b, err := json.Marshal(i) + if err != nil { + return nil, err + } + + var m map[string]interface{} + if err := json.Unmarshal(b, &m); err != nil { + return nil, err + } + + return m, nil +} + +// DecodeUTXOID decodes given string into avax.UTXOID +func DecodeUTXOID(s string) (*avax.UTXOID, error) { + split := strings.Split(s, ":") + if len(split) != 2 { + return nil, errors.New("invalid utxo ID format") + } + + txID, err := ids.FromString(split[0]) + if err != nil { + return nil, fmt.Errorf("invalid tx ID: %w", err) + } + + outputIdx, err := strconv.ParseUint(split[1], 10, 32) + if err != nil { + return nil, fmt.Errorf("invalid output index: %w", err) + } + + return &avax.UTXOID{ + TxID: txID, + OutputIndex: uint32(outputIdx), + }, nil +} + +// EncodeBytes encodes given bytes to string +func EncodeBytes(bytes []byte) (string, error) { + return formatting.Encode(formatting.Hex, bytes) +} + +// DecodeToBytes decodes given string into bytes using the same encoding as EncodeBytes +func DecodeToBytes(binaryData string) ([]byte, error) { + return formatting.Decode(formatting.Hex, binaryData) +} diff --git a/server/mapper/pchain/helper.go b/server/mapper/pchain/helper.go new file mode 100644 index 0000000..cf1e574 --- /dev/null +++ b/server/mapper/pchain/helper.go @@ -0,0 +1,33 @@ +package pchain + +import ( + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/coinbase/rosetta-sdk-go/types" +) + +func ParseRosettaTxs( + parserCfg TxParserConfig, + txs []*txs.Tx, + dependencyTxs BlockTxDependencies, +) ([]*types.Transaction, error) { + inputAddresses, err := dependencyTxs.GetReferencedAccounts(parserCfg.Hrp) + if err != nil { + return nil, err + } + + parser, err := NewTxParser(parserCfg, inputAddresses, dependencyTxs) + if err != nil { + return nil, err + } + + transactions := make([]*types.Transaction, 0, len(txs)) + for _, tx := range txs { + t, err := parser.Parse(tx) + if err != nil { + return nil, err + } + + transactions = append(transactions, t) + } + return transactions, nil +} diff --git a/server/mapper/pchain/test_data.go b/server/mapper/pchain/test_data.go new file mode 100644 index 0000000..f453883 --- /dev/null +++ b/server/mapper/pchain/test_data.go @@ -0,0 +1,450 @@ +package pchain + +import ( + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/block" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/coinbase/rosetta-sdk-go/types" +) + +func buildImport() (*txs.Tx, *txs.ImportTx, map[string]*types.AccountIdentifier) { + avaxAssetID, _ := ids.FromString("U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK") + sourceChain, _ := ids.FromString("2JVSBoinj9C2J33VntvzYtVJNZdN2NKiwwKjcumHUWEb5DbBrm") + outAddr1, _ := address.ParseToID("P-fuji1xm0r37l6gyf2mly4pmzc0tz6wnwqkugedh95fk") + outAddr2, _ := address.ParseToID("P-fuji1fmragvegm5k26qzlt6vy0ghhdr508u6r4a5rxj") + outAddr3, _ := address.ParseToID("P-fuji1j3sw805usytrsymfwxxrcwfqguyarumn45cllj") + importAddr, _ := address.ParseToID("C-fuji1xm0r37l6gyf2mly4pmzc0tz6wnwqkugedh95fk") + importedTxID, _ := ids.FromString("2DtYhzCvo9LRYMRJ6sCtYJ4aNPRpsibp46ETNyY6H5Cox1VLvX") + importTx := &txs.ImportTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: uint32(5), + BlockchainID: [32]byte{}, + Outs: []*avax.TransferableOutput{ + { + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + Out: &secp256k1fx.TransferOutput{ + Amt: 8000000, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{outAddr1}, + }, + }, + }, + { // this will be skipped as it is multisig + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + Out: &secp256k1fx.TransferOutput{ + Amt: 8000000, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 2, + Addrs: []ids.ShortID{outAddr1, outAddr2, outAddr3}, + }, + }, + }, + { // this will be skipped as it does not have any addresses + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + Out: &secp256k1fx.TransferOutput{ + Amt: 1000000, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 0, + Addrs: []ids.ShortID{}, + }, + }, + }, + }, + Ins: nil, + Memo: []byte{}, + }, + }, + SourceChain: sourceChain, + ImportedInputs: []*avax.TransferableInput{{ + UTXOID: avax.UTXOID{ + TxID: importedTxID, + OutputIndex: 0, + Symbol: false, + }, + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + In: &secp256k1fx.TransferInput{ + Amt: 9000000, + Input: secp256k1fx.Input{ + SigIndices: []uint32{}, + }, + }, + }}, + } + + signedTx, _ := txs.NewSigned(importTx, block.Codec, nil) + + inputTxAccounts := map[string]*types.AccountIdentifier{} + inputTxAccounts[importTx.ImportedInputs[0].String()] = &types.AccountIdentifier{Address: importAddr.String()} + + return signedTx, importTx, inputTxAccounts +} + +func buildExport() (*txs.Tx, *txs.ExportTx, map[string]*types.AccountIdentifier) { + avaxAssetID, _ := ids.FromString("U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK") + outAddr, _ := address.ParseToID("P-fuji1wmd9dfrqpud6daq0cde47u0r7pkrr46ep60399") + exportOutAddr, _ := address.ParseToID("P-fuji1wmd9dfrqpud6daq0cde47u0r7pkrr46ep60399") + txID, _ := ids.FromString("27LaDkrUrMY1bhVf2i8RARCrRwFjeRw7vEu8ntLQXracgLzL1v") + destinationID, _ := ids.FromString("yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp") + exportTx := &txs.ExportTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: uint32(5), + BlockchainID: [32]byte{}, + Outs: []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + Out: &secp256k1fx.TransferOutput{ + Amt: 2910137500, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{outAddr}, + }, + }, + }}, + Ins: []*avax.TransferableInput{{ + UTXOID: avax.UTXOID{TxID: txID, OutputIndex: 0, Symbol: false}, + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + In: &secp256k1fx.TransferInput{ + Amt: 2921137500, + Input: secp256k1fx.Input{SigIndices: []uint32{}}, + }, + }}, + Memo: []byte{}, + }, + }, + DestinationChain: destinationID, + ExportedOutputs: []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + Out: &secp256k1fx.TransferOutput{ + Amt: 10000000, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{exportOutAddr}, + }, + }, + }}, + } + + signedTx, _ := txs.NewSigned(exportTx, block.Codec, nil) + + inputTxAccounts := map[string]*types.AccountIdentifier{} + inputTxAccounts[exportTx.Ins[0].String()] = &types.AccountIdentifier{Address: outAddr.String()} + + return signedTx, exportTx, inputTxAccounts +} + +// TODO: Remove Post-Durango +func buildAddDelegator() (*txs.Tx, *txs.AddDelegatorTx, map[string]*types.AccountIdentifier) { + avaxAssetID, _ := ids.FromString("U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK") + txID, _ := ids.FromString("2JQGX1MBdszAaeV6eApCZm7CBpc917qWiyQ2cygFRJ6WteDkre") + outAddr, _ := address.ParseToID("P-fuji1gdkq8g208e3j4epyjmx65jglsw7vauh86l47ac") + validatorID, _ := ids.NodeIDFromString("NodeID-BFa1padLXBj7VHa2JYvYGzcTBPQGjPhUy") + stakeAddr, _ := address.ParseToID("P-fuji1l022sue7g2kzvrcuxughl30xkss2cj0az3e5r2") + rewardAddr, _ := address.ParseToID("P-fuji1l022sue7g2kzvrcuxughl30xkss2cj0az3e5r2") + addDelegator := &txs.AddDelegatorTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: uint32(5), + BlockchainID: [32]byte{}, + Outs: []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + Out: &secp256k1fx.TransferOutput{ + Amt: 996649063, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 9, + Threshold: 1, + Addrs: []ids.ShortID{outAddr}, + }, + }, + }}, + Ins: []*avax.TransferableInput{{ + UTXOID: avax.UTXOID{TxID: txID, OutputIndex: 0, Symbol: false}, + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + In: &secp256k1fx.TransferInput{ + Amt: 1996649063, + Input: secp256k1fx.Input{SigIndices: []uint32{}}, + }, + }}, + Memo: []byte{}, + }, + }, + Validator: txs.Validator{ + NodeID: validatorID, + Start: 1656058022, + End: 1657872569, + Wght: 1000000000, + }, + StakeOuts: []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + Out: &secp256k1fx.TransferOutput{ + Amt: 1000000000, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{stakeAddr}, + }, + }, + }}, + DelegationRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{rewardAddr}, + }, + } + + signedTx, _ := txs.NewSigned(addDelegator, block.Codec, nil) + + inputTxAccounts := map[string]*types.AccountIdentifier{} + inputTxAccounts[addDelegator.Ins[0].String()] = &types.AccountIdentifier{Address: stakeAddr.String()} + + return signedTx, addDelegator, inputTxAccounts +} + +// TODO: Remove Post-Durango +func buildValidatorTx() (*txs.Tx, *txs.AddValidatorTx, map[string]*types.AccountIdentifier) { + avaxAssetID, _ := ids.FromString("U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK") + + txID, _ := ids.FromString("88tfp1Pkw9vyKrRtVNiMrghFBrre6Q6CzqPW1t7StDNX9PJEo") + stakeAddr, _ := address.ParseToID("P-fuji1ljdzyey6vu3hgn3cwg4j5lpy0svd6arlxpj6je") + rewardAddr, _ := address.ParseToID("P-fuji1ljdzyey6vu3hgn3cwg4j5lpy0svd6arlxpj6je") + validatorID, _ := ids.NodeIDFromString("NodeID-CCecHmRK3ANe92VyvASxkNav26W4vAVpX") + addValidator := &txs.AddValidatorTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: uint32(5), + BlockchainID: [32]byte{}, + Outs: nil, + Ins: []*avax.TransferableInput{ // two inputs, the second locktimed + { + UTXOID: avax.UTXOID{TxID: txID, OutputIndex: 0}, + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + In: &secp256k1fx.TransferInput{ + Amt: 2000000000, + Input: secp256k1fx.Input{SigIndices: []uint32{1}}, + }, + }, + { + UTXOID: avax.UTXOID{TxID: txID, OutputIndex: 1}, + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + In: &stakeable.LockIn{ + Locktime: uint64(1666781236), // a unix time + TransferableIn: &secp256k1fx.TransferInput{ + Amt: 2000000000, + Input: secp256k1fx.Input{SigIndices: []uint32{1}}, + }, + }, + }, + }, + Memo: []byte{}, + }, + }, + Validator: txs.Validator{ + NodeID: validatorID, + Start: 1656084079, + End: 1687620079, + Wght: 2000000000, + }, + StakeOuts: []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + Out: &secp256k1fx.TransferOutput{ + Amt: 2000000000, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{stakeAddr}, + }, + }, + }}, + RewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{rewardAddr}, + }, + DelegationShares: 20000, + } + + signedTx, _ := txs.NewSigned(addValidator, block.Codec, nil) + + inputTxAccounts := map[string]*types.AccountIdentifier{ + addValidator.Ins[0].String(): {Address: stakeAddr.String()}, + addValidator.Ins[1].String(): {Address: stakeAddr.String()}, + } + + return signedTx, addValidator, inputTxAccounts +} + +func buildAddPermissionlessDelegator() (*txs.Tx, *txs.AddPermissionlessDelegatorTx, map[string]*types.AccountIdentifier) { + avaxAssetID, _ := ids.FromString("U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK") + txID, _ := ids.FromString("2JQGX1MBdszAaeV6eApCZm7CBpc917qWiyQ2cygFRJ6WteDkre") + outAddr, _ := address.ParseToID("P-fuji1gdkq8g208e3j4epyjmx65jglsw7vauh86l47ac") + validatorID, _ := ids.NodeIDFromString("NodeID-BFa1padLXBj7VHa2JYvYGzcTBPQGjPhUy") + stakeAddr, _ := address.ParseToID("P-fuji1l022sue7g2kzvrcuxughl30xkss2cj0az3e5r2") + rewardAddr, _ := address.ParseToID("P-fuji1l022sue7g2kzvrcuxughl30xkss2cj0az3e5r2") + addPermissionlessDelegator := &txs.AddPermissionlessDelegatorTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: uint32(5), + BlockchainID: [32]byte{}, + Outs: []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + Out: &secp256k1fx.TransferOutput{ + Amt: 996649063, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 9, + Threshold: 1, + Addrs: []ids.ShortID{outAddr}, + }, + }, + }}, + Ins: []*avax.TransferableInput{{ + UTXOID: avax.UTXOID{TxID: txID, OutputIndex: 0, Symbol: false}, + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + In: &secp256k1fx.TransferInput{ + Amt: 1996649063, + Input: secp256k1fx.Input{SigIndices: []uint32{}}, + }, + }}, + Memo: []byte{}, + }, + }, + Validator: txs.Validator{ + NodeID: validatorID, + Start: 1656058022, + End: 1657872569, + Wght: 1000000000, + }, + Subnet: constants.PrimaryNetworkID, + StakeOuts: []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + Out: &secp256k1fx.TransferOutput{ + Amt: 1000000000, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{stakeAddr}, + }, + }, + }}, + DelegationRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{rewardAddr}, + }, + } + + signedTx, _ := txs.NewSigned(addPermissionlessDelegator, block.Codec, nil) + + inputTxAccounts := map[string]*types.AccountIdentifier{} + inputTxAccounts[addPermissionlessDelegator.Ins[0].String()] = &types.AccountIdentifier{Address: stakeAddr.String()} + + return signedTx, addPermissionlessDelegator, inputTxAccounts +} + +func buildAddPermissionlessValidator() (*txs.Tx, *txs.AddPermissionlessValidatorTx, map[string]*types.AccountIdentifier) { + avaxAssetID, _ := ids.FromString("U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK") + txID, _ := ids.FromString("88tfp1Pkw9vyKrRtVNiMrghFBrre6Q6CzqPW1t7StDNX9PJEo") + stakeAddr, _ := address.ParseToID("P-fuji1ljdzyey6vu3hgn3cwg4j5lpy0svd6arlxpj6je") + rewardAddr, _ := address.ParseToID("P-fuji1ljdzyey6vu3hgn3cwg4j5lpy0svd6arlxpj6je") + validatorID, _ := ids.NodeIDFromString("NodeID-CCecHmRK3ANe92VyvASxkNav26W4vAVpX") + addPermissionlessValidator := &txs.AddPermissionlessValidatorTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: uint32(5), + BlockchainID: [32]byte{}, + Outs: nil, + Ins: []*avax.TransferableInput{ // two inputs, the second locktimed + { + UTXOID: avax.UTXOID{TxID: txID, OutputIndex: 0}, + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + In: &secp256k1fx.TransferInput{ + Amt: 2000000000, + Input: secp256k1fx.Input{SigIndices: []uint32{1}}, + }, + }, + { + UTXOID: avax.UTXOID{TxID: txID, OutputIndex: 1}, + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + In: &stakeable.LockIn{ + Locktime: uint64(1666781236), // a unix time + TransferableIn: &secp256k1fx.TransferInput{ + Amt: 2000000000, + Input: secp256k1fx.Input{SigIndices: []uint32{1}}, + }, + }, + }, + }, + Memo: []byte{}, + }, + }, + Validator: txs.Validator{ + NodeID: validatorID, + Start: 1656084079, + End: 1687620079, + Wght: 2000000000, + }, + Subnet: constants.PrimaryNetworkID, + Signer: &signer.Empty{}, + StakeOuts: []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + Out: &secp256k1fx.TransferOutput{ + Amt: 2000000000, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{stakeAddr}, + }, + }, + }}, + ValidatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{rewardAddr}, + }, + DelegatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{rewardAddr}, + }, + DelegationShares: 20000, + } + + signedTx, _ := txs.NewSigned(addPermissionlessValidator, block.Codec, nil) + + inputTxAccounts := map[string]*types.AccountIdentifier{ + addPermissionlessValidator.Ins[0].String(): {Address: stakeAddr.String()}, + addPermissionlessValidator.Ins[1].String(): {Address: stakeAddr.String()}, + } + + return signedTx, addPermissionlessValidator, inputTxAccounts +} diff --git a/server/mapper/pchain/tx_builder.go b/server/mapper/pchain/tx_builder.go new file mode 100644 index 0000000..2a60859 --- /dev/null +++ b/server/mapper/pchain/tx_builder.go @@ -0,0 +1,589 @@ +package pchain + +import ( + "errors" + "fmt" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/formatting" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/coinbase/rosetta-sdk-go/parser" + "github.com/coinbase/rosetta-sdk-go/types" + "github.com/ethereum/go-ethereum/common/math" + + "github.com/ava-labs/avalanche-rosetta/mapper" +) + +var ( + errInvalidMetadata = errors.New("invalid metadata") + errOutputAmountOverflow = errors.New("sum of output amounts caused overflow") +) + +// BuildTx constructs a P-chain Tx based on the provided operation type, Rosetta matches and metadata +// This method is only used during construction. +func BuildTx( + opType string, + matches []*parser.Match, + payloadMetadata Metadata, + codec codec.Manager, + avaxAssetID ids.ID, +) (*txs.Tx, []*types.AccountIdentifier, error) { + switch opType { + case OpImportAvax: + return buildImportTx(matches, payloadMetadata, codec, avaxAssetID) + case OpExportAvax: + return buildExportTx(matches, payloadMetadata, codec, avaxAssetID) + case OpAddPermissionlessValidator: + return buildAddPermissionlessValidatorTx(matches, payloadMetadata, codec, avaxAssetID) + case OpAddPermissionlessDelegator: + return buildAddPermissionlessDelegatorTx(matches, payloadMetadata, codec, avaxAssetID) + case OpAddValidator: + // TODO: Remove Post-Durango + return buildAddValidatorTx(matches, payloadMetadata, codec, avaxAssetID) + case OpAddDelegator: + // TODO: Remove Post-Durango + return buildAddDelegatorTx(matches, payloadMetadata, codec, avaxAssetID) + default: + return nil, nil, fmt.Errorf("invalid tx type: %s", opType) + } +} + +// [buildImportTx] returns a duly initialized tx if it does not err +func buildImportTx( + matches []*parser.Match, + metadata Metadata, + codec codec.Manager, + avaxAssetID ids.ID, +) (*txs.Tx, []*types.AccountIdentifier, error) { + blockchainID := metadata.BlockchainID + sourceChainID := metadata.SourceChainID + + ins, imported, signers, err := buildInputs(matches[0].Operations, avaxAssetID) + if err != nil { + return nil, nil, fmt.Errorf("parse inputs failed: %w", err) + } + + outs, _, _, err := buildOutputs(matches[1].Operations, codec, avaxAssetID) + if err != nil { + return nil, nil, fmt.Errorf("parse outputs failed: %w", err) + } + + tx := &txs.Tx{Unsigned: &txs.ImportTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: metadata.NetworkID, + BlockchainID: blockchainID, + Outs: outs, + Ins: ins, + }}, + ImportedInputs: imported, + SourceChain: sourceChainID, + }} + + return tx, signers, tx.Sign(codec, nil) +} + +// [buildExportTx] returns a duly initialized tx if it does not err +func buildExportTx( + matches []*parser.Match, + metadata Metadata, + codec codec.Manager, + avaxAssetID ids.ID, +) (*txs.Tx, []*types.AccountIdentifier, error) { + if metadata.ExportMetadata == nil { + return nil, nil, errInvalidMetadata + } + blockchainID := metadata.BlockchainID + destinationChainID := metadata.DestinationChainID + + ins, _, signers, err := buildInputs(matches[0].Operations, avaxAssetID) + if err != nil { + return nil, nil, fmt.Errorf("parse inputs failed: %w", err) + } + + outs, _, exported, err := buildOutputs(matches[1].Operations, codec, avaxAssetID) + if err != nil { + return nil, nil, fmt.Errorf("parse outputs failed: %w", err) + } + + tx := &txs.Tx{Unsigned: &txs.ExportTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: metadata.NetworkID, + BlockchainID: blockchainID, + Outs: outs, + Ins: ins, + }}, + DestinationChain: destinationChainID, + ExportedOutputs: exported, + }} + + return tx, signers, tx.Sign(codec, nil) +} + +// TODO: Remove Post-Durango +// [buildAddValidatorTx] returns a duly initialized tx if it does not err +func buildAddValidatorTx( + matches []*parser.Match, + metadata Metadata, + codec codec.Manager, + avaxAssetID ids.ID, +) (*txs.Tx, []*types.AccountIdentifier, error) { + if metadata.StakingMetadata == nil { + return nil, nil, errInvalidMetadata + } + + blockchainID := metadata.BlockchainID + + nodeID, err := ids.NodeIDFromString(metadata.NodeID) + if err != nil { + return nil, nil, err + } + + rewardsOwner, err := buildOutputOwner( + metadata.ValidationRewardsOwners, + metadata.Locktime, + metadata.Threshold, + ) + if err != nil { + return nil, nil, err + } + + ins, _, signers, err := buildInputs(matches[0].Operations, avaxAssetID) + if err != nil { + return nil, nil, fmt.Errorf("parse inputs failed: %w", err) + } + + outs, stakeOutputs, _, err := buildOutputs(matches[1].Operations, codec, avaxAssetID) + if err != nil { + return nil, nil, fmt.Errorf("parse outputs failed: %w", err) + } + + weight, err := sumOutputAmounts(stakeOutputs) + if err != nil { + return nil, nil, err + } + + tx := &txs.Tx{Unsigned: &txs.AddValidatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: metadata.NetworkID, + BlockchainID: blockchainID, + Outs: outs, + Ins: ins, + }}, + Validator: txs.Validator{ + NodeID: nodeID, + Start: metadata.Start, + End: metadata.End, + Wght: weight, + }, + StakeOuts: stakeOutputs, + RewardsOwner: rewardsOwner, + DelegationShares: metadata.Shares, + }} + + return tx, signers, tx.Sign(codec, nil) +} + +// TODO: Remove Post-Durango +// [buildAddDelegatorTx] returns a duly initialized tx if it does not err +func buildAddDelegatorTx( + matches []*parser.Match, + metadata Metadata, + codec codec.Manager, + avaxAssetID ids.ID, +) (*txs.Tx, []*types.AccountIdentifier, error) { + if metadata.StakingMetadata == nil { + return nil, nil, errInvalidMetadata + } + + blockchainID := metadata.BlockchainID + + nodeID, err := ids.NodeIDFromString(metadata.NodeID) + if err != nil { + return nil, nil, err + } + + rewardsOwner, err := buildOutputOwner( + metadata.ValidationRewardsOwners, + metadata.Locktime, + metadata.Threshold, + ) + if err != nil { + return nil, nil, err + } + + ins, _, signers, err := buildInputs(matches[0].Operations, avaxAssetID) + if err != nil { + return nil, nil, fmt.Errorf("parse inputs failed: %w", err) + } + + outs, stakeOutputs, _, err := buildOutputs(matches[1].Operations, codec, avaxAssetID) + if err != nil { + return nil, nil, fmt.Errorf("parse outputs failed: %w", err) + } + + weight, err := sumOutputAmounts(stakeOutputs) + if err != nil { + return nil, nil, err + } + + tx := &txs.Tx{Unsigned: &txs.AddDelegatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: metadata.NetworkID, + BlockchainID: blockchainID, + Outs: outs, + Ins: ins, + }}, + Validator: txs.Validator{ + NodeID: nodeID, + Start: metadata.Start, + End: metadata.End, + Wght: weight, + }, + StakeOuts: stakeOutputs, + DelegationRewardsOwner: rewardsOwner, + }} + + return tx, signers, tx.Sign(codec, nil) +} + +// [buildAddPermissionlessValidatorTx] returns a duly initialized tx if it does not err +func buildAddPermissionlessValidatorTx( + matches []*parser.Match, + metadata Metadata, + codec codec.Manager, + avaxAssetID ids.ID, +) (*txs.Tx, []*types.AccountIdentifier, error) { + if metadata.StakingMetadata == nil { + return nil, nil, errInvalidMetadata + } + + blockchainID := metadata.BlockchainID + + nodeID, err := ids.NodeIDFromString(metadata.NodeID) + if err != nil { + return nil, nil, err + } + + subnetID := constants.PrimaryNetworkID + if metadata.Subnet != "" { + subnetID, err = ids.FromString(metadata.Subnet) + if err != nil { + return nil, nil, fmt.Errorf("%w: invalid subnet id", errInvalidMetadata) + } + } + + publicKeyBytes, err := formatting.Decode(formatting.HexNC, metadata.BLSPublicKey) + if err != nil { + return nil, nil, err + } + popBytes, err := formatting.Decode(formatting.HexNC, metadata.BLSProofOfPossession) + if err != nil { + return nil, nil, err + } + pop := &signer.ProofOfPossession{} + copy(pop.PublicKey[:], publicKeyBytes) + copy(pop.ProofOfPossession[:], popBytes) + if err = pop.Verify(); err != nil { + return nil, nil, err + } + + validationRewardsOwner, err := buildOutputOwner( + metadata.ValidationRewardsOwners, + metadata.Locktime, + metadata.Threshold, + ) + if err != nil { + return nil, nil, err + } + + var delegationRewardsOwner *secp256k1fx.OutputOwners + if len(metadata.DelegationRewardsOwners) == 0 { + delegationRewardsOwner = validationRewardsOwner + } else { + delegationRewardsOwner, err = buildOutputOwner( + metadata.DelegationRewardsOwners, + metadata.Locktime, + metadata.Threshold, + ) + if err != nil { + return nil, nil, err + } + } + + ins, _, signers, err := buildInputs(matches[0].Operations, avaxAssetID) + if err != nil { + return nil, nil, fmt.Errorf("parse inputs failed: %w", err) + } + + outs, stakeOutputs, _, err := buildOutputs(matches[1].Operations, codec, avaxAssetID) + if err != nil { + return nil, nil, fmt.Errorf("parse outputs failed: %w", err) + } + + weight, err := sumOutputAmounts(stakeOutputs) + if err != nil { + return nil, nil, err + } + + tx := &txs.Tx{Unsigned: &txs.AddPermissionlessValidatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: metadata.NetworkID, + BlockchainID: blockchainID, + Outs: outs, + Ins: ins, + }}, + Validator: txs.Validator{ + NodeID: nodeID, + Start: metadata.Start, + End: metadata.End, + Wght: weight, + }, + Subnet: subnetID, + Signer: pop, + StakeOuts: stakeOutputs, + ValidatorRewardsOwner: validationRewardsOwner, + DelegatorRewardsOwner: delegationRewardsOwner, + DelegationShares: metadata.Shares, + }} + + return tx, signers, tx.Sign(codec, nil) +} + +// [buildAddPermissionlessDelegatorTx] returns a duly initialized tx if it does not err +func buildAddPermissionlessDelegatorTx( + matches []*parser.Match, + metadata Metadata, + codec codec.Manager, + avaxAssetID ids.ID, +) (*txs.Tx, []*types.AccountIdentifier, error) { + if metadata.StakingMetadata == nil { + return nil, nil, errInvalidMetadata + } + + blockchainID := metadata.BlockchainID + + nodeID, err := ids.NodeIDFromString(metadata.NodeID) + if err != nil { + return nil, nil, err + } + rewardsOwner, err := buildOutputOwner(metadata.ValidationRewardsOwners, metadata.Locktime, metadata.Threshold) + if err != nil { + return nil, nil, err + } + + ins, _, signers, err := buildInputs(matches[0].Operations, avaxAssetID) + if err != nil { + return nil, nil, fmt.Errorf("parse inputs failed: %w", err) + } + + outs, stakeOutputs, _, err := buildOutputs(matches[1].Operations, codec, avaxAssetID) + if err != nil { + return nil, nil, fmt.Errorf("parse outputs failed: %w", err) + } + + weight, err := sumOutputAmounts(stakeOutputs) + if err != nil { + return nil, nil, err + } + + subnetID := constants.PrimaryNetworkID + if metadata.Subnet != "" { + subnetID, err = ids.FromString(metadata.Subnet) + if err != nil { + return nil, nil, fmt.Errorf("%w: invalid subnet id", errInvalidMetadata) + } + } + + tx := &txs.Tx{Unsigned: &txs.AddPermissionlessDelegatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: metadata.NetworkID, + BlockchainID: blockchainID, + Outs: outs, + Ins: ins, + }}, + Validator: txs.Validator{ + NodeID: nodeID, + Start: metadata.Start, + End: metadata.End, + Wght: weight, + }, + Subnet: subnetID, + StakeOuts: stakeOutputs, + DelegationRewardsOwner: rewardsOwner, + }} + + return tx, signers, tx.Sign(codec, nil) +} + +func buildOutputOwner( + addrs []string, + locktime uint64, + threshold uint32, +) (*secp256k1fx.OutputOwners, error) { + rewardAddrs := make([]ids.ShortID, len(addrs)) + for i, addr := range addrs { + addrID, err := address.ParseToID(addr) + if err != nil { + return nil, err + } + rewardAddrs[i] = addrID + } + utils.Sort(rewardAddrs) + + return &secp256k1fx.OutputOwners{ + Locktime: locktime, + Threshold: threshold, + Addrs: rewardAddrs, + }, nil +} + +func buildInputs( + operations []*types.Operation, + avaxAssetID ids.ID, +) ( + ins []*avax.TransferableInput, + imported []*avax.TransferableInput, + signers []*types.AccountIdentifier, + err error, +) { + for _, op := range operations { + utxoID, err := mapper.DecodeUTXOID(op.CoinChange.CoinIdentifier.Identifier) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to decode UTXO ID: %w", err) + } + + opMetadata, err := ParseOpMetadata(op.Metadata) + if err != nil { + return nil, nil, nil, fmt.Errorf("parse input operation Metadata failed: %w", err) + } + + val, err := types.AmountValue(op.Amount) + if err != nil { + return nil, nil, nil, fmt.Errorf("parse operation amount failed: %w", err) + } + + in := &avax.TransferableInput{ + UTXOID: *utxoID, + Asset: avax.Asset{ID: avaxAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: val.Uint64(), + Input: secp256k1fx.Input{ + SigIndices: opMetadata.SigIndices, + }, + }, + } + + switch opMetadata.Type { + case OpTypeImport: + imported = append(imported, in) + case OpTypeInput: + ins = append(ins, in) + default: + return nil, nil, nil, fmt.Errorf("invalid option type: %s", op.Type) + } + signers = append(signers, op.Account) + } + + utils.Sort(ins) + utils.Sort(imported) + + return ins, imported, signers, nil +} + +// ParseOpMetadata creates an OperationMetadata from given generic metadata map +func ParseOpMetadata(metadata map[string]interface{}) (*OperationMetadata, error) { + var operationMetadata OperationMetadata + if err := mapper.UnmarshalJSONMap(metadata, &operationMetadata); err != nil { + return nil, err + } + + // set threshold default to 1 + if operationMetadata.Threshold == 0 { + operationMetadata.Threshold = 1 + } + + // set sig indices to a single signer if not provided + if operationMetadata.SigIndices == nil { + operationMetadata.SigIndices = []uint32{0} + } + + return &operationMetadata, nil +} + +func buildOutputs( + operations []*types.Operation, + codec codec.Manager, + avaxAssetID ids.ID, +) ( + outs []*avax.TransferableOutput, + stakeOutputs []*avax.TransferableOutput, + exported []*avax.TransferableOutput, + err error, +) { + for _, op := range operations { + opMetadata, err := ParseOpMetadata(op.Metadata) + if err != nil { + return nil, nil, nil, fmt.Errorf("parse output operation Metadata failed: %w", err) + } + + addrID, err := address.ParseToID(op.Account.Address) + if err != nil { + return nil, nil, nil, fmt.Errorf("parse output address failed: %w", err) + } + + outputOwners := &secp256k1fx.OutputOwners{ + Addrs: []ids.ShortID{addrID}, + Locktime: opMetadata.Locktime, + Threshold: opMetadata.Threshold, + } + + val, err := types.AmountValue(op.Amount) + if err != nil { + return nil, nil, nil, fmt.Errorf("parse operation amount failed: %w", err) + } + + out := &avax.TransferableOutput{ + Asset: avax.Asset{ID: avaxAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: val.Uint64(), + OutputOwners: *outputOwners, + }, + } + + switch opMetadata.Type { + case OpTypeOutput: + outs = append(outs, out) + case OpTypeStakeOutput: + stakeOutputs = append(stakeOutputs, out) + case OpTypeExport: + exported = append(exported, out) + default: + return nil, nil, nil, fmt.Errorf("invalid option type: %s", op.Type) + } + } + + avax.SortTransferableOutputs(outs, codec) + avax.SortTransferableOutputs(stakeOutputs, codec) + avax.SortTransferableOutputs(exported, codec) + + return outs, stakeOutputs, exported, nil +} + +func sumOutputAmounts(stakeOutputs []*avax.TransferableOutput) (uint64, error) { + var stakeOutputAmountSum uint64 + for _, out := range stakeOutputs { + outAmount := out.Output().Amount() + if outAmount > math.MaxUint64-stakeOutputAmountSum { + return 0, errOutputAmountOverflow + } + stakeOutputAmountSum += outAmount + } + return stakeOutputAmountSum, nil +} diff --git a/server/mapper/pchain/tx_dependency.go b/server/mapper/pchain/tx_dependency.go new file mode 100644 index 0000000..be7e23f --- /dev/null +++ b/server/mapper/pchain/tx_dependency.go @@ -0,0 +1,192 @@ +package pchain + +import ( + "fmt" + "log" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/ava-labs/avalanche-rosetta/constants" +) + +type BlockTxDependencies map[ids.ID]*SingleTxDependency + +// GetTxDependenciesIDs generates the list of transaction ids used in the inputs to given unsigned transaction +// this list is then used to fetch the dependency transactions in order to extract source addresses +// as this information is not part of the transaction objects on chain. +func GetTxDependenciesIDs(tx txs.UnsignedTx) ([]ids.ID, error) { + // collect tx inputs + // TODO: Move to using [txs.Visitor] from AvalancheGo + // Ref: https://github.com/ava-labs/avalanchego/blob/master/vms/platformvm/txs/visitor.go + var ins []*avax.TransferableInput + switch unsignedTx := tx.(type) { + case *txs.AddValidatorTx: + ins = unsignedTx.Ins + case *txs.AddSubnetValidatorTx: + ins = unsignedTx.Ins + case *txs.AddDelegatorTx: + ins = unsignedTx.Ins + case *txs.CreateChainTx: + ins = unsignedTx.Ins + case *txs.CreateSubnetTx: + ins = unsignedTx.Ins + case *txs.ImportTx: + ins = unsignedTx.Ins + case *txs.ExportTx: + ins = unsignedTx.Ins + case *txs.AdvanceTimeTx: + return []ids.ID{}, nil + case *txs.RewardValidatorTx: + return []ids.ID{unsignedTx.TxID}, nil + case *txs.RemoveSubnetValidatorTx: + ins = unsignedTx.Ins + case *txs.TransformSubnetTx: + ins = unsignedTx.Ins + case *txs.AddPermissionlessValidatorTx: + ins = unsignedTx.Ins + case *txs.AddPermissionlessDelegatorTx: + ins = unsignedTx.Ins + case *txs.TransferSubnetOwnershipTx: + ins = unsignedTx.Ins + case *txs.BaseTx: + ins = unsignedTx.Ins + default: + return nil, fmt.Errorf("unknown tx type %T", unsignedTx) + } + + // extract txIDs and filter out duplicates + txIDs := make(map[ids.ID]ids.ID) + for _, in := range ins { + txIDs[in.UTXOID.TxID] = in.UTXOID.TxID + } + uniqueTxIDs := make([]ids.ID, 0, len(txIDs)) + for _, txnID := range txIDs { + uniqueTxIDs = append(uniqueTxIDs, txnID) + } + utils.Sort(uniqueTxIDs) + + return uniqueTxIDs, nil +} + +// GetReferencedAccounts extracts destination accounts from given dependency transactions +func (bd BlockTxDependencies) GetReferencedAccounts(hrp string) (map[string]*types.AccountIdentifier, error) { + addresses := make(map[string]*types.AccountIdentifier) + for _, dependencyTx := range bd { + utxoMap := dependencyTx.GetUtxos() + + for _, utxo := range utxoMap { + addressable, ok := utxo.Out.(avax.Addressable) + if !ok { + return nil, errFailedToGetUTXOAddresses + } + + addrs := addressable.Addresses() + + if len(addrs) != 1 { + continue + } + + addr, err := address.Format(constants.PChain.String(), hrp, addrs[0]) + addresses[utxo.UTXOID.String()] = &types.AccountIdentifier{Address: addr} + if err != nil { + return nil, err + } + } + } + + return addresses, nil +} + +// SingleTxDependency represents a single dependency of a give transaction +type SingleTxDependency struct { + // [Tx] has some of its outputs spent as + // input from a tx dependent on it + Tx *txs.Tx + + // Staker txs are rewarded at the end of staking period + // with some utxos appended to staker txs' ones. + // [RewardUTXOs] collects those reward utxos + RewardUTXOs []*avax.UTXO + + // [utxosMap] caches mapping of Tx utxoID --> Tx utxo + // for both Tx and RewardUTXOs + utxosMap map[avax.UTXOID]*avax.UTXO +} + +func (d *SingleTxDependency) GetUtxos() map[avax.UTXOID]*avax.UTXO { + if d.utxosMap != nil { + return d.utxosMap + } + d.utxosMap = make(map[avax.UTXOID]*avax.UTXO) + + // Add reward UTXOs + for _, utxo := range d.RewardUTXOs { + d.utxosMap[utxo.UTXOID] = utxo + } + + if d.Tx != nil { + // Generate UTXOs from outputs + // TODO: Move to using [txs.Visitor] from AvalancheGo + // Ref: https://github.com/ava-labs/avalanchego/blob/master/vms/platformvm/txs/visitor.go + outsToAdd := make([]*avax.TransferableOutput, 0) + switch unsignedTx := d.Tx.Unsigned.(type) { + case *txs.AddValidatorTx: + outsToAdd = append(outsToAdd, unsignedTx.Outputs()...) + outsToAdd = append(outsToAdd, unsignedTx.Stake()...) + case *txs.AddSubnetValidatorTx: + outsToAdd = append(outsToAdd, unsignedTx.Outputs()...) + case *txs.AddDelegatorTx: + outsToAdd = append(outsToAdd, unsignedTx.Outputs()...) + outsToAdd = append(outsToAdd, unsignedTx.Stake()...) + case *txs.CreateChainTx: + outsToAdd = append(outsToAdd, unsignedTx.Outputs()...) + case *txs.CreateSubnetTx: + outsToAdd = append(outsToAdd, unsignedTx.Outputs()...) + case *txs.ImportTx: + outsToAdd = append(outsToAdd, unsignedTx.Outputs()...) + case *txs.ExportTx: + outsToAdd = append(outsToAdd, unsignedTx.Outputs()...) + case *txs.AdvanceTimeTx: + // No outputs to add + case *txs.RewardValidatorTx: + // No outputs to add + case *txs.RemoveSubnetValidatorTx: + outsToAdd = append(outsToAdd, unsignedTx.Outputs()...) + case *txs.TransformSubnetTx: + outsToAdd = append(outsToAdd, unsignedTx.Outputs()...) + case *txs.AddPermissionlessValidatorTx: + outsToAdd = append(outsToAdd, unsignedTx.Outputs()...) + outsToAdd = append(outsToAdd, unsignedTx.Stake()...) + case *txs.AddPermissionlessDelegatorTx: + outsToAdd = append(outsToAdd, unsignedTx.Outputs()...) + outsToAdd = append(outsToAdd, unsignedTx.Stake()...) + case *txs.TransferSubnetOwnershipTx: + outsToAdd = append(outsToAdd, unsignedTx.Outputs()...) + case *txs.BaseTx: + outsToAdd = append(outsToAdd, unsignedTx.Outputs()...) + default: + log.Printf("unknown type %T", unsignedTx) + } + + // add collected utxos + txID := d.Tx.ID() + for i, out := range outsToAdd { + utxoID := avax.UTXOID{ + TxID: txID, + OutputIndex: uint32(i), + } + d.utxosMap[utxoID] = &avax.UTXO{ + UTXOID: utxoID, + Asset: out.Asset, + Out: out.Out, + } + } + } + + return d.utxosMap +} diff --git a/server/mapper/pchain/tx_dependency_test.go b/server/mapper/pchain/tx_dependency_test.go new file mode 100644 index 0000000..a67c203 --- /dev/null +++ b/server/mapper/pchain/tx_dependency_test.go @@ -0,0 +1,331 @@ +package pchain + +import ( + "testing" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/reward" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/stretchr/testify/require" +) + +var preFundedKeys = secp256k1.TestKeys() + +func TestTxDependencyIsCreateChain(t *testing.T) { + require := require.New(t) + + in := &avax.TransferableInput{ + UTXOID: avax.UTXOID{ + TxID: ids.ID{'t', 'x', 'I', 'D'}, + OutputIndex: 2, + }, + Asset: avax.Asset{ID: ids.ID{'a', 's', 's', 'e', 'r', 't'}}, + In: &secp256k1fx.TransferInput{ + Amt: uint64(5678), + Input: secp256k1fx.Input{SigIndices: []uint32{0}}, + }, + } + + // simple output + out := &avax.TransferableOutput{ + Asset: avax.Asset{ID: ids.ID{'a', 's', 's', 'e', 't', '1'}}, + Out: &secp256k1fx.TransferOutput{ + Amt: uint64(1234), + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + }, + } + + // multisign output + multiSignOut := &avax.TransferableOutput{ + Asset: avax.Asset{ID: ids.ID{'a', 's', 's', 'e', 't', '2'}}, + Out: &secp256k1fx.TransferOutput{ + Amt: uint64(5678), + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + preFundedKeys[1].PublicKey().Address(), + preFundedKeys[2].PublicKey().Address(), + }, + }, + }, + } + + // create a non-reward validator tx + utx := &txs.CreateChainTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: 10, + BlockchainID: ids.ID{'c', 'h', 'a', 'i', 'n', 'I', 'D'}, + Ins: []*avax.TransferableInput{in}, + Outs: []*avax.TransferableOutput{out, multiSignOut}, + Memo: []byte{1, 2, 3, 4, 5, 6, 7, 8}, + }}, + SubnetID: ids.ID{'s', 'u', 'b', 'n', 'e', 't', 'I', 'D'}, + ChainName: "a chain", + VMID: ids.GenerateTestID(), + FxIDs: []ids.ID{ids.GenerateTestID()}, + GenesisData: []byte{'g', 'e', 'n', 'D', 'a', 't', 'a'}, + SubnetAuth: &secp256k1fx.Input{SigIndices: []uint32{1}}, + } + tx, err := txs.NewSigned(utx, txs.Codec, nil) + require.NoError(err) + + dep := &SingleTxDependency{Tx: tx} + res := dep.GetUtxos() + require.Len(res, 2) + + expectedUTXOs := []*avax.UTXO{ + { + UTXOID: avax.UTXOID{ + TxID: tx.ID(), + OutputIndex: 0, + }, + Asset: out.Asset, + Out: out.Out, + }, + { + UTXOID: avax.UTXOID{ + TxID: tx.ID(), + OutputIndex: 1, + }, + Asset: multiSignOut.Asset, + Out: multiSignOut.Out, + }, + } + + utxo, found := res[expectedUTXOs[0].UTXOID] + require.True(found) + require.Equal(utxo, expectedUTXOs[0]) + + utxo, found = res[expectedUTXOs[1].UTXOID] + require.True(found) + require.Equal(utxo, expectedUTXOs[1]) + + // show idempotency + res2 := dep.GetUtxos() + require.Equal(res, res2) +} + +// TODO: Remove Post-Durango +func TestTxDependencyIsAddValidator(t *testing.T) { + require := require.New(t) + + var ( + clk = mockable.Clock{} + avaxAssetID = ids.GenerateTestID() + validatorWeight = uint64(2022) + ) + + in := &avax.TransferableInput{ + UTXOID: avax.UTXOID{ + TxID: ids.ID{'t', 'x', 'I', 'D'}, + OutputIndex: 2, + }, + Asset: avax.Asset{ID: avaxAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: uint64(5678), + Input: secp256k1fx.Input{SigIndices: []uint32{0}}, + }, + } + out := &avax.TransferableOutput{ + Asset: avax.Asset{ID: avaxAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: uint64(1234), + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + }, + } + stake := &avax.TransferableOutput{ + Asset: avax.Asset{ID: avaxAssetID}, + Out: &stakeable.LockOut{ + Locktime: uint64(clk.Time().Add(time.Second).Unix()), + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: validatorWeight, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + }, + }, + } + utx := &txs.AddValidatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: uint32(1492), + BlockchainID: ids.GenerateTestID(), + Ins: []*avax.TransferableInput{in}, + Outs: []*avax.TransferableOutput{out}, + }}, + Validator: txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: uint64(clk.Time().Unix()), + End: uint64(clk.Time().Add(time.Hour).Unix()), + Wght: validatorWeight, + }, + StakeOuts: []*avax.TransferableOutput{stake}, + RewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[1].PublicKey().Address()}, + }, + DelegationShares: reward.PercentDenominator, + } + tx, err := txs.NewSigned(utx, txs.Codec, nil) + require.NoError(err) + + dep := &SingleTxDependency{Tx: tx} + res := dep.GetUtxos() + require.Len(res, 2) + + expectedUTXOs := []*avax.UTXO{ + { + UTXOID: avax.UTXOID{ + TxID: tx.ID(), + OutputIndex: 0, + }, + Asset: out.Asset, + Out: out.Out, + }, + { + UTXOID: avax.UTXOID{ + TxID: tx.ID(), + OutputIndex: 1, + }, + Asset: stake.Asset, + Out: stake.Out, + }, + } + + utxo, found := res[expectedUTXOs[0].UTXOID] + require.True(found) + require.Equal(utxo, expectedUTXOs[0]) + + utxo, found = res[expectedUTXOs[1].UTXOID] + require.True(found) + require.Equal(utxo, expectedUTXOs[1]) + + // show idempotency + res2 := dep.GetUtxos() + require.Equal(res, res2) +} + +func TestTxDependencyIsAddPermissionlessValidator(t *testing.T) { + require := require.New(t) + + var ( + clk = mockable.Clock{} + avaxAssetID = ids.GenerateTestID() + validatorWeight = uint64(2022) + ) + + in := &avax.TransferableInput{ + UTXOID: avax.UTXOID{ + TxID: ids.ID{'t', 'x', 'I', 'D'}, + OutputIndex: 2, + }, + Asset: avax.Asset{ID: avaxAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: uint64(5678), + Input: secp256k1fx.Input{SigIndices: []uint32{0}}, + }, + } + out := &avax.TransferableOutput{ + Asset: avax.Asset{ID: avaxAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: uint64(1234), + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + }, + } + stake := &avax.TransferableOutput{ + Asset: avax.Asset{ID: avaxAssetID}, + Out: &stakeable.LockOut{ + Locktime: uint64(clk.Time().Add(time.Second).Unix()), + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: validatorWeight, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + }, + }, + } + utx := &txs.AddPermissionlessValidatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: uint32(1492), + BlockchainID: ids.GenerateTestID(), + Ins: []*avax.TransferableInput{in}, + Outs: []*avax.TransferableOutput{out}, + }}, + Validator: txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: uint64(clk.Time().Unix()), + End: uint64(clk.Time().Add(time.Hour).Unix()), + Wght: validatorWeight, + }, + Subnet: constants.PrimaryNetworkID, + Signer: &signer.Empty{}, + StakeOuts: []*avax.TransferableOutput{stake}, + ValidatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[1].PublicKey().Address()}, + }, + DelegatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[1].PublicKey().Address()}, + }, + DelegationShares: reward.PercentDenominator, + } + tx, err := txs.NewSigned(utx, txs.Codec, nil) + require.NoError(err) + + dep := &SingleTxDependency{Tx: tx} + res := dep.GetUtxos() + require.Len(res, 2) + + expectedUTXOs := []*avax.UTXO{ + { + UTXOID: avax.UTXOID{ + TxID: tx.ID(), + OutputIndex: 0, + }, + Asset: out.Asset, + Out: out.Out, + }, + { + UTXOID: avax.UTXOID{ + TxID: tx.ID(), + OutputIndex: 1, + }, + Asset: stake.Asset, + Out: stake.Out, + }, + } + + utxo, found := res[expectedUTXOs[0].UTXOID] + require.True(found) + require.Equal(utxo, expectedUTXOs[0]) + + utxo, found = res[expectedUTXOs[1].UTXOID] + require.True(found) + require.Equal(utxo, expectedUTXOs[1]) + + // show idempotency + res2 := dep.GetUtxos() + require.Equal(res, res2) +} diff --git a/server/mapper/pchain/tx_ops.go b/server/mapper/pchain/tx_ops.go new file mode 100644 index 0000000..789232e --- /dev/null +++ b/server/mapper/pchain/tx_ops.go @@ -0,0 +1,62 @@ +package pchain + +import "github.com/coinbase/rosetta-sdk-go/types" + +// txOps collects all balance-changing information within a transaction +type txOps struct { + isConstruction bool + Ins []*types.Operation + Outs []*types.Operation + StakeOuts []*types.Operation + ImportIns []*types.Operation + ExportOuts []*types.Operation +} + +func newTxOps(isConstruction bool) *txOps { + return &txOps{isConstruction: isConstruction} +} + +func (t *txOps) IncludedOperations() []*types.Operation { + ops := []*types.Operation{} + ops = append(ops, t.Ins...) + ops = append(ops, t.Outs...) + ops = append(ops, t.StakeOuts...) + return ops +} + +// Used to populate operation identifier +func (t *txOps) Len() int { + return len(t.Ins) + len(t.Outs) + len(t.StakeOuts) +} + +// Used to populate coin identifier +func (t *txOps) OutputLen() int { + return len(t.Outs) + len(t.StakeOuts) +} + +func (t *txOps) Append(op *types.Operation, metaType string) { + switch metaType { + case OpTypeImport: + if t.isConstruction { + t.Ins = append(t.Ins, op) + } else { + // removing operation identifier as these will be skipped in the final operations list + op.OperationIdentifier = nil + t.ImportIns = append(t.ImportIns, op) + } + case OpTypeExport: + if t.isConstruction { + t.Outs = append(t.Outs, op) + } else { + // removing operation identifier as these will be skipped in the final operations list + op.OperationIdentifier = nil + t.ExportOuts = append(t.ExportOuts, op) + } + case OpTypeStakeOutput, OpTypeReward: + t.StakeOuts = append(t.StakeOuts, op) + case OpTypeOutput: + t.Outs = append(t.Outs, op) + case OpTypeInput: + t.Ins = append(t.Ins, op) + } +} diff --git a/server/mapper/pchain/tx_parser.go b/server/mapper/pchain/tx_parser.go new file mode 100644 index 0000000..c8a4eda --- /dev/null +++ b/server/mapper/pchain/tx_parser.go @@ -0,0 +1,755 @@ +package pchain + +import ( + "context" + "errors" + "fmt" + "log" + "math/big" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/mapper" +) + +var ( + errNilPChainClient = errors.New("pchain client can only be nil during construction") + errNilInputTxAccounts = errors.New("input tx accounts cannot be nil") + errUnknownDestinationChain = errors.New("unknown destination chain") + errNoDependencyTxs = errors.New("no dependency txs provided") + errNoMatchingRewardOutputs = errors.New("no matching reward outputs") + errNoMatchingInputAddresses = errors.New("no matching input addresses") + errNoOutputAddresses = errors.New("no output addresses") + errFailedToGetUTXOAddresses = errors.New("failed to get utxo addresses") + errFailedToCheckMultisig = errors.New("failed to check utxo for multisig") + errUnknownOutputType = errors.New("unknown output type") + errUnknownInputType = errors.New("unknown input type") + errUnknownRewardSourceTransaction = errors.New("unknown source tx type for reward tx") + errUnsupportedAssetInConstruction = errors.New("unsupported asset passed during construction") +) + +type TxParserConfig struct { + // IsConstruction indicates if parsing is done as part of construction or /block endpoints + IsConstruction bool + // Hrp used for address formatting + Hrp string + + // ChainIDs maps chain id to chain id alias mappings + // ChainIDs may provided by TxParser called or lazily initialized, + // as soon as pChainClient is ready to serve requests + ChainIDs map[ids.ID]constants.ChainIDAlias + + // AvaxAssetID contains asset id for AVAX currency + AvaxAssetID ids.ID + // PChainClient holds a P-chain client, used to lookup asset descriptions for non-AVAX assets + PChainClient client.PChainClient +} + +func (cfg *TxParserConfig) lazyInitChainIDs() error { + if cfg.ChainIDs != nil { + return nil // mapping provided by caller + } + + cfg.ChainIDs = map[ids.ID]constants.ChainIDAlias{ + ids.Empty: constants.PChain, + } + + ctx := context.Background() + cChainID, err := cfg.PChainClient.GetBlockchainID(ctx, constants.CChain.String()) + if err != nil { + return err + } + cfg.ChainIDs[cChainID] = constants.CChain + + xChainID, err := cfg.PChainClient.GetBlockchainID(ctx, constants.XChain.String()) + if err != nil { + return err + } + cfg.ChainIDs[xChainID] = constants.XChain + return nil +} + +// TxParser parses P-chain transactions and generate corresponding Rosetta operations +type TxParser struct { + cfg TxParserConfig + + // dependencyTxs maps transaction id to dependence transaction mapping + dependencyTxs BlockTxDependencies + // inputTxAccounts contain utxo id to account identifier mappings + inputTxAccounts map[string]*types.AccountIdentifier +} + +// NewTxParser returns a new transaction parser +func NewTxParser( + cfg TxParserConfig, + inputTxAccounts map[string]*types.AccountIdentifier, + dependencyTxs BlockTxDependencies, +) (*TxParser, error) { + if err := cfg.lazyInitChainIDs(); err != nil { + return nil, err + } + + if inputTxAccounts == nil { + return nil, errNilInputTxAccounts + } + + if !cfg.IsConstruction && cfg.PChainClient == nil { + return nil, errNilPChainClient + } + + return &TxParser{ + cfg: cfg, + inputTxAccounts: inputTxAccounts, + dependencyTxs: dependencyTxs, + }, nil +} + +// Parse converts the given unsigned P-chain tx to corresponding Rosetta Transaction +func (t *TxParser) Parse(signedTx *txs.Tx) (*types.Transaction, error) { + var ( + ops *txOps + txType string + err error + ) + + // TODO: Move to using [txs.Visitor] from AvalancheGo + // Ref: https://github.com/ava-labs/avalanchego/blob/master/vms/platformvm/txs/visitor.go + txID := signedTx.ID() + switch unsignedTx := signedTx.Unsigned.(type) { + case *txs.AddValidatorTx: + txType = OpAddValidator + ops, err = t.parseAddValidatorTx(txID, unsignedTx) + case *txs.AddSubnetValidatorTx: + txType = OpAddSubnetValidator + ops, err = t.parseAddSubnetValidatorTx(txID, unsignedTx) + case *txs.AddDelegatorTx: + txType = OpAddDelegator + ops, err = t.parseAddDelegatorTx(txID, unsignedTx) + case *txs.CreateChainTx: + txType = OpCreateChain + ops, err = t.parseCreateChainTx(txID, unsignedTx) + case *txs.CreateSubnetTx: + txType = OpCreateSubnet + ops, err = t.parseCreateSubnetTx(txID, unsignedTx) + case *txs.ImportTx: + txType = OpImportAvax + ops, err = t.parseImportTx(txID, unsignedTx) + case *txs.ExportTx: + txType = OpExportAvax + ops, err = t.parseExportTx(txID, unsignedTx) + case *txs.AdvanceTimeTx: + txType = OpAdvanceTime + // no op tx + case *txs.RewardValidatorTx: + txType = OpRewardValidator + ops, err = t.parseRewardValidatorTx(unsignedTx) + case *txs.RemoveSubnetValidatorTx: + txType = OpRemoveSubnetValidator + ops, err = t.parseRemoveSubnetValidatorTx(txID, unsignedTx) + case *txs.TransformSubnetTx: + txType = OpTransformSubnetValidator + ops, err = t.parseTransformSubnetTx(txID, unsignedTx) + case *txs.AddPermissionlessValidatorTx: + txType = OpAddPermissionlessValidator + ops, err = t.parseAddPermissionlessValidatorTx(txID, unsignedTx) + case *txs.AddPermissionlessDelegatorTx: + txType = OpAddPermissionlessDelegator + ops, err = t.parseAddPermissionlessDelegatorTx(txID, unsignedTx) + case *txs.TransferSubnetOwnershipTx: + txType = OpTransferSubnetOwnership + ops, err = t.parseTransferSubnetOwnershipTx(txID, unsignedTx) + case *txs.BaseTx: + txType = OpBase + ops, err = t.parseBaseTx(txID, unsignedTx) + default: + log.Printf("unknown type %T", unsignedTx) + } + if err != nil { + return nil, err + } + + txMetadata := map[string]interface{}{ + MetadataTxType: txType, + } + + var operations []*types.Operation + if ops != nil { + operations = ops.IncludedOperations() + idx := len(operations) + if ops.ImportIns != nil { + importedInputs := addOperationIdentifiers(ops.ImportIns, idx) + idx += len(importedInputs) + txMetadata[mapper.MetadataImportedInputs] = importedInputs + } + + if ops.ExportOuts != nil { + exportedOutputs := addOperationIdentifiers(ops.ExportOuts, idx) + txMetadata[mapper.MetadataExportedOutputs] = exportedOutputs + } + } + + return &types.Transaction{ + TransactionIdentifier: &types.TransactionIdentifier{ + Hash: txID.String(), + }, + Operations: operations, + Metadata: txMetadata, + }, nil +} + +func addOperationIdentifiers(operations []*types.Operation, startIdx int) []*types.Operation { + result := make([]*types.Operation, 0, len(operations)) + for idx, operation := range operations { + operation := operation + operation.OperationIdentifier = &types.OperationIdentifier{Index: int64(startIdx + idx)} + result = append(result, operation) + } + + return result +} + +func (t *TxParser) parseExportTx(txID ids.ID, tx *txs.ExportTx) (*txOps, error) { + ops, err := t.baseTxToCombinedOperations(txID, &tx.BaseTx, OpExportAvax) + if err != nil { + return nil, err + } + + chainIDAlias, ok := t.cfg.ChainIDs[tx.DestinationChain] + if !ok { + return nil, errUnknownDestinationChain + } + + err = t.outsToOperations(ops, OpExportAvax, txID, tx.ExportedOutputs, OpTypeExport, chainIDAlias) + if err != nil { + return nil, err + } + + return ops, nil +} + +func (t *TxParser) parseImportTx(txID ids.ID, tx *txs.ImportTx) (*txOps, error) { + ops := newTxOps(t.cfg.IsConstruction) + + err := t.insToOperations(ops, OpImportAvax, tx.Ins, OpTypeInput) + if err != nil { + return nil, err + } + + err = t.insToOperations(ops, OpImportAvax, tx.ImportedInputs, OpTypeImport) + if err != nil { + return nil, err + } + + err = t.outsToOperations(ops, OpImportAvax, txID, tx.Outs, OpTypeOutput, constants.PChain) + if err != nil { + return nil, err + } + + return ops, nil +} + +func (t *TxParser) parseAddValidatorTx(txID ids.ID, tx *txs.AddValidatorTx) (*txOps, error) { + ops, err := t.baseTxToCombinedOperations(txID, &tx.BaseTx, OpAddValidator) + if err != nil { + return nil, err + } + + err = t.outsToOperations(ops, OpAddValidator, txID, tx.Stake(), OpTypeStakeOutput, constants.PChain) + if err != nil { + return nil, err + } + addValidatorMetadataToStakeOuts(ops, tx, tx.Validator.StartTime(), t.cfg.Hrp) + + return ops, nil +} + +func (t *TxParser) parseAddPermissionlessValidatorTx(txID ids.ID, tx *txs.AddPermissionlessValidatorTx) (*txOps, error) { + ops, err := t.baseTxToCombinedOperations(txID, &tx.BaseTx, OpAddPermissionlessValidator) + if err != nil { + return nil, err + } + + err = t.outsToOperations(ops, OpAddPermissionlessValidator, txID, tx.Stake(), OpTypeStakeOutput, constants.PChain) + if err != nil { + return nil, err + } + addValidatorMetadataToStakeOuts(ops, tx, tx.Validator.StartTime(), t.cfg.Hrp) + + if tx.Signer != nil { + for _, out := range ops.StakeOuts { + out.Metadata[MetadataSigner] = tx.Signer + } + } + + return ops, nil +} + +func (t *TxParser) parseAddDelegatorTx(txID ids.ID, tx *txs.AddDelegatorTx) (*txOps, error) { + ops, err := t.baseTxToCombinedOperations(txID, &tx.BaseTx, OpAddDelegator) + if err != nil { + return nil, err + } + + err = t.outsToOperations(ops, OpAddDelegator, txID, tx.Stake(), OpTypeStakeOutput, constants.PChain) + if err != nil { + return nil, err + } + addDelegatorMetadataToStakeOuts(ops, tx, tx.Validator.StartTime(), t.cfg.Hrp) + + return ops, nil +} + +func (t *TxParser) parseAddPermissionlessDelegatorTx(txID ids.ID, tx *txs.AddPermissionlessDelegatorTx) (*txOps, error) { + ops, err := t.baseTxToCombinedOperations(txID, &tx.BaseTx, OpAddPermissionlessDelegator) + if err != nil { + return nil, err + } + + err = t.outsToOperations(ops, OpAddPermissionlessDelegator, txID, tx.Stake(), OpTypeStakeOutput, constants.PChain) + if err != nil { + return nil, err + } + addDelegatorMetadataToStakeOuts(ops, tx, tx.Validator.StartTime(), t.cfg.Hrp) + + return ops, nil +} + +func (t *TxParser) parseRewardValidatorTx(tx *txs.RewardValidatorTx) (*txOps, error) { + stakingTxID := tx.TxID + + if t.dependencyTxs == nil { + return nil, errNoDependencyTxs + } + dep := t.dependencyTxs[stakingTxID] + if dep == nil { + return nil, errNoMatchingRewardOutputs + } + ops := newTxOps(t.cfg.IsConstruction) + err := t.utxosToOperations(ops, OpRewardValidator, dep.RewardUTXOs, OpTypeReward, constants.PChain) + if err != nil { + return nil, err + } + + switch utx := dep.Tx.Unsigned.(type) { + case *txs.AddValidatorTx: + addValidatorMetadataToStakeOuts(ops, utx, utx.Validator.StartTime(), t.cfg.Hrp) + case *txs.AddDelegatorTx: + addDelegatorMetadataToStakeOuts(ops, utx, utx.Validator.StartTime(), t.cfg.Hrp) + case *txs.AddPermissionlessValidatorTx: + addValidatorMetadataToStakeOuts(ops, utx, utx.Validator.StartTime(), t.cfg.Hrp) + case *txs.AddPermissionlessDelegatorTx: + addDelegatorMetadataToStakeOuts(ops, utx, utx.Validator.StartTime(), t.cfg.Hrp) + default: + return nil, errUnknownRewardSourceTransaction + } + + return ops, nil +} + +func getAddressArray(owners *secp256k1fx.OutputOwners, hrp string) []string { + addrs := make([]string, len(owners.Addrs)) + for i, addr := range owners.Addresses() { + addrs[i], _ = address.Format("P", hrp, addr) + } + return addrs +} + +func addValidatorMetadataToStakeOuts(ops *txOps, validator txs.ValidatorTx, startTime time.Time, hrp string) { + if validator == nil { + return + } + + for _, out := range ops.StakeOuts { + out.Metadata[MetadataValidatorNodeID] = validator.NodeID().String() + out.Metadata[MetadataStakingStartTime] = uint64(startTime.Unix()) + out.Metadata[MetadataStakingEndTime] = uint64(validator.EndTime().Unix()) + out.Metadata[MetadataValidatorRewardsOwner] = getAddressArray(validator.ValidationRewardsOwner().(*secp256k1fx.OutputOwners), hrp) + out.Metadata[MetadataDelegationRewardsOwner] = getAddressArray(validator.DelegationRewardsOwner().(*secp256k1fx.OutputOwners), hrp) + out.Metadata[MetadataSubnetID] = validator.SubnetID().String() + } +} + +func addDelegatorMetadataToStakeOuts(ops *txOps, delegator txs.DelegatorTx, startTime time.Time, hrp string) { + if delegator == nil { + return + } + + for _, out := range ops.StakeOuts { + out.Metadata[MetadataValidatorNodeID] = delegator.NodeID().String() + out.Metadata[MetadataStakingStartTime] = uint64(startTime.Unix()) + out.Metadata[MetadataStakingEndTime] = uint64(delegator.EndTime().Unix()) + out.Metadata[MetadataDelegatorRewardsOwner] = getAddressArray(delegator.RewardsOwner().(*secp256k1fx.OutputOwners), hrp) + out.Metadata[MetadataSubnetID] = delegator.SubnetID().String() + } +} + +func (t *TxParser) parseBaseTx(txID ids.ID, tx *txs.BaseTx) (*txOps, error) { + return t.baseTxToCombinedOperations(txID, tx, OpBase) +} + +func (t *TxParser) parseTransferSubnetOwnershipTx(txID ids.ID, tx *txs.TransferSubnetOwnershipTx) (*txOps, error) { + return t.baseTxToCombinedOperations(txID, &tx.BaseTx, OpTransferSubnetOwnership) +} + +func (t *TxParser) parseCreateSubnetTx(txID ids.ID, tx *txs.CreateSubnetTx) (*txOps, error) { + return t.baseTxToCombinedOperations(txID, &tx.BaseTx, OpCreateSubnet) +} + +func (t *TxParser) parseAddSubnetValidatorTx(txID ids.ID, tx *txs.AddSubnetValidatorTx) (*txOps, error) { + return t.baseTxToCombinedOperations(txID, &tx.BaseTx, OpAddSubnetValidator) +} + +func (t *TxParser) parseRemoveSubnetValidatorTx(txID ids.ID, tx *txs.RemoveSubnetValidatorTx) (*txOps, error) { + return t.baseTxToCombinedOperations(txID, &tx.BaseTx, OpRemoveSubnetValidator) +} + +func (t *TxParser) parseTransformSubnetTx(txID ids.ID, tx *txs.TransformSubnetTx) (*txOps, error) { + return t.baseTxToCombinedOperations(txID, &tx.BaseTx, OpTransformSubnetValidator) +} + +func (t *TxParser) parseCreateChainTx(txID ids.ID, tx *txs.CreateChainTx) (*txOps, error) { + return t.baseTxToCombinedOperations(txID, &tx.BaseTx, OpCreateChain) +} + +func (t *TxParser) baseTxToCombinedOperations(txID ids.ID, tx *txs.BaseTx, txType string) (*txOps, error) { + ops := newTxOps(t.cfg.IsConstruction) + + err := t.insToOperations(ops, txType, tx.Ins, OpTypeInput) + if err != nil { + return nil, err + } + + err = t.outsToOperations(ops, txType, txID, tx.Outs, OpTypeOutput, constants.PChain) + if err != nil { + return nil, err + } + + return ops, nil +} + +func (t *TxParser) insToOperations( + inOps *txOps, + opType string, + txIns []*avax.TransferableInput, + metaType string, +) error { + status := types.String(mapper.StatusSuccess) + if t.cfg.IsConstruction { + status = nil + } + + for _, in := range txIns { + metadata := &OperationMetadata{ + Type: metaType, + } + + input := in.In + if stakeableIn, ok := input.(*stakeable.LockIn); ok { + metadata.Locktime = stakeableIn.Locktime + input = stakeableIn.TransferableIn + } + transferInput, ok := input.(*secp256k1fx.TransferInput) + if !ok { + return errUnknownInputType + } + metadata.SigIndices = transferInput.SigIndices + + opMetadata, err := mapper.MarshalJSONMap(metadata) + if err != nil { + return err + } + + utxoIDStr := in.UTXOID.String() + + var account *types.AccountIdentifier + + // Check if the dependency is not multisig and extract account id from it + // for non-imported inputs or when tx is being constructed + if t.cfg.IsConstruction || metaType != OpTypeImport { + // If dependency txs are provided, which is the case for /block endpoints + // check whether the input UTXO is multisig. If so, skip it. + if t.dependencyTxs != nil { + isMultisig, err := t.isMultisig(in.UTXOID) + if err != nil { + return errFailedToCheckMultisig + } + if isMultisig { + continue + } + } + + var ok bool + account, ok = t.inputTxAccounts[utxoIDStr] + if !ok { + return errNoMatchingInputAddresses + } + } + + bigAmount := new(big.Int).SetUint64(in.In.Amount()) + // Negating input amount + inputAmount := new(big.Int).Neg(bigAmount) + + amount, err := t.buildAmount(inputAmount, in.AssetID()) + if err != nil { + return err + } + + inOp := &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(inOps.Len()), + }, + Type: opType, + Status: status, + Account: account, + Amount: amount, + CoinChange: &types.CoinChange{ + CoinIdentifier: &types.CoinIdentifier{ + Identifier: utxoIDStr, + }, + CoinAction: types.CoinSpent, + }, + Metadata: opMetadata, + } + + inOps.Append(inOp, metaType) + } + return nil +} + +func (t *TxParser) buildAmount(value *big.Int, assetID ids.ID) (*types.Amount, error) { + if assetID == t.cfg.AvaxAssetID { + return mapper.AtomicAvaxAmount(value), nil + } + + if t.cfg.IsConstruction { + return nil, errUnsupportedAssetInConstruction + } + + currency, err := t.lookupCurrency(assetID) + if err != nil { + return nil, err + } + + return mapper.Amount(value, currency), nil +} + +func (t *TxParser) outsToOperations( + outOps *txOps, + opType string, + txID ids.ID, + txOut []*avax.TransferableOutput, + metaType string, + chainIDAlias constants.ChainIDAlias, +) error { + outIndexOffset := outOps.OutputLen() + status := types.String(mapper.StatusSuccess) + if t.cfg.IsConstruction { + status = nil + } + + for outIndex, out := range txOut { + transferOut := out.Out + + if lockOut, ok := transferOut.(*stakeable.LockOut); ok { + transferOut = lockOut.TransferableOut + } + + transferOutput, ok := transferOut.(*secp256k1fx.TransferOutput) + if !ok { + return errUnknownOutputType + } + + // Rosetta cannot handle multisig at the moment. In order to pass data validation, + // we treat multisig outputs like a burn and inputs line a mint and therefore + // not include them in the operations + // + // Additionally, it is possible to have outputs without any addresses + // (e.g. https://testnet.avascan.info/blockchain/p/block/81016) + // + // therefore we skip parsing operations unless there is exactly 1 address + if len(transferOutput.Addrs) != 1 { + continue + } + + outOp, err := t.buildOutputOperation( + transferOutput, + out.AssetID(), + status, + outOps.Len(), + txID, + uint32(outIndexOffset+outIndex), + opType, + metaType, + chainIDAlias, + ) + if err != nil { + return err + } + + outOps.Append(outOp, metaType) + } + + return nil +} + +func (t *TxParser) utxosToOperations( + outOps *txOps, + opType string, + utxos []*avax.UTXO, + metaType string, + chainIDAlias constants.ChainIDAlias, +) error { + status := types.String(mapper.StatusSuccess) + if t.cfg.IsConstruction { + status = nil + } + + for _, utxo := range utxos { + outIntf := utxo.Out + if lockedOut, ok := outIntf.(*stakeable.LockOut); ok { + outIntf = lockedOut.TransferableOut + } + + out, ok := outIntf.(*secp256k1fx.TransferOutput) + + if !ok { + return errUnknownOutputType + } + + // Rosetta cannot handle multisig at the moment. In order to pass data validation, + // we treat multisig outputs like a burn and inputs line a mint and therefore + // not include them in the operations + // + // Additionally, it is possible to have outputs without any addresses + // (e.g. https://testnet.avascan.info/blockchain/p/block/81016) + // + // therefore we skip parsing operations unless there is exactly 1 address + if len(out.Addrs) != 1 { + continue + } + + outOp, err := t.buildOutputOperation( + out, + utxo.AssetID(), + status, + outOps.Len(), + utxo.TxID, + utxo.OutputIndex, + opType, + metaType, + chainIDAlias, + ) + if err != nil { + return err + } + + outOps.Append(outOp, metaType) + } + + return nil +} + +func (t *TxParser) buildOutputOperation( + out *secp256k1fx.TransferOutput, + assetID ids.ID, + status *string, + startIndex int, + txID ids.ID, + outIndex uint32, + opType, metaType string, + chainIDAlias constants.ChainIDAlias, +) (*types.Operation, error) { + if len(out.Addrs) == 0 { + return nil, errNoOutputAddresses + } + + outAddrID := out.Addrs[0] + outAddrFormat, err := address.Format(chainIDAlias.String(), t.cfg.Hrp, outAddrID[:]) + if err != nil { + return nil, err + } + + metadata := &OperationMetadata{ + Type: metaType, + Threshold: out.OutputOwners.Threshold, + Locktime: out.OutputOwners.Locktime, + } + + opMetadata, err := mapper.MarshalJSONMap(metadata) + if err != nil { + return nil, err + } + + outBigAmount := big.NewInt(int64(out.Amount())) + + utxoID := avax.UTXOID{TxID: txID, OutputIndex: outIndex} + + // Do not add coin change during construction as txid is not yet generated + // and therefore UTXO ids would be incorrect + var coinChange *types.CoinChange + if !t.cfg.IsConstruction { + coinChange = &types.CoinChange{ + CoinIdentifier: &types.CoinIdentifier{Identifier: utxoID.String()}, + CoinAction: types.CoinCreated, + } + } + + amount, err := t.buildAmount(outBigAmount, assetID) + if err != nil { + return nil, err + } + + return &types.Operation{ + Type: opType, + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(startIndex), + }, + CoinChange: coinChange, + Status: status, + Account: &types.AccountIdentifier{Address: outAddrFormat}, + Amount: amount, + Metadata: opMetadata, + }, nil +} + +func (t *TxParser) isMultisig(utxoid avax.UTXOID) (bool, error) { + dependencyTx, ok := t.dependencyTxs[utxoid.TxID] + if !ok { + return false, errFailedToCheckMultisig + } + + utxoMap := dependencyTx.GetUtxos() + utxo, ok := utxoMap[utxoid] + if !ok { + return false, errFailedToCheckMultisig + } + + addressable, ok := utxo.Out.(avax.Addressable) + if !ok { + return false, errFailedToCheckMultisig + } + isMultisig := len(addressable.Addresses()) != 1 + + return isMultisig, nil +} + +func (t *TxParser) lookupCurrency(assetID ids.ID) (*types.Currency, error) { + asset, err := t.cfg.PChainClient.GetAssetDescription(context.Background(), assetID.String()) + if err != nil { + return nil, fmt.Errorf("error while looking up currency: %w", err) + } + + return &types.Currency{ + Symbol: asset.Symbol, + Decimals: int32(asset.Denomination), + }, nil +} diff --git a/server/mapper/pchain/tx_parser_test.go b/server/mapper/pchain/tx_parser_test.go new file mode 100644 index 0000000..79cd2d7 --- /dev/null +++ b/server/mapper/pchain/tx_parser_test.go @@ -0,0 +1,536 @@ +package pchain + +import ( + "math/big" + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/coinbase/rosetta-sdk-go/types" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/mapper" + + avaconstants "github.com/ava-labs/avalanchego/utils/constants" +) + +var ( + avaxAssetID, _ = ids.FromString("U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK") + cChainID, _ = ids.FromString("yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp") + chainIDs = map[ids.ID]constants.ChainIDAlias{ + ids.Empty: constants.PChain, + cChainID: constants.CChain, + } +) + +func TestMapInOperation(t *testing.T) { + require := require.New(t) + + _, addValidatorTx, inputAccounts := buildValidatorTx() + + require.Len(addValidatorTx.Ins, 2) + require.Empty(addValidatorTx.Outs) + + ctrl := gomock.NewController(t) + pchainClient := client.NewMockPChainClient(ctrl) + parserCfg := TxParserConfig{ + IsConstruction: false, + Hrp: avaconstants.FujiHRP, + ChainIDs: chainIDs, + AvaxAssetID: avaxAssetID, + PChainClient: pchainClient, + } + parser, err := NewTxParser(parserCfg, inputAccounts, nil) + require.NoError(err) + inOps := newTxOps(false) + require.NoError(parser.insToOperations(inOps, OpAddValidator, addValidatorTx.Ins, OpTypeInput)) + + rosettaInOp := inOps.Ins + + // first input checks + in := addValidatorTx.Ins[0] + rosettaOp := rosettaInOp[0] + require.Equal(int64(0), rosettaOp.OperationIdentifier.Index) + require.Equal(OpAddValidator, rosettaOp.Type) + require.Equal(in.UTXOID.String(), rosettaOp.CoinChange.CoinIdentifier.Identifier) + require.Equal(types.CoinSpent, rosettaOp.CoinChange.CoinAction) + require.Equal(OpTypeInput, rosettaOp.Metadata["type"]) + require.Equal(OpAddValidator, rosettaOp.Type) + require.Equal(types.String(mapper.StatusSuccess), rosettaOp.Status) + require.Equal(float64(0), rosettaOp.Metadata["locktime"]) + require.Nil(rosettaOp.Metadata["threshold"]) + require.NotNil(rosettaOp.Metadata["sig_indices"]) + + // second input checks + in = addValidatorTx.Ins[1] + rosettaOp = rosettaInOp[1] + require.Equal(int64(1), rosettaOp.OperationIdentifier.Index) + require.Equal(OpAddValidator, rosettaOp.Type) + require.Equal(in.UTXOID.String(), rosettaOp.CoinChange.CoinIdentifier.Identifier) + require.Equal(types.CoinSpent, rosettaOp.CoinChange.CoinAction) + require.Equal(OpTypeInput, rosettaOp.Metadata["type"]) + require.Equal(OpAddValidator, rosettaOp.Type) + require.Equal(types.String(mapper.StatusSuccess), rosettaOp.Status) + require.Equal(float64(1666781236), rosettaOp.Metadata["locktime"]) + require.Nil(rosettaOp.Metadata["threshold"]) + require.NotNil(rosettaOp.Metadata["sig_indices"]) +} + +func TestMapNonAvaxTransactionInConstruction(t *testing.T) { + require := require.New(t) + + _, importTx, inputAccounts := buildImport() + + avaxIn := importTx.ImportedInputs[0] + + ctrl := gomock.NewController(t) + pchainClient := client.NewMockPChainClient(ctrl) + parserCfg := TxParserConfig{ + IsConstruction: true, + Hrp: avaconstants.FujiHRP, + ChainIDs: chainIDs, + + // passing empty as AVAX id, so that + // actual avax id in import transaction will not match with AVAX transaction + AvaxAssetID: ids.Empty, + PChainClient: pchainClient, + } + parser, err := NewTxParser(parserCfg, inputAccounts, nil) + require.NoError(err) + inOps := newTxOps(true) + err = parser.insToOperations(inOps, OpImportAvax, []*avax.TransferableInput{avaxIn}, OpTypeInput) + require.ErrorIs(errUnsupportedAssetInConstruction, err) +} + +func TestMapOutOperation(t *testing.T) { + require := require.New(t) + + _, addDelegatorTx, inputAccounts := buildAddDelegator() + + require.Len(addDelegatorTx.Ins, 1) + require.Len(addDelegatorTx.Outs, 1) + + avaxOut := addDelegatorTx.Outs[0] + + ctrl := gomock.NewController(t) + pchainClient := client.NewMockPChainClient(ctrl) + parserCfg := TxParserConfig{ + IsConstruction: true, + Hrp: avaconstants.FujiHRP, + ChainIDs: chainIDs, + AvaxAssetID: avaxAssetID, + PChainClient: pchainClient, + } + parser, err := NewTxParser(parserCfg, inputAccounts, nil) + require.NoError(err) + outOps := newTxOps(false) + require.NoError(parser.outsToOperations(outOps, OpAddDelegator, ids.Empty, []*avax.TransferableOutput{avaxOut}, OpTypeOutput, constants.PChain)) + + rosettaOutOp := outOps.Outs + + require.Equal(int64(0), rosettaOutOp[0].OperationIdentifier.Index) + require.Equal("P-fuji1gdkq8g208e3j4epyjmx65jglsw7vauh86l47ac", rosettaOutOp[0].Account.Address) + require.Equal(mapper.AtomicAvaxCurrency, rosettaOutOp[0].Amount.Currency) + require.Equal("996649063", rosettaOutOp[0].Amount.Value) + require.Equal(OpTypeOutput, rosettaOutOp[0].Metadata["type"]) + require.Nil(rosettaOutOp[0].Status) + require.Equal(OpAddDelegator, rosettaOutOp[0].Type) + + require.NotNil(rosettaOutOp[0].Metadata["threshold"]) + require.NotNil(rosettaOutOp[0].Metadata["locktime"]) + require.Nil(rosettaOutOp[0].Metadata["sig_indices"]) +} + +// TODO: Remove Post-Durango +func TestMapAddValidatorTx(t *testing.T) { + require := require.New(t) + + signedTx, addValidatorTx, inputAccounts := buildValidatorTx() + + require.Len(addValidatorTx.Ins, 2) + require.Empty(addValidatorTx.Outs) + + ctrl := gomock.NewController(t) + pchainClient := client.NewMockPChainClient(ctrl) + parserCfg := TxParserConfig{ + IsConstruction: true, + Hrp: avaconstants.FujiHRP, + ChainIDs: chainIDs, + AvaxAssetID: avaxAssetID, + PChainClient: pchainClient, + } + parser, err := NewTxParser(parserCfg, inputAccounts, nil) + require.NoError(err) + rosettaTransaction, err := parser.Parse(signedTx) + require.NoError(err) + + total := len(addValidatorTx.Ins) + len(addValidatorTx.Outs) + len(addValidatorTx.StakeOuts) + require.Len(rosettaTransaction.Operations, total) + + cntTxType, cntInputMeta, cntOutputMeta, cntMetaType := verifyRosettaTransaction(rosettaTransaction.Operations, OpAddValidator, OpTypeStakeOutput) + + require.Equal(3, cntTxType) + require.Equal(2, cntInputMeta) + require.Zero(cntOutputMeta) + require.Equal(1, cntMetaType) +} + +func TestMapAddPermissionlessValidatorTx(t *testing.T) { + require := require.New(t) + + signedTx, addPermissionlessValidatorTx, inputAccounts := buildAddPermissionlessValidator() + + require.Len(addPermissionlessValidatorTx.Ins, 2) + require.Empty(addPermissionlessValidatorTx.Outs) + + ctrl := gomock.NewController(t) + pchainClient := client.NewMockPChainClient(ctrl) + parserCfg := TxParserConfig{ + IsConstruction: true, + Hrp: avaconstants.FujiHRP, + ChainIDs: chainIDs, + AvaxAssetID: avaxAssetID, + PChainClient: pchainClient, + } + parser, err := NewTxParser(parserCfg, inputAccounts, nil) + require.NoError(err) + rosettaTransaction, err := parser.Parse(signedTx) + require.NoError(err) + + total := len(addPermissionlessValidatorTx.Ins) + len(addPermissionlessValidatorTx.Outs) + len(addPermissionlessValidatorTx.StakeOuts) + require.Len(rosettaTransaction.Operations, total) + + cntTxType, cntInputMeta, cntOutputMeta, cntMetaType := verifyRosettaTransaction(rosettaTransaction.Operations, OpAddPermissionlessValidator, OpTypeStakeOutput) + + require.Equal(3, cntTxType) + require.Equal(2, cntInputMeta) + require.Zero(cntOutputMeta) + require.Equal(1, cntMetaType) +} + +// TODO: Remove Post-Durango +func TestMapAddDelegatorTx(t *testing.T) { + require := require.New(t) + + signedTx, addDelegatorTx, inputAccounts := buildAddDelegator() + + require.Len(addDelegatorTx.Ins, 1) + require.Len(addDelegatorTx.Outs, 1) + require.Len(addDelegatorTx.StakeOuts, 1) + + ctrl := gomock.NewController(t) + pchainClient := client.NewMockPChainClient(ctrl) + parserCfg := TxParserConfig{ + IsConstruction: true, + Hrp: avaconstants.FujiHRP, + ChainIDs: chainIDs, + AvaxAssetID: avaxAssetID, + PChainClient: pchainClient, + } + parser, err := NewTxParser(parserCfg, inputAccounts, nil) + require.NoError(err) + rosettaTransaction, err := parser.Parse(signedTx) + require.NoError(err) + + total := len(addDelegatorTx.Ins) + len(addDelegatorTx.Outs) + len(addDelegatorTx.StakeOuts) + require.Len(rosettaTransaction.Operations, total) + + cntTxType, cntInputMeta, cntOutputMeta, cntMetaType := verifyRosettaTransaction(rosettaTransaction.Operations, OpAddDelegator, OpTypeStakeOutput) + + require.Equal(3, cntTxType) + require.Equal(1, cntInputMeta) + require.Equal(1, cntOutputMeta) + require.Equal(1, cntMetaType) + + require.Equal(types.CoinSpent, rosettaTransaction.Operations[0].CoinChange.CoinAction) + require.Nil(rosettaTransaction.Operations[1].CoinChange) + require.Nil(rosettaTransaction.Operations[2].CoinChange) + + require.Equal(addDelegatorTx.Ins[0].UTXOID.String(), rosettaTransaction.Operations[0].CoinChange.CoinIdentifier.Identifier) + + require.Equal(int64(0), rosettaTransaction.Operations[0].OperationIdentifier.Index) + require.Equal(int64(1), rosettaTransaction.Operations[1].OperationIdentifier.Index) + require.Equal(int64(2), rosettaTransaction.Operations[2].OperationIdentifier.Index) + + require.Equal(OpAddDelegator, rosettaTransaction.Operations[0].Type) + require.Equal(OpAddDelegator, rosettaTransaction.Operations[1].Type) + require.Equal(OpAddDelegator, rosettaTransaction.Operations[2].Type) + + require.Equal(OpTypeInput, rosettaTransaction.Operations[0].Metadata["type"]) + require.Equal(OpTypeOutput, rosettaTransaction.Operations[1].Metadata["type"]) + require.Equal(OpTypeStakeOutput, rosettaTransaction.Operations[2].Metadata["type"]) +} + +func TestMapAddPermissionlessDelegatorTx(t *testing.T) { + require := require.New(t) + + signedTx, addPermissionlessDelegatorTx, inputAccounts := buildAddPermissionlessDelegator() + + require.Len(addPermissionlessDelegatorTx.Ins, 1) + require.Len(addPermissionlessDelegatorTx.Outs, 1) + require.Len(addPermissionlessDelegatorTx.StakeOuts, 1) + + ctrl := gomock.NewController(t) + pchainClient := client.NewMockPChainClient(ctrl) + parserCfg := TxParserConfig{ + IsConstruction: true, + Hrp: avaconstants.FujiHRP, + ChainIDs: chainIDs, + AvaxAssetID: avaxAssetID, + PChainClient: pchainClient, + } + parser, err := NewTxParser(parserCfg, inputAccounts, nil) + require.NoError(err) + rosettaTransaction, err := parser.Parse(signedTx) + require.NoError(err) + + total := len(addPermissionlessDelegatorTx.Ins) + len(addPermissionlessDelegatorTx.Outs) + len(addPermissionlessDelegatorTx.StakeOuts) + require.Len(rosettaTransaction.Operations, total) + + cntTxType, cntInputMeta, cntOutputMeta, cntMetaType := verifyRosettaTransaction(rosettaTransaction.Operations, OpAddPermissionlessDelegator, OpTypeStakeOutput) + + require.Equal(3, cntTxType) + require.Equal(1, cntInputMeta) + require.Equal(1, cntOutputMeta) + require.Equal(1, cntMetaType) + + require.Equal(types.CoinSpent, rosettaTransaction.Operations[0].CoinChange.CoinAction) + require.Nil(rosettaTransaction.Operations[1].CoinChange) + require.Nil(rosettaTransaction.Operations[2].CoinChange) + + require.Equal(addPermissionlessDelegatorTx.Ins[0].UTXOID.String(), rosettaTransaction.Operations[0].CoinChange.CoinIdentifier.Identifier) + + require.Equal(int64(0), rosettaTransaction.Operations[0].OperationIdentifier.Index) + require.Equal(int64(1), rosettaTransaction.Operations[1].OperationIdentifier.Index) + require.Equal(int64(2), rosettaTransaction.Operations[2].OperationIdentifier.Index) + + require.Equal(OpAddPermissionlessDelegator, rosettaTransaction.Operations[0].Type) + require.Equal(OpAddPermissionlessDelegator, rosettaTransaction.Operations[1].Type) + require.Equal(OpAddPermissionlessDelegator, rosettaTransaction.Operations[2].Type) + + require.Equal(OpTypeInput, rosettaTransaction.Operations[0].Metadata["type"]) + require.Equal(OpTypeOutput, rosettaTransaction.Operations[1].Metadata["type"]) + require.Equal(OpTypeStakeOutput, rosettaTransaction.Operations[2].Metadata["type"]) +} + +func TestMapImportTx(t *testing.T) { + require := require.New(t) + signedTx, importTx, inputAccounts := buildImport() + + require.Empty(importTx.Ins) + require.Len(importTx.Outs, 3) + require.Len(importTx.ImportedInputs, 1) + + ctrl := gomock.NewController(t) + pchainClient := client.NewMockPChainClient(ctrl) + parserCfg := TxParserConfig{ + IsConstruction: true, + Hrp: avaconstants.FujiHRP, + ChainIDs: chainIDs, + AvaxAssetID: avaxAssetID, + PChainClient: pchainClient, + } + parser, err := NewTxParser(parserCfg, inputAccounts, nil) + require.NoError(err) + rosettaTransaction, err := parser.Parse(signedTx) + require.NoError(err) + + total := len(importTx.Ins) + len(importTx.Outs) + len(importTx.ImportedInputs) - 2 // - 1 for the multisig output + require.Len(rosettaTransaction.Operations, total) + + cntTxType, cntInputMeta, cntOutputMeta, cntMetaType := verifyRosettaTransaction(rosettaTransaction.Operations, OpImportAvax, OpTypeImport) + + require.Equal(2, cntTxType) + require.Zero(cntInputMeta) + require.Equal(1, cntOutputMeta) + require.Equal(1, cntMetaType) + + require.Equal(types.CoinSpent, rosettaTransaction.Operations[0].CoinChange.CoinAction) + require.Nil(rosettaTransaction.Operations[1].CoinChange) +} + +func TestMapNonConstructionImportTx(t *testing.T) { + require := require.New(t) + + signedTx, importTx, inputAccounts := buildImport() + + require.Empty(importTx.Ins) + require.Len(importTx.Outs, 3) + require.Len(importTx.ImportedInputs, 1) + + ctrl := gomock.NewController(t) + pchainClient := client.NewMockPChainClient(ctrl) + parserCfg := TxParserConfig{ + IsConstruction: false, + Hrp: avaconstants.FujiHRP, + ChainIDs: chainIDs, + AvaxAssetID: avaxAssetID, + PChainClient: pchainClient, + } + parser, err := NewTxParser(parserCfg, inputAccounts, nil) + require.NoError(err) + rosettaTransaction, err := parser.Parse(signedTx) + require.NoError(err) + + total := len(importTx.Ins) + len(importTx.Outs) + len(importTx.ImportedInputs) - 3 // - 1 for the multisig output + require.Len(rosettaTransaction.Operations, total) + + cntTxType, cntInputMeta, cntOutputMeta, cntMetaType := verifyRosettaTransaction(rosettaTransaction.Operations, OpImportAvax, OpTypeImport) + + require.Equal(1, cntTxType) + require.Zero(cntInputMeta) + require.Equal(1, cntOutputMeta) + require.Zero(cntMetaType) + + require.Equal(types.CoinCreated, rosettaTransaction.Operations[0].CoinChange.CoinAction) + + // Verify that export output are properly generated + importInputs, ok := rosettaTransaction.Metadata[mapper.MetadataImportedInputs].([]*types.Operation) + require.True(ok) + + importedInput := importTx.ImportedInputs[0] + expectedImportedInputs := []*types.Operation{{ + OperationIdentifier: &types.OperationIdentifier{Index: 1}, + Type: OpImportAvax, + Status: types.String(mapper.StatusSuccess), + Account: nil, + Amount: mapper.AtomicAvaxAmount(big.NewInt(-int64(importedInput.Input().Amount()))), + CoinChange: &types.CoinChange{ + CoinIdentifier: &types.CoinIdentifier{Identifier: importedInput.UTXOID.String()}, + CoinAction: types.CoinSpent, + }, + Metadata: map[string]interface{}{ + "type": OpTypeImport, + "locktime": 0.0, + }, + }} + + require.Equal(expectedImportedInputs, importInputs) +} + +func TestMapExportTx(t *testing.T) { + require := require.New(t) + signedTx, exportTx, inputAccounts := buildExport() + + require.Len(exportTx.Ins, 1) + require.Len(exportTx.Outs, 1) + require.Len(exportTx.ExportedOutputs, 1) + + ctrl := gomock.NewController(t) + pchainClient := client.NewMockPChainClient(ctrl) + parserCfg := TxParserConfig{ + IsConstruction: true, + Hrp: avaconstants.FujiHRP, + ChainIDs: chainIDs, + AvaxAssetID: avaxAssetID, + PChainClient: pchainClient, + } + parser, err := NewTxParser(parserCfg, inputAccounts, nil) + require.NoError(err) + rosettaTransaction, err := parser.Parse(signedTx) + require.NoError(err) + + total := len(exportTx.Ins) + len(exportTx.Outs) + len(exportTx.ExportedOutputs) + require.Len(rosettaTransaction.Operations, total) + + cntTxType, cntInputMeta, cntOutputMeta, cntMetaType := verifyRosettaTransaction(rosettaTransaction.Operations, OpExportAvax, OpTypeExport) + + require.Equal(3, cntTxType) + require.Equal(1, cntInputMeta) + require.Equal(1, cntOutputMeta) + require.Equal(1, cntMetaType) +} + +func TestMapNonConstructionExportTx(t *testing.T) { + require := require.New(t) + + signedTx, exportTx, inputAccounts := buildExport() + + require.Len(exportTx.Ins, 1) + require.Len(exportTx.Outs, 1) + require.Len(exportTx.ExportedOutputs, 1) + + ctrl := gomock.NewController(t) + pchainClient := client.NewMockPChainClient(ctrl) + parserCfg := TxParserConfig{ + IsConstruction: false, + Hrp: avaconstants.FujiHRP, + ChainIDs: chainIDs, + AvaxAssetID: avaxAssetID, + PChainClient: pchainClient, + } + parser, err := NewTxParser(parserCfg, inputAccounts, nil) + require.NoError(err) + rosettaTransaction, err := parser.Parse(signedTx) + require.NoError(err) + + total := len(exportTx.Ins) + len(exportTx.Outs) + require.Len(rosettaTransaction.Operations, total) + + cntTxType, cntInputMeta, cntOutputMeta, cntMetaType := verifyRosettaTransaction(rosettaTransaction.Operations, OpExportAvax, OpTypeExport) + + require.Equal(2, cntTxType) + require.Equal(1, cntInputMeta) + require.Equal(1, cntOutputMeta) + require.Zero(cntMetaType) + + txType, ok := rosettaTransaction.Metadata[MetadataTxType].(string) + require.True(ok) + require.Equal(OpExportAvax, txType) + + // Verify that export output are properly generated + exportOutputs, ok := rosettaTransaction.Metadata[mapper.MetadataExportedOutputs].([]*types.Operation) + require.True(ok) + + // setting isConstruction to true in order to include exported output in the operations + parserCfg = TxParserConfig{ + IsConstruction: true, + Hrp: avaconstants.FujiHRP, + ChainIDs: chainIDs, + AvaxAssetID: avaxAssetID, + PChainClient: pchainClient, + } + parser, err = NewTxParser(parserCfg, inputAccounts, nil) + require.NoError(err) + rosettaTransactionWithExportOperations, err := parser.Parse(signedTx) + require.NoError(err) + + out := rosettaTransactionWithExportOperations.Operations[2] + out.Status = types.String(mapper.StatusSuccess) + out.CoinChange = exportOutputs[0].CoinChange + require.Equal([]*types.Operation{out}, exportOutputs) +} + +func verifyRosettaTransaction(operations []*types.Operation, txType string, metaType string) (int, int, int, int) { + cntOpInputMeta := 0 + cntOpOutputMeta := 0 + cntTxType := 0 + cntMetaType := 0 + + for _, v := range operations { + if v.Type == txType { + cntTxType++ + } + + meta := &OperationMetadata{} + _ = mapper.UnmarshalJSONMap(v.Metadata, meta) + + if meta.Type == OpTypeInput { + cntOpInputMeta++ + continue + } + if meta.Type == OpTypeOutput { + cntOpOutputMeta++ + continue + } + if meta.Type == metaType { + cntMetaType++ + continue + } + } + + return cntTxType, cntOpInputMeta, cntOpOutputMeta, cntMetaType +} diff --git a/server/mapper/pchain/types.go b/server/mapper/pchain/types.go new file mode 100644 index 0000000..3c8c178 --- /dev/null +++ b/server/mapper/pchain/types.go @@ -0,0 +1,135 @@ +package pchain + +import ( + "github.com/ava-labs/avalanchego/ids" +) + +const ( + OpImportAvax = "IMPORT_AVAX" + OpExportAvax = "EXPORT_AVAX" + OpAddValidator = "ADD_VALIDATOR" + OpAddPermissionlessValidator = "ADD_PERMISSIONLESS_VALIDATOR" + OpAddDelegator = "ADD_DELEGATOR" + OpAddPermissionlessDelegator = "ADD_PERMISSIONLESS_DELEGATOR" + OpRewardValidator = "REWARD_VALIDATOR" + OpCreateChain = "CREATE_CHAIN" + OpCreateSubnet = "CREATE_SUBNET" + OpAddSubnetValidator = "ADD_SUBNET_VALIDATOR" + OpRemoveSubnetValidator = "REMOVE_SUBNET_VALIDATOR" + OpTransformSubnetValidator = "TRANSFORM_SUBNET_VALIDATOR" + OpAdvanceTime = "ADVANCE_TIME" + OpBase = "BASE" + OpTransferSubnetOwnership = "TRANSFER_SUBNET_OWNERSHIP" + + OpTypeImport = "IMPORT" + OpTypeExport = "EXPORT" + OpTypeInput = "INPUT" + OpTypeOutput = "OUTPUT" + OpTypeStakeOutput = "STAKE" + OpTypeReward = "REWARD" + + MetadataOpType = "type" + MetadataTxType = "tx_type" + MetadataStakingTxID = "staking_tx_id" + MetadataValidatorNodeID = "validator_node_id" + MetadataStakingStartTime = "staking_start_time" + MetadataStakingEndTime = "staking_end_time" + MetadataMessage = "message" + MetadataSigner = "signer" + + MetadataValidatorRewards = "validator_rewards" + MetadataValidatorRewardsOwner = "validator_rewards_owner" + MetadataDelegationRewardsOwner = "delegation_rewards_owner" + MetadataDelegatorRewardsOwner = "delegator_rewards_owner" + MetadataDelegationRewards = "delegation_rewards" + MetadataDelegationFeeRewards = "delegation_fee_rewards" + MetadataSubnetID = "subnet_id" + + SubAccountTypeSharedMemory = "shared_memory" + SubAccountTypeUnlocked = "unlocked" + SubAccountTypeLockedStakeable = "locked_stakeable" + SubAccountTypeLockedNotStakeable = "locked_not_stakeable" + SubAccountTypeStaked = "staked" +) + +var ( + OperationTypes = []string{ + OpImportAvax, + OpExportAvax, + OpAddValidator, + OpAddDelegator, + OpRewardValidator, + OpCreateChain, + OpCreateSubnet, + OpAddSubnetValidator, + OpRemoveSubnetValidator, + OpTransformSubnetValidator, + OpAddPermissionlessValidator, + OpAddPermissionlessDelegator, + } + CallMethods = []string{} +) + +// OperationMetadata contains metadata fields specific to individual Rosetta operations as opposed to transactions +type OperationMetadata struct { + Type string `json:"type"` + SigIndices []uint32 `json:"sig_indices,omitempty"` + Locktime uint64 `json:"locktime"` + Threshold uint32 `json:"threshold,omitempty"` +} + +// ImportExportOptions contain response fields returned by /construction/preprocess for P-chain Import/Export transactions +type ImportExportOptions struct { + SourceChain string `json:"source_chain"` + DestinationChain string `json:"destination_chain"` +} + +// StakingOptions contain response fields returned by /construction/preprocess for P-chain AddValidator/AddDelegator transactions +type StakingOptions struct { + NodeID string `json:"node_id"` + BLSPublicKey string `json:"bls_public_key"` + BLSProofOfPossession string `json:"bls_proof_of_possession"` + ValidationRewardsOwners []string `json:"reward_addresses"` + DelegationRewardsOwners []string `json:"delegator_reward_addresses"` + Start uint64 `json:"start"` // TODO: Remove Post-Durango + End uint64 `json:"end"` + Subnet string `json:"subnet"` + Shares uint32 `json:"shares"` + Locktime uint64 `json:"locktime"` + Threshold uint32 `json:"threshold"` +} + +// Metadata contains metadata values returned by /construction/metadata for P-chain transactions +type Metadata struct { + NetworkID uint32 `json:"network_id"` + BlockchainID ids.ID `json:"blockchain_id"` + *ImportMetadata + *ExportMetadata + *StakingMetadata +} + +// ImportMetadata contain response fields returned by /construction/metadata for P-chain Import transactions +type ImportMetadata struct { + SourceChainID ids.ID `json:"source_chain_id"` +} + +// ExportMetadata contain response fields returned by /construction/metadata for P-chain Export transactions +type ExportMetadata struct { + DestinationChain string `json:"destination_chain"` + DestinationChainID ids.ID `json:"destination_chain_id"` +} + +// StakingMetadata contain response fields returned by /construction/metadata for P-chain AddValidator/AddDelegator transactions +type StakingMetadata struct { + NodeID string `json:"node_id"` + BLSPublicKey string `json:"bls_public_key"` + BLSProofOfPossession string `json:"bls_proof_of_possession"` + ValidationRewardsOwners []string `json:"reward_addresses"` + DelegationRewardsOwners []string `json:"delegator_reward_addresses"` + Start uint64 `json:"start"` // TODO: Remove Post-Durango + End uint64 `json:"end"` + Subnet string `json:"subnet"` + Shares uint32 `json:"shares"` + Locktime uint64 `json:"locktime"` + Threshold uint32 `json:"threshold"` +} diff --git a/server/mapper/peers.go b/server/mapper/peers.go index 4876e19..f666640 100644 --- a/server/mapper/peers.go +++ b/server/mapper/peers.go @@ -1,10 +1,8 @@ package mapper import ( - "github.com/ava-labs/avalanche-rosetta/client" - "github.com/coinbase/rosetta-sdk-go/types" - "github.com/ava-labs/avalanchego/api/info" + "github.com/coinbase/rosetta-sdk-go/types" ) func Peers(peers []info.Peer) []*types.Peer { @@ -27,26 +25,4 @@ func Peers(peers []info.Peer) []*types.Peer { } return result -} - -func Peers_v1_11(peers []client.Peer_v1_11) []*types.Peer { - result := make([]*types.Peer, len(peers)) - - for idx, peer := range peers { - result[idx] = &types.Peer{ - PeerID: peer.ID.String(), - Metadata: map[string]interface{}{ - "ip": peer.IP, - "public_ip": peer.PublicIP, - "version": peer.Version, - "last_sent": peer.LastSent, - "last_received": peer.LastReceived, - "benched": peer.Benched, - "observed_uptime": peer.ObservedUptime, - "tracked_subnets": peer.TrackedSubnets, - }, - } - } - - return result -} +} \ No newline at end of file diff --git a/server/mapper/transaction.go b/server/mapper/transaction.go index 058b291..14b65c5 100644 --- a/server/mapper/transaction.go +++ b/server/mapper/transaction.go @@ -4,14 +4,22 @@ import ( "fmt" "log" "math/big" + "strconv" "strings" - ethtypes "github.com/ava-labs/coreth/core/types" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/coreth/core" "github.com/ava-labs/coreth/plugin/evm" "github.com/coinbase/rosetta-sdk-go/types" "github.com/ethereum/go-ethereum/common" - clientTypes "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/constants" + + ethtypes "github.com/ava-labs/coreth/core/types" ) const ( @@ -22,7 +30,7 @@ const ( ) var ( - x2crate = big.NewInt(1000000000) + X2crate = big.NewInt(1000000000) zeroAddress = common.Address{} burnAddress = common.HexToAddress("0x000000000000000000000000000000000000dEaD") @@ -37,23 +45,23 @@ var ( func Transaction( header *ethtypes.Header, tx *ethtypes.Transaction, - msg *ethtypes.Message, + msg *core.Message, receipt *ethtypes.Receipt, - trace *clientTypes.Call, - flattenedTrace []*clientTypes.FlatCall, - client clientTypes.Client, + trace *client.Call, + flattenedTrace []*client.FlatCall, + rpcClient client.Client, isAnalyticsMode bool, standardModeWhiteList []string, includeUnknownTokens bool, ) (*types.Transaction, error) { ops := []*types.Operation{} - sender := msg.From() + sender := msg.From feeReceiver := &burnAddress txFee := new(big.Int).SetUint64(receipt.GasUsed) - txFee = txFee.Mul(txFee, msg.GasPrice()) + txFee = txFee.Mul(txFee, msg.GasPrice) - if msg.To() != nil && *msg.To() == ftsoContractAddress && receipt.Status == ethtypes.ReceiptStatusSuccessful && tx.Gas() <= ftsoGasRefundLimit { + if msg.To != nil && *msg.To == ftsoContractAddress && receipt.Status == ethtypes.ReceiptStatusSuccessful && tx.Gas() <= ftsoGasRefundLimit { txFee = ftsoTxFee } @@ -100,24 +108,24 @@ func Transaction( switch len(log.Topics) { case topicsInErc721Transfer: - symbol, _, err := client.GetContractInfo(log.Address, false) + symbol, _, err := rpcClient.GetContractInfo(log.Address, false) if err != nil { return nil, err } - if symbol == clientTypes.UnknownERC721Symbol && !includeUnknownTokens { + if symbol == client.UnknownERC721Symbol && !includeUnknownTokens { continue } erc721Ops := erc721Ops(log, int64(len(ops))) ops = append(ops, erc721Ops...) case topicsInErc20Transfer: - symbol, decimals, err := client.GetContractInfo(log.Address, true) + symbol, decimals, err := rpcClient.GetContractInfo(log.Address, true) if err != nil { return nil, err } - if symbol == clientTypes.UnknownERC20Symbol && !includeUnknownTokens { + if symbol == client.UnknownERC20Symbol && !includeUnknownTokens { continue } @@ -143,18 +151,25 @@ func Transaction( } func crossChainTransaction( + networkIdentifier *types.NetworkIdentifier, + chainIDToAliasMapping map[ids.ID]constants.ChainIDAlias, rawIdx int, flrAssetID string, tx *evm.Tx, -) ([]*types.Operation, error) { +) ([]*types.Operation, map[string]interface{}, error) { var ( - ops = []*types.Operation{} - idx = int64(rawIdx) + ops = []*types.Operation{} + exportedOuts = []*types.Operation{} + idx = int64(rawIdx) + metadata = map[string]interface{}{} + + totalInputAmount = big.NewInt(0) + totalOutputAmount = big.NewInt(0) ) // Prepare transaction for ID calcuation if err := tx.Sign(evm.Codec, nil); err != nil { - return nil, err + return nil, nil, err } switch t := tx.UnsignedAtomicTx.(type) { @@ -164,6 +179,7 @@ func crossChainTransaction( mTxIDs := map[string]struct{}{} for _, in := range t.ImportedInputs { mTxIDs[in.TxID.String()] = struct{}{} + totalInputAmount.Add(totalInputAmount, big.NewInt(int64(in.In.Amount()))) } i := 0 txIDs := make([]string, len(mTxIDs)) @@ -177,6 +193,9 @@ func crossChainTransaction( continue } + outAmount := new(big.Int).SetUint64(out.Amount) + totalOutputAmount.Add(totalOutputAmount, outAmount) + op := &types.Operation{ OperationIdentifier: &types.OperationIdentifier{ Index: idx, @@ -187,7 +206,7 @@ func crossChainTransaction( Address: out.Address.Hex(), }, Amount: &types.Amount{ - Value: new(big.Int).Mul(new(big.Int).SetUint64(out.Amount), x2crate).String(), + Value: new(big.Int).Mul(new(big.Int).SetUint64(out.Amount), X2crate).String(), Currency: FlareCurrency, }, Metadata: map[string]interface{}{ @@ -209,6 +228,10 @@ func crossChainTransaction( continue } + // Add input amounts to tx fee + inAmount := new(big.Int).SetUint64(in.Amount) + totalInputAmount.Add(totalInputAmount, inAmount) + op := &types.Operation{ OperationIdentifier: &types.OperationIdentifier{ Index: idx, @@ -219,7 +242,7 @@ func crossChainTransaction( Address: in.Address.Hex(), }, Amount: &types.Amount{ - Value: new(big.Int).Mul(new(big.Int).SetUint64(in.Amount), new(big.Int).Neg(x2crate)).String(), + Value: new(big.Int).Mul(inAmount, new(big.Int).Neg(X2crate)).String(), Currency: FlareCurrency, }, Metadata: map[string]interface{}{ @@ -233,14 +256,89 @@ func crossChainTransaction( } ops = append(ops, op) idx++ + + if alias, ok := chainIDToAliasMapping[t.DestinationChain]; t.ExportedOutputs != nil && ok { + operations, totalExportedAmount, err := createExportedOuts(networkIdentifier, alias, t.ID(), t.ExportedOutputs) + if err != nil { + return nil, nil, err + } + + exportedOuts = append(exportedOuts, operations...) + metadata[MetadataExportedOutputs] = exportedOuts + totalOutputAmount.Add(totalOutputAmount, totalExportedAmount) + } } default: - return nil, fmt.Errorf("unsupported transaction: %T", t) + return nil, nil, fmt.Errorf("unsupported transaction: %T", t) } - return ops, nil + + // Adding operation identifiers to exported outs here since OperationIdentifier is a required field in the spec. + // As Rosetta does not allow gaps in operation identifiers within the same transaction, + // setting the identifier is deferred to here and all operations in the transaction are given sequential indices + for i, exportedOut := range exportedOuts { + exportedOut.OperationIdentifier = &types.OperationIdentifier{ + Index: idx + int64(i), + } + } + + // tx fee is the diff between sums of input/importedInput and output/exportedOutput amounts + txFeeAtomicAvax := new(big.Int).Sub(totalInputAmount, totalOutputAmount) + metadata[MetadataTxFee] = AtomicAvaxAmount(txFeeAtomicAvax) + + return ops, metadata, nil +} + +func createExportedOuts( + networkIdentifier *types.NetworkIdentifier, + chainAlias constants.ChainIDAlias, + txID ids.ID, + exportedOuts []*avax.TransferableOutput, +) ([]*types.Operation, *big.Int, error) { + hrp, err := GetHRP(networkIdentifier) + if err != nil { + return nil, nil, err + } + + operations := []*types.Operation{} + totalAmount := big.NewInt(0) + for outIndex, out := range exportedOuts { + var addr string + transferOutput := out.Output().(*secp256k1fx.TransferOutput) + if transferOutput != nil && len(transferOutput.Addrs) > 0 { + var err error + addr, err = address.Format(chainAlias.String(), hrp, transferOutput.Addrs[0][:]) + if err != nil { + return nil, nil, err + } + } + + utxoID := &avax.UTXOID{ + TxID: txID, + OutputIndex: uint32(outIndex), + } + + totalAmount.Add(totalAmount, new(big.Int).SetUint64(out.Out.Amount())) + + operations = append(operations, &types.Operation{ + Account: &types.AccountIdentifier{Address: addr}, + Type: OpExport, + Status: types.String(StatusSuccess), + Amount: &types.Amount{ + Value: strconv.FormatUint(out.Out.Amount(), 10), + Currency: AtomicAvaxCurrency, + }, + CoinChange: &types.CoinChange{ + CoinIdentifier: &types.CoinIdentifier{Identifier: utxoID.String()}, + CoinAction: types.CoinCreated, + }, + }) + } + return operations, totalAmount, nil } func CrossChainTransactions( + networkIdentifier *types.NetworkIdentifier, + chainIDToAliasMapping map[ids.ID]constants.ChainIDAlias, flrAssetID string, block *ethtypes.Block, ap5Activation uint64, @@ -257,32 +355,28 @@ func CrossChainTransactions( return nil, err } - ops := []*types.Operation{} for _, tx := range atomicTxs { - txOps, err := crossChainTransaction(len(ops), flrAssetID, tx) + txOps, metadata, err := crossChainTransaction(networkIdentifier, chainIDToAliasMapping, 0, flrAssetID, tx) if err != nil { return nil, err } - ops = append(ops, txOps...) - } - // TODO: migrate to using atomic transaction ID instead of marking as a block - // transaction - // - // NOTE: We need to be very careful about this because it will require - // integrators to re-index the chain to get the new result. - transactions = append(transactions, &types.Transaction{ - TransactionIdentifier: &types.TransactionIdentifier{ - Hash: block.Hash().String(), - }, - Operations: ops, - }) + transaction := &types.Transaction{ + TransactionIdentifier: &types.TransactionIdentifier{ + Hash: tx.ID().String(), + }, + Operations: txOps, + Metadata: metadata, + } + + transactions = append(transactions, transaction) + } return transactions, nil } // MempoolTransactionsIDs returns a list of transction IDs in the mempool -func MempoolTransactionsIDs(accountMap clientTypes.TxAccountMap) []*types.TransactionIdentifier { +func MempoolTransactionsIDs(accountMap client.TxAccountMap) []*types.TransactionIdentifier { result := []*types.TransactionIdentifier{} for _, txNonceMap := range accountMap { @@ -299,7 +393,7 @@ func MempoolTransactionsIDs(accountMap clientTypes.TxAccountMap) []*types.Transa return result } -func traceOps(trace []*clientTypes.FlatCall, startIndex int) []*types.Operation { +func traceOps(trace []*client.FlatCall, startIndex int) []*types.Operation { ops := []*types.Operation{} if len(trace) == 0 { return ops @@ -371,7 +465,7 @@ func traceOps(trace []*clientTypes.FlatCall, startIndex int) []*types.Operation if call.Type == OpSelfDestruct { destroyedAccounts[from] = new(big.Int) - // If destination of of SELFDESTRUCT is self, + // If destination of SELFDESTRUCT is self, // we should skip. In the EVM, the balance is reset // after the balance is increased on the destination // so this is a no-op. diff --git a/server/mapper/transaction_test.go b/server/mapper/transaction_test.go index e632610..01c53ce 100644 --- a/server/mapper/transaction_test.go +++ b/server/mapper/transaction_test.go @@ -1,12 +1,20 @@ package mapper import ( + "encoding/hex" + "math/big" "testing" - ethtypes "github.com/ava-labs/coreth/core/types" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/coreth/plugin/evm" "github.com/coinbase/rosetta-sdk-go/types" - ethcommon "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/assert" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanche-rosetta/constants" + + ethtypes "github.com/ava-labs/coreth/core/types" ) var WFLR = &types.Currency{ @@ -18,24 +26,22 @@ var WFLR = &types.Currency{ } func TestZeroAddress(t *testing.T) { - t.Run("correct address", func(t *testing.T) { - assert.Equal(t, ethcommon.HexToAddress("0x0000000000000000000000000000000000000000"), zeroAddress) - }) + require.Equal(t, common.HexToAddress("0x0000000000000000000000000000000000000000"), zeroAddress) } func TestERC20Ops(t *testing.T) { t.Run("transfer op", func(t *testing.T) { log := ðtypes.Log{ - Address: ethcommon.HexToAddress("0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"), - Topics: []ethcommon.Hash{ - ethcommon.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), - ethcommon.HexToHash("0x000000000000000000000000f1b77573a8525acfa116a785092d1ba90d96bf37"), - ethcommon.HexToHash("0x0000000000000000000000005d95ae932d42e53bb9da4de65e9b7263a4fa8564"), + Address: common.HexToAddress("0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"), + Topics: []common.Hash{ + common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), + common.HexToHash("0x000000000000000000000000f1b77573a8525acfa116a785092d1ba90d96bf37"), + common.HexToHash("0x0000000000000000000000005d95ae932d42e53bb9da4de65e9b7263a4fa8564"), }, - Data: ethcommon.FromHex("0x0000000000000000000000000000000000000000000009513ea9de0243800000"), + Data: common.FromHex("0x0000000000000000000000000000000000000000000009513ea9de0243800000"), } - assert.Equal(t, []*types.Operation{ + require.Equal(t, []*types.Operation{ { OperationIdentifier: &types.OperationIdentifier{ Index: 1, @@ -74,16 +80,16 @@ func TestERC20Ops(t *testing.T) { t.Run("burn op", func(t *testing.T) { log := ðtypes.Log{ - Address: ethcommon.HexToAddress("0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"), - Topics: []ethcommon.Hash{ - ethcommon.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), - ethcommon.HexToHash("0x000000000000000000000000f1b77573a8525acfa116a785092d1ba90d96bf37"), - ethcommon.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), + Address: common.HexToAddress("0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"), + Topics: []common.Hash{ + common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), + common.HexToHash("0x000000000000000000000000f1b77573a8525acfa116a785092d1ba90d96bf37"), + common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), }, - Data: ethcommon.FromHex("0x0000000000000000000000000000000000000000000009513ea9de0243800000"), + Data: common.FromHex("0x0000000000000000000000000000000000000000000009513ea9de0243800000"), } - assert.Equal(t, []*types.Operation{ + require.Equal(t, []*types.Operation{ { OperationIdentifier: &types.OperationIdentifier{ Index: 1, @@ -103,16 +109,16 @@ func TestERC20Ops(t *testing.T) { t.Run("mint op", func(t *testing.T) { log := ðtypes.Log{ - Address: ethcommon.HexToAddress("0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"), - Topics: []ethcommon.Hash{ - ethcommon.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), - ethcommon.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), - ethcommon.HexToHash("0x000000000000000000000000f1b77573a8525acfa116a785092d1ba90d96bf37"), + Address: common.HexToAddress("0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"), + Topics: []common.Hash{ + common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), + common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), + common.HexToHash("0x000000000000000000000000f1b77573a8525acfa116a785092d1ba90d96bf37"), }, - Data: ethcommon.FromHex("0x0000000000000000000000000000000000000000000009513ea9de0243800000"), + Data: common.FromHex("0x0000000000000000000000000000000000000000000009513ea9de0243800000"), } - assert.Equal(t, []*types.Operation{ + require.Equal(t, []*types.Operation{ { OperationIdentifier: &types.OperationIdentifier{ Index: 1, @@ -134,16 +140,16 @@ func TestERC20Ops(t *testing.T) { func TestERC721Ops(t *testing.T) { t.Run("transfer op", func(t *testing.T) { log := ðtypes.Log{ - Address: ethcommon.HexToAddress("0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"), - Topics: []ethcommon.Hash{ - ethcommon.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), - ethcommon.HexToHash("0x000000000000000000000000f1b77573a8525acfa116a785092d1ba90d96bf37"), - ethcommon.HexToHash("0x0000000000000000000000005d95ae932d42e53bb9da4de65e9b7263a4fa8564"), - ethcommon.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000051"), + Address: common.HexToAddress("0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"), + Topics: []common.Hash{ + common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), + common.HexToHash("0x000000000000000000000000f1b77573a8525acfa116a785092d1ba90d96bf37"), + common.HexToHash("0x0000000000000000000000005d95ae932d42e53bb9da4de65e9b7263a4fa8564"), + common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000051"), }, } - assert.Equal(t, []*types.Operation{ + require.Equal(t, []*types.Operation{ { OperationIdentifier: &types.OperationIdentifier{ Index: 1, @@ -182,16 +188,16 @@ func TestERC721Ops(t *testing.T) { t.Run("burn op", func(t *testing.T) { log := ðtypes.Log{ - Address: ethcommon.HexToAddress("0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"), - Topics: []ethcommon.Hash{ - ethcommon.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), - ethcommon.HexToHash("0x000000000000000000000000f1b77573a8525acfa116a785092d1ba90d96bf37"), - ethcommon.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), - ethcommon.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000051"), + Address: common.HexToAddress("0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"), + Topics: []common.Hash{ + common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), + common.HexToHash("0x000000000000000000000000f1b77573a8525acfa116a785092d1ba90d96bf37"), + common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), + common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000051"), }, } - assert.Equal(t, []*types.Operation{ + require.Equal(t, []*types.Operation{ { OperationIdentifier: &types.OperationIdentifier{ Index: 1, @@ -211,16 +217,16 @@ func TestERC721Ops(t *testing.T) { t.Run("mint op", func(t *testing.T) { log := ðtypes.Log{ - Address: ethcommon.HexToAddress("0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"), - Topics: []ethcommon.Hash{ - ethcommon.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), - ethcommon.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), - ethcommon.HexToHash("0x000000000000000000000000f1b77573a8525acfa116a785092d1ba90d96bf37"), - ethcommon.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000051"), + Address: common.HexToAddress("0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"), + Topics: []common.Hash{ + common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), + common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), + common.HexToHash("0x000000000000000000000000f1b77573a8525acfa116a785092d1ba90d96bf37"), + common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000051"), }, } - assert.Equal(t, []*types.Operation{ + require.Equal(t, []*types.Operation{ { OperationIdentifier: &types.OperationIdentifier{ Index: 1, @@ -238,3 +244,133 @@ func TestERC721Ops(t *testing.T) { }, erc721Ops(log, 1)) }) } + +func TestCrossChainImportedInputs(t *testing.T) { + require := require.New(t) + + var ( + rawIdx = 0 + avaxAssetID = "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK" + hexTx = "0x000000000000000000057fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d5000000000000000000000000000000000000000000000000000000000000000000000001d4e3812503247f042cc9bbb3395ceca49e28687726489b0ea2d4dd259fadb8b6000000003d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000500000000772651c00000000100000000000000013158e80abd5a1e1aa716003c9db096792c37962100000000772209123d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa000000010000000900000001309767786d3c3548f34373c858f2bab210c6bd6c837d069e314930273d32acc361e86a05bc5bd251e4cb5809bbca7680361ed3263ff5ba2aa467294bfa000aef00cfb71da5" + decodeTx, _ = DecodeToBytes(hexTx) + tx = &evm.Tx{} + + networkIdentifier = &types.NetworkIdentifier{ + Network: constants.TestnetNetwork, + } + chainIDToAliasMapping = map[ids.ID]constants.ChainIDAlias{ + ids.Empty: constants.PChain, + } + ) + + _, err := evm.Codec.Unmarshal(decodeTx, tx) + require.NoError(err) + ops, metadata, err := crossChainTransaction(networkIdentifier, chainIDToAliasMapping, rawIdx, avaxAssetID, tx) + require.NoError(err) + require.Nil(metadata[MetadataExportedOutputs]) + require.Equal(AtomicAvaxAmount(big.NewInt(280750)), metadata[MetadataTxFee]) + + unsignedImportTx := tx.UnsignedAtomicTx.(*evm.UnsignedImportTx) + require.Equal([]*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: OpImport, + Status: types.String(StatusSuccess), + Account: &types.AccountIdentifier{ + Address: "0x3158e80abD5A1e1aa716003C9Db096792C379621", + }, + Amount: &types.Amount{ + Value: "1998719250000000000", + Currency: FlareCurrency, + }, + Metadata: map[string]interface{}{ + "asset_id": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "blockchain_id": "yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp", + "meta": unsignedImportTx.Metadata, + "network_id": uint32(5), + "source_chain": "11111111111111111111111111111111LpoYY", + "tx": "G8BG8APvViEryMzeEMb4YM49TtZeozLPsQ8aR44aZf7waGWJR", + "tx_ids": []string{"2ckxSTK6TbZuC2kwoTD5QWmwiGCq4EXMGQpZWcdJK2ye9v4dSE"}, + }, + }, + }, ops) +} + +func TestCrossChainExportedOuts(t *testing.T) { + require := require.New(t) + + var ( + rawIdx = 0 + avaxAssetID = "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK" + hexTx = "000000000001000000057fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d50000000000000000000000000000000000000000000000000000000000000000000000013158e80abd5a1e1aa716003c9db096792c3796210000000000138aee3d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa000000000000003b000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000700000000000f424000000000000000000000000100000001c83ea4dc195a9275a349e4f616cbb45e23eab2fb00000001000000090000000167fb4fdaa15ce6804e680dc182f0e702259e6f9572a9f5fe0fc6053094951f612a3d9e8128d08be17ae5122d1790160ac8f2e6d21c4b7dde702624eb6219de7301" + decodeTx, _ = hex.DecodeString(hexTx) + tx = &evm.Tx{} + + networkIdentifier = &types.NetworkIdentifier{ + Network: constants.TestnetNetwork, + } + chainIDToAliasMapping = map[ids.ID]constants.ChainIDAlias{ + ids.Empty: constants.PChain, + } + metaBytes, _ = hex.DecodeString("000000000001000000057fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d50000000000000000000000000000000000000000000000000000000000000000000000013158e80abd5a1e1aa716003c9db096792c3796210000000000138aee3d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa000000000000003b000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000700000000000f424000000000000000000000000100000001c83ea4dc195a9275a349e4f616cbb45e23eab2fb00000001000000090000000167fb4fdaa15ce6804e680dc182f0e702259e6f9572a9f5fe0fc6053094951f612a3d9e8128d08be17ae5122d1790160ac8f2e6d21c4b7dde702624eb6219de7301") + metaUnsignedBytes, _ = hex.DecodeString("000000000001000000057fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d50000000000000000000000000000000000000000000000000000000000000000000000013158e80abd5a1e1aa716003c9db096792c3796210000000000138aee3d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa000000000000003b000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000700000000000f424000000000000000000000000100000001c83ea4dc195a9275a349e4f616cbb45e23eab2fb") + meta = &avax.Metadata{} + ) + + meta.Initialize(metaUnsignedBytes, metaBytes) + _, err := evm.Codec.Unmarshal(decodeTx, tx) + require.NoError(err) + ops, metadata, err := crossChainTransaction(networkIdentifier, chainIDToAliasMapping, rawIdx, avaxAssetID, tx) + require.NoError(err) + require.Equal(AtomicAvaxAmount(big.NewInt(280750)), metadata[MetadataTxFee]) + + require.Equal([]*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: OpExport, + Status: types.String(StatusSuccess), + Account: &types.AccountIdentifier{ + Address: "0x3158e80abD5A1e1aa716003C9Db096792C379621", + }, + Amount: &types.Amount{ + Value: "-1280750000000000", + Currency: FlareCurrency, + }, + Metadata: map[string]interface{}{ + "tx": "7QUPqUAMdny53bVptZ2DgxLLN4qZ5X7MnBPseUKYnoh5C5v47", + "blockchain_id": "yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp", + "network_id": uint32(5), + "destination_chain": "11111111111111111111111111111111LpoYY", + "meta": *meta, + "asset_id": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + }, + }, + }, ops) + + require.Equal([]*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 1, + }, + Type: OpExport, + Status: types.String(StatusSuccess), + Account: &types.AccountIdentifier{ + Address: "P-fuji1eql2fhqet2f8tg6funmpdja5tc374vhmdj2xz2", + }, + Amount: &types.Amount{ + Value: "1000000", + Currency: AtomicAvaxCurrency, + }, + CoinChange: &types.CoinChange{ + CoinIdentifier: &types.CoinIdentifier{ + Identifier: "7QUPqUAMdny53bVptZ2DgxLLN4qZ5X7MnBPseUKYnoh5C5v47:0", + }, + CoinAction: types.CoinCreated, + }, + }, + }, metadata[MetadataExportedOutputs]) +} diff --git a/server/mapper/types.go b/server/mapper/types.go index bf30004..d710a5b 100644 --- a/server/mapper/types.go +++ b/server/mapper/types.go @@ -1,20 +1,22 @@ package mapper import ( - "github.com/ethereum/go-ethereum/common" - "github.com/coinbase/rosetta-sdk-go/types" + "github.com/ethereum/go-ethereum/common" ) const ( FlareChainID = 14 FlareAssetID = "2MxKSeEWXViLdYyDhW1SQ46AECZEbE2bnVRZptv42JrxqyUX5k" + FlareHRP = "flare" CostwoChainID = 114 CostwoAssetID = "fxMAKpBQQpFedrUhWMsDYfCUJxdUw4mneTczKBzNg3rc2JUub" + CostwoHRP = "costwo" LocalFlareChainID = 162 LocalFlareAssetID = "QJx3BomP9MGxCy5RPVXyNUeEqzduvuNbxQVXMuPKDakacdY6K" + LocalFlareHRP = "localflare" ContractAddressMetadata = "contractAddress" IndexTransferredMetadata = "indexTransferred" @@ -41,6 +43,12 @@ const ( StatusSuccess = "SUCCESS" StatusFailure = "FAILURE" + + MetadataTxFee = "tx_fee" + MetadataImportedInputs = "imported_inputs" + MetadataExportedOutputs = "exported_outputs" + MetadataAddressFormat = "address_format" + AddressFormatBech32 = "bech32" ) var ( @@ -59,6 +67,11 @@ var ( Decimals: 18, } + AtomicAvaxCurrency = &types.Currency{ + Symbol: "AVAX", + Decimals: 9, + } + OperationStatuses = []*types.OperationStatus{ { Status: StatusSuccess, @@ -94,6 +107,19 @@ var ( CallMethods = []string{ "eth_getTransactionReceipt", } + + ChainIDToHRP = map[uint32]string{ + FlareChainID: FlareHRP, + CostwoChainID: CostwoHRP, + LocalFlareChainID: LocalFlareHRP, + } + + HRPToChainID = map[string]uint32{ + FlareHRP: FlareChainID, + CostwoHRP: CostwoChainID, + LocalFlareHRP: LocalFlareChainID, + "fuji": CostwoChainID, // Alias for backwards compatibility with tests + } ) func CallType(t string) bool { @@ -137,3 +163,13 @@ func ToCurrency(symbol string, decimals uint8, contractAddress common.Address) * }, } } + +func IsSupportedNetworkID(id uint32) bool { + _, ok := ChainIDToHRP[id] + return ok +} + +func IsSupportedHRP(hrp string) bool { + _, ok := HRPToChainID[hrp] + return ok +} diff --git a/server/mapper/types_test.go b/server/mapper/types_test.go index df26481..1fb446d 100644 --- a/server/mapper/types_test.go +++ b/server/mapper/types_test.go @@ -6,7 +6,7 @@ import ( "github.com/coinbase/rosetta-sdk-go/types" "github.com/coinbase/rosetta-sdk-go/utils" "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var USDC = &types.Currency{ @@ -18,7 +18,6 @@ var USDC = &types.Currency{ } func TestMixedCaseAddress(t *testing.T) { - t.Run("correct address", func(t *testing.T) { - assert.True(t, utils.Equal(USDC, ToCurrency("USDC", 6, common.HexToAddress("0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E")))) - }) + parsedCurrency := ToCurrency("USDC", 6, common.HexToAddress("0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E")) + require.True(t, utils.Equal(USDC, parsedCurrency)) } diff --git a/server/mocks/client/client.go b/server/mocks/client/client.go deleted file mode 100644 index fc8dfe6..0000000 --- a/server/mocks/client/client.go +++ /dev/null @@ -1,711 +0,0 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. - -package client - -import ( - big "math/big" - - client "github.com/ava-labs/avalanche-rosetta/client" - common "github.com/ethereum/go-ethereum/common" - - context "context" - - ids "github.com/ava-labs/avalanchego/ids" - - info "github.com/ava-labs/avalanchego/api/info" - - interfaces "github.com/ava-labs/coreth/interfaces" - - mock "github.com/stretchr/testify/mock" - - rpc "github.com/ava-labs/avalanchego/utils/rpc" - - types "github.com/ava-labs/coreth/core/types" -) - -// Client is an autogenerated mock type for the Client type -type Client struct { - mock.Mock -} - -// BalanceAt provides a mock function with given fields: _a0, _a1, _a2 -func (_m *Client) BalanceAt(_a0 context.Context, _a1 common.Address, _a2 *big.Int) (*big.Int, error) { - ret := _m.Called(_a0, _a1, _a2) - - if len(ret) == 0 { - panic("no return value specified for BalanceAt") - } - - var r0 *big.Int - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) (*big.Int, error)); ok { - return rf(_a0, _a1, _a2) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) *big.Int); ok { - r0 = rf(_a0, _a1, _a2) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*big.Int) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Address, *big.Int) error); ok { - r1 = rf(_a0, _a1, _a2) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BlockByHash provides a mock function with given fields: _a0, _a1 -func (_m *Client) BlockByHash(_a0 context.Context, _a1 common.Hash) (*types.Block, error) { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for BlockByHash") - } - - var r0 *types.Block - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Block, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) *types.Block); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Block) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Hash) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BlockByNumber provides a mock function with given fields: _a0, _a1 -func (_m *Client) BlockByNumber(_a0 context.Context, _a1 *big.Int) (*types.Block, error) { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for BlockByNumber") - } - - var r0 *types.Block - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *big.Int) (*types.Block, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, *big.Int) *types.Block); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Block) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *big.Int) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// CallContract provides a mock function with given fields: _a0, _a1, _a2 -func (_m *Client) CallContract(_a0 context.Context, _a1 interfaces.CallMsg, _a2 *big.Int) ([]byte, error) { - ret := _m.Called(_a0, _a1, _a2) - - if len(ret) == 0 { - panic("no return value specified for CallContract") - } - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, interfaces.CallMsg, *big.Int) ([]byte, error)); ok { - return rf(_a0, _a1, _a2) - } - if rf, ok := ret.Get(0).(func(context.Context, interfaces.CallMsg, *big.Int) []byte); ok { - r0 = rf(_a0, _a1, _a2) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, interfaces.CallMsg, *big.Int) error); ok { - r1 = rf(_a0, _a1, _a2) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ChainID provides a mock function with given fields: _a0 -func (_m *Client) ChainID(_a0 context.Context) (*big.Int, error) { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for ChainID") - } - - var r0 *big.Int - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(context.Context) *big.Int); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*big.Int) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// EstimateGas provides a mock function with given fields: _a0, _a1 -func (_m *Client) EstimateGas(_a0 context.Context, _a1 interfaces.CallMsg) (uint64, error) { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for EstimateGas") - } - - var r0 uint64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, interfaces.CallMsg) (uint64, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, interfaces.CallMsg) uint64); ok { - r0 = rf(_a0, _a1) - } else { - r0 = ret.Get(0).(uint64) - } - - if rf, ok := ret.Get(1).(func(context.Context, interfaces.CallMsg) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetContractInfo provides a mock function with given fields: _a0, _a1 -func (_m *Client) GetContractInfo(_a0 common.Address, _a1 bool) (string, uint8, error) { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for GetContractInfo") - } - - var r0 string - var r1 uint8 - var r2 error - if rf, ok := ret.Get(0).(func(common.Address, bool) (string, uint8, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(common.Address, bool) string); ok { - r0 = rf(_a0, _a1) - } else { - r0 = ret.Get(0).(string) - } - - if rf, ok := ret.Get(1).(func(common.Address, bool) uint8); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Get(1).(uint8) - } - - if rf, ok := ret.Get(2).(func(common.Address, bool) error); ok { - r2 = rf(_a0, _a1) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// GetNetworkName provides a mock function with given fields: _a0, _a1 -func (_m *Client) GetNetworkName(_a0 context.Context, _a1 ...rpc.Option) (string, error) { - _va := make([]interface{}, len(_a1)) - for _i := range _a1 { - _va[_i] = _a1[_i] - } - var _ca []interface{} - _ca = append(_ca, _a0) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for GetNetworkName") - } - - var r0 string - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ...rpc.Option) (string, error)); ok { - return rf(_a0, _a1...) - } - if rf, ok := ret.Get(0).(func(context.Context, ...rpc.Option) string); ok { - r0 = rf(_a0, _a1...) - } else { - r0 = ret.Get(0).(string) - } - - if rf, ok := ret.Get(1).(func(context.Context, ...rpc.Option) error); ok { - r1 = rf(_a0, _a1...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// HeaderByHash provides a mock function with given fields: _a0, _a1 -func (_m *Client) HeaderByHash(_a0 context.Context, _a1 common.Hash) (*types.Header, error) { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for HeaderByHash") - } - - var r0 *types.Header - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Header, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) *types.Header); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Header) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Hash) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// HeaderByNumber provides a mock function with given fields: _a0, _a1 -func (_m *Client) HeaderByNumber(_a0 context.Context, _a1 *big.Int) (*types.Header, error) { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for HeaderByNumber") - } - - var r0 *types.Header - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *big.Int) (*types.Header, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, *big.Int) *types.Header); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Header) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *big.Int) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// IsBootstrapped provides a mock function with given fields: _a0, _a1, _a2 -func (_m *Client) IsBootstrapped(_a0 context.Context, _a1 string, _a2 ...rpc.Option) (bool, error) { - _va := make([]interface{}, len(_a2)) - for _i := range _a2 { - _va[_i] = _a2[_i] - } - var _ca []interface{} - _ca = append(_ca, _a0, _a1) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for IsBootstrapped") - } - - var r0 bool - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, ...rpc.Option) (bool, error)); ok { - return rf(_a0, _a1, _a2...) - } - if rf, ok := ret.Get(0).(func(context.Context, string, ...rpc.Option) bool); ok { - r0 = rf(_a0, _a1, _a2...) - } else { - r0 = ret.Get(0).(bool) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, ...rpc.Option) error); ok { - r1 = rf(_a0, _a1, _a2...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NonceAt provides a mock function with given fields: _a0, _a1, _a2 -func (_m *Client) NonceAt(_a0 context.Context, _a1 common.Address, _a2 *big.Int) (uint64, error) { - ret := _m.Called(_a0, _a1, _a2) - - if len(ret) == 0 { - panic("no return value specified for NonceAt") - } - - var r0 uint64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) (uint64, error)); ok { - return rf(_a0, _a1, _a2) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) uint64); ok { - r0 = rf(_a0, _a1, _a2) - } else { - r0 = ret.Get(0).(uint64) - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Address, *big.Int) error); ok { - r1 = rf(_a0, _a1, _a2) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Peers provides a mock function with given fields: _a0, _a1 -func (_m *Client) Peers(_a0 context.Context, _a1 ...rpc.Option) ([]info.Peer, error) { - _va := make([]interface{}, len(_a1)) - for _i := range _a1 { - _va[_i] = _a1[_i] - } - var _ca []interface{} - _ca = append(_ca, _a0) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for Peers") - } - - var r0 []info.Peer - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ...rpc.Option) ([]info.Peer, error)); ok { - return rf(_a0, _a1...) - } - if rf, ok := ret.Get(0).(func(context.Context, ...rpc.Option) []info.Peer); ok { - r0 = rf(_a0, _a1...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]info.Peer) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, ...rpc.Option) error); ok { - r1 = rf(_a0, _a1...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Peers_v1_11 provides a mock function with given fields: _a0, _a1, _a2 -func (_m *Client) Peers_v1_11(_a0 context.Context, _a1 []ids.NodeID, _a2 ...rpc.Option) ([]client.Peer_v1_11, error) { - _va := make([]interface{}, len(_a2)) - for _i := range _a2 { - _va[_i] = _a2[_i] - } - var _ca []interface{} - _ca = append(_ca, _a0, _a1) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for Peers_v1_11") - } - - var r0 []client.Peer_v1_11 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, []ids.NodeID, ...rpc.Option) ([]client.Peer_v1_11, error)); ok { - return rf(_a0, _a1, _a2...) - } - if rf, ok := ret.Get(0).(func(context.Context, []ids.NodeID, ...rpc.Option) []client.Peer_v1_11); ok { - r0 = rf(_a0, _a1, _a2...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]client.Peer_v1_11) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, []ids.NodeID, ...rpc.Option) error); ok { - r1 = rf(_a0, _a1, _a2...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// SendTransaction provides a mock function with given fields: _a0, _a1 -func (_m *Client) SendTransaction(_a0 context.Context, _a1 *types.Transaction) error { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for SendTransaction") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *types.Transaction) error); ok { - r0 = rf(_a0, _a1) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// SuggestGasPrice provides a mock function with given fields: _a0 -func (_m *Client) SuggestGasPrice(_a0 context.Context) (*big.Int, error) { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for SuggestGasPrice") - } - - var r0 *big.Int - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(context.Context) *big.Int); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*big.Int) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// TraceBlockByHash provides a mock function with given fields: _a0, _a1 -func (_m *Client) TraceBlockByHash(_a0 context.Context, _a1 string) ([]*client.Call, [][]*client.FlatCall, error) { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for TraceBlockByHash") - } - - var r0 []*client.Call - var r1 [][]*client.FlatCall - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string) ([]*client.Call, [][]*client.FlatCall, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, string) []*client.Call); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*client.Call) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string) [][]*client.FlatCall); ok { - r1 = rf(_a0, _a1) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).([][]*client.FlatCall) - } - } - - if rf, ok := ret.Get(2).(func(context.Context, string) error); ok { - r2 = rf(_a0, _a1) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// TraceTransaction provides a mock function with given fields: _a0, _a1 -func (_m *Client) TraceTransaction(_a0 context.Context, _a1 string) (*client.Call, []*client.FlatCall, error) { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for TraceTransaction") - } - - var r0 *client.Call - var r1 []*client.FlatCall - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*client.Call, []*client.FlatCall, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, string) *client.Call); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*client.Call) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string) []*client.FlatCall); ok { - r1 = rf(_a0, _a1) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).([]*client.FlatCall) - } - } - - if rf, ok := ret.Get(2).(func(context.Context, string) error); ok { - r2 = rf(_a0, _a1) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// TransactionByHash provides a mock function with given fields: _a0, _a1 -func (_m *Client) TransactionByHash(_a0 context.Context, _a1 common.Hash) (*types.Transaction, bool, error) { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for TransactionByHash") - } - - var r0 *types.Transaction - var r1 bool - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Transaction, bool, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) *types.Transaction); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Transaction) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Hash) bool); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Get(1).(bool) - } - - if rf, ok := ret.Get(2).(func(context.Context, common.Hash) error); ok { - r2 = rf(_a0, _a1) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// TransactionReceipt provides a mock function with given fields: _a0, _a1 -func (_m *Client) TransactionReceipt(_a0 context.Context, _a1 common.Hash) (*types.Receipt, error) { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for TransactionReceipt") - } - - var r0 *types.Receipt - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Receipt, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) *types.Receipt); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Receipt) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Hash) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// TxPoolContent provides a mock function with given fields: _a0 -func (_m *Client) TxPoolContent(_a0 context.Context) (*client.TxPoolContent, error) { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for TxPoolContent") - } - - var r0 *client.TxPoolContent - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (*client.TxPoolContent, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(context.Context) *client.TxPoolContent); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*client.TxPoolContent) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewClient(t interface { - mock.TestingT - Cleanup(func()) -}) *Client { - mock := &Client{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/server/postman/Avalanche Rosetta.postman_collection.json b/server/postman/Avalanche Rosetta.postman_collection.json new file mode 100644 index 0000000..e66ca2a --- /dev/null +++ b/server/postman/Avalanche Rosetta.postman_collection.json @@ -0,0 +1,3693 @@ +{ + "info": { + "_postman_id": "afdd0b91-b049-4c38-9410-fdaaff7f275c", + "name": "Avalanche Rosetta", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Network", + "item": [ + { + "name": "P Chain", + "item": [ + { + "name": "/network/options", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/network/options", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "network", + "options" + ] + } + }, + "response": [] + }, + { + "name": "/network/status", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/network/status", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "network", + "status" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "C Chain", + "item": [ + { + "name": "/network/options", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/network/options", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "network", + "options" + ] + } + }, + "response": [] + }, + { + "name": "/network/status", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/network/status", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "network", + "status" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "/network/list", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/network/list", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "network", + "list" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Construction", + "item": [ + { + "name": "Derive", + "item": [ + { + "name": "P Chain", + "item": [ + { + "name": "/construction/derive", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"public_key\": {\n \"hex_bytes\": \"{{PublicKey}}\",\n \"curve_type\": \"secp256k1\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/derive", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "derive" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "C Chain", + "item": [ + { + "name": "/construction/derive", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"public_key\": {\n \"hex_bytes\": \"{{PublicKey}}\",\n \"curve_type\": \"secp256k1\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/derive", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "derive" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "C Chain Bech32", + "item": [ + { + "name": "/construction/derive", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"Fuji\"\n },\n \"public_key\": {\n \"hex_bytes\": \"{{PublicKey}}\",\n \"curve_type\": \"secp256k1\"\n },\n \"metadata\": {\n \"address_format\": \"bech32\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/derive", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "derive" + ] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "P Chain", + "item": [ + { + "name": "ExportAvax", + "item": [ + { + "name": "/construction/preprocess", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const requestBody = JSON.parse(pm.request.body.raw);", + "Utils.set(pm.environment, EnvVar.Operations, requestBody.operations);", + "", + "const response = pm.response.json();", + "Utils.set(pm.environment, EnvVar.Options, response.options);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"operations\": [\n {\n \"operation_identifier\": {\n \"index\": 0\n },\n \"type\": \"EXPORT_AVAX\",\n \"account\": {\n \"address\": \"{{PChainAddress}}\"\n },\n \"amount\": {\n \"value\": \"-1000000000\",\n \"currency\": {\n \"symbol\": \"AVAX\",\n \"decimals\": 9\n }\n },\n \"coin_change\": {\n \"coin_action\": \"coin_spent\",\n \"coin_identifier\": {\n \"identifier\": \"NGcWaGCzBUtUsD85wDuX1DwbHFkvMHwJ9tDFiN7HCCnVcB9B8:0\"\n }\n },\n \"metadata\": {\n \"type\": \"INPUT\"\n }\n },\n {\n \"operation_identifier\": {\n \"index\": 1\n },\n \"type\": \"EXPORT_AVAX\",\n \"account\": {\n \"address\": \"{{CChainBech32Address}}\"\n },\n \"amount\": {\n \"value\": \"8000000\",\n \"currency\": {\n \"symbol\": \"AVAX\",\n \"decimals\": 9\n }\n },\n \"metadata\": {\n \"type\": \"EXPORT\"\n }\n }\n ],\n \"metadata\": {\n \"destination_chain\": \"C\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/preprocess", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "preprocess" + ] + } + }, + "response": [] + }, + { + "name": "/construction/metadata", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.Metadata, response.metadata);", + "Utils.set(pm.environment, EnvVar.SuggestedFee, response.suggested_fee[0]);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('options', Utils.get(pm.environment, EnvVar.Options));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"options\": {{options}},\n \"public_keys\": [\n {\n \"hex_bytes\": \"{{PublicKey}}\",\n \"curve_type\": \"secp256k1\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/metadata?", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "metadata" + ], + "query": [ + { + "key": "", + "value": null + } + ] + } + }, + "response": [] + }, + { + "name": "/construction/payloads", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.UnsignedTx, response.unsigned_transaction);", + "Utils.set(pm.environment, EnvVar.SigningPayloads, response.payloads);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('operations', Utils.get(pm.environment, EnvVar.Operations));", + "pm.variables.set('metadata', Utils.get(pm.environment, EnvVar.Metadata));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"operations\": {{operations}},\n \"metadata\": {{metadata}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/payloads", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "payloads" + ] + } + }, + "response": [] + }, + { + "name": "/construction/parse (unsigned)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.UnsignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"signed\": false,\n \"transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/parse", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "parse" + ] + } + }, + "response": [] + }, + { + "name": "test signing utility - /sign", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.Signatures, response.signatures);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SigningPayloads));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"payloads\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{SignerEndpoint}}/sign", + "host": [ + "{{SignerEndpoint}}" + ], + "path": [ + "sign" + ] + } + }, + "response": [] + }, + { + "name": "/construction/combine", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.SignedTx, response.signed_transaction);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.UnsignedTx));", + "pm.variables.set('signatures', Utils.get(pm.environment, EnvVar.Signatures));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"unsigned_transaction\": {{transaction}},\n \"signatures\": {{signatures}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/combine", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "combine" + ] + } + }, + "response": [] + }, + { + "name": "/construction/parse (signed)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"signed\": true,\n \"transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/parse", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "parse" + ] + } + }, + "response": [] + }, + { + "name": "/construction/hash", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"signed_transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/hash", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "hash" + ] + } + }, + "response": [] + }, + { + "name": "/construction/submit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"signed_transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/submit", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "submit" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "// This is used to namespace environment variables used for passing data around, not used in any Rosetta requests.", + "pm.environment.set('requestType', 'p-chain-export')", + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "ImportAvax", + "item": [ + { + "name": "/construction/preprocess", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const requestBody = JSON.parse(pm.request.body.raw);", + "Utils.set(pm.environment, EnvVar.Operations, requestBody.operations);", + "", + "const response = pm.response.json();", + "Utils.set(pm.environment, EnvVar.Options, response.options);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"operations\": [\n {\n \"operation_identifier\": {\n \"index\": 0\n },\n \"type\": \"IMPORT_AVAX\",\n \"account\": {\n \"address\": \"{{CChainBech32Address}}\"\n },\n \"amount\": {\n \"value\": \"-1001000000\",\n \"currency\": {\n \"symbol\": \"AVAX\",\n \"decimals\": 18\n }\n },\n \"coin_change\": {\n \"coin_action\": \"coin_spent\",\n \"coin_identifier\": {\n \"identifier\": \"qnfiXfiskzghMdzWPEesLV9GZ2DypbEZLKAWmPLigk5U5oAYL:0\"\n }\n },\n \"metadata\": {\n \"type\": \"IMPORT\"\n }\n },\n {\n \"operation_identifier\": {\n \"index\": 1\n },\n \"type\": \"IMPORT_AVAX\",\n \"account\": {\n \"address\": \"{{PChainAddress}}\"\n },\n \"amount\": {\n \"value\": \"1000000000\",\n \"currency\": {\n \"symbol\": \"AVAX\",\n \"decimals\": 9\n }\n },\n \"metadata\": {\n \"type\": \"OUTPUT\"\n }\n }\n ],\n \"metadata\": {\n \"source_chain\": \"C\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/preprocess", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "preprocess" + ] + } + }, + "response": [] + }, + { + "name": "/construction/metadata", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.Metadata, response.metadata);", + "Utils.set(pm.environment, EnvVar.SuggestedFee, response.suggested_fee[0]);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('options', Utils.get(pm.environment, EnvVar.Options));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"options\": {{options}},\n \"public_keys\": [\n {\n \"hex_bytes\": \"{{PublicKey}}\",\n \"curve_type\": \"secp256k1\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/metadata", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "metadata" + ] + } + }, + "response": [] + }, + { + "name": "/construction/payloads", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.UnsignedTx, response.unsigned_transaction);", + "Utils.set(pm.environment, EnvVar.SigningPayloads, response.payloads);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('operations', Utils.get(pm.environment, EnvVar.Operations));", + "pm.variables.set('metadata', Utils.get(pm.environment, EnvVar.Metadata));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"operations\": {{operations}},\n \"metadata\": {{metadata}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/payloads", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "payloads" + ] + } + }, + "response": [] + }, + { + "name": "/construction/parse (unsigned)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.UnsignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"signed\": false,\n \"transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/parse", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "parse" + ] + } + }, + "response": [] + }, + { + "name": "test signing utility - /sign", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.Signatures, response.signatures);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SigningPayloads));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"payloads\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{SignerEndpoint}}/sign", + "host": [ + "{{SignerEndpoint}}" + ], + "path": [ + "sign" + ] + } + }, + "response": [] + }, + { + "name": "/construction/combine", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.SignedTx, response.signed_transaction);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.UnsignedTx));", + "pm.variables.set('signatures', Utils.get(pm.environment, EnvVar.Signatures));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"unsigned_transaction\": {{transaction}},\n \"signatures\": {{signatures}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/combine", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "combine" + ] + } + }, + "response": [] + }, + { + "name": "/construction/parse (signed)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const requestType = pm.environment.get('requestType')", + "", + "const response = pm.response.json();", + "const options = response.options;", + "", + "pm.environment.set(`${requestType}-metadata-options`, JSON.stringify(options, null, 2));", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"signed\": true,\n \"transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/parse", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "parse" + ] + } + }, + "response": [] + }, + { + "name": "/construction/hash", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"signed_transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/hash", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "hash" + ] + } + }, + "response": [] + }, + { + "name": "/construction/submit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"signed_transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/submit", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "submit" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "// This is used to namespace environment variables used for passing data around, not used in any Rosetta requests.", + "pm.environment.set('requestType', 'p-chain-import')", + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "AddValidator", + "item": [ + { + "name": "/construction/preprocess", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const requestBody = JSON.parse(pm.request.body.raw);", + "Utils.set(pm.environment, EnvVar.Operations, requestBody.operations);", + "", + "const response = pm.response.json();", + "Utils.set(pm.environment, EnvVar.Options, response.options);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const startTimestamp = Math.floor(Date.now() / 1000) + 60;", + "const endTimestamp = startTimestamp + 86400 * 2; // 2 day staking period", + "", + "pm.variables.set('startTimestamp', startTimestamp);", + "pm.variables.set('endTimestamp', endTimestamp);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"operations\": [\n {\n \"operation_identifier\": {\n \"index\": 0\n },\n \"type\": \"ADD_VALIDATOR\",\n \"account\": {\n \"address\": \"{{PChainAddress}}\"\n },\n \"amount\": {\n \"value\": \"-2877137500\",\n \"currency\": {\n \"symbol\": \"AVAX\",\n \"decimals\": 9\n }\n },\n \"coin_change\": {\n \"coin_action\": \"coin_spent\",\n \"coin_identifier\": {\n \"identifier\": \"pyQfA1Aq9vLaDETjeQe5DAwVxr2KAYdHg4CHzawmaj9oA6ppn:0\"\n }\n },\n \"metadata\": {\n \"type\": \"INPUT\"\n }\n },\n {\n \"operation_identifier\": {\n \"index\": 1\n },\n \"type\": \"ADD_VALIDATOR\",\n \"account\": {\n \"address\": \"{{PChainAddress}}\"\n },\n \"amount\": {\n \"value\": \"2000000000\",\n \"currency\": {\n \"symbol\": \"AVAX\",\n \"decimals\": 9\n }\n },\n \"metadata\": {\n \"type\": \"STAKE\"\n }\n },\n {\n \"operation_identifier\": {\n \"index\": 2\n },\n \"type\": \"ADD_VALIDATOR\",\n \"account\": {\n \"address\": \"{{PChainAddress}}\"\n },\n \"amount\": {\n \"value\": \"877137500\",\n \"currency\": {\n \"symbol\": \"AVAX\",\n \"decimals\": 9\n }\n },\n \"metadata\": {\n \"type\": \"OUTPUT\"\n }\n }\n ],\n \"metadata\": {\n \"node_id\": \"NodeID-Bvsx89JttQqhqdgwtizAPoVSNW74Xcr2S\",\n\t \"start\": {{startTimestamp}},\n \"end\": {{endTimestamp}},\n \"shares\": 100000\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/preprocess", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "preprocess" + ] + } + }, + "response": [] + }, + { + "name": "/construction/metadata", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.Metadata, response.metadata);", + "Utils.set(pm.environment, EnvVar.SuggestedFee, response.suggested_fee[0]);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('options', Utils.get(pm.environment, EnvVar.Options));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"options\": {{options}},\n \"public_keys\": [\n {\n \"hex_bytes\": \"{{PublicKey}}\",\n \"curve_type\": \"secp256k1\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/metadata", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "metadata" + ] + } + }, + "response": [] + }, + { + "name": "/construction/payloads", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.UnsignedTx, response.unsigned_transaction);", + "Utils.set(pm.environment, EnvVar.SigningPayloads, response.payloads);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('operations', Utils.get(pm.environment, EnvVar.Operations));", + "pm.variables.set('metadata', Utils.get(pm.environment, EnvVar.Metadata));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"operations\": {{operations}},\n \"metadata\": {{metadata}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/payloads", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "payloads" + ] + } + }, + "response": [] + }, + { + "name": "/construction/parse (unsigned)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.UnsignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"signed\": false,\n \"transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/parse", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "parse" + ] + } + }, + "response": [] + }, + { + "name": "test signing utility - /sign", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.Signatures, response.signatures);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SigningPayloads));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"payloads\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{SignerEndpoint}}/sign", + "host": [ + "{{SignerEndpoint}}" + ], + "path": [ + "sign" + ] + } + }, + "response": [] + }, + { + "name": "/construction/combine", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.SignedTx, response.signed_transaction);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.UnsignedTx));", + "pm.variables.set('signatures', Utils.get(pm.environment, EnvVar.Signatures));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"unsigned_transaction\": {{transaction}},\n \"signatures\": {{signatures}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/combine", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "combine" + ] + } + }, + "response": [] + }, + { + "name": "/construction/parse (signed)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const requestType = pm.environment.get('requestType')", + "", + "const response = pm.response.json();", + "const options = response.options;", + "", + "pm.environment.set(`${requestType}-metadata-options`, JSON.stringify(options, null, 2));", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"signed\": true,\n \"transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/parse", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "parse" + ] + } + }, + "response": [] + }, + { + "name": "/construction/hash", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"signed_transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/hash", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "hash" + ] + } + }, + "response": [] + }, + { + "name": "/construction/submit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"signed_transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/submit", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "submit" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "// This is used to namespace environment variables used for passing data around, not used in any Rosetta requests.", + "pm.environment.set('requestType', 'p-chain-add-validator')", + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "AddDelegator", + "item": [ + { + "name": "/construction/preprocess", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const requestBody = JSON.parse(pm.request.body.raw);", + "Utils.set(pm.environment, EnvVar.Operations, requestBody.operations);", + "", + "const response = pm.response.json();", + "Utils.set(pm.environment, EnvVar.Options, response.options);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const startTimestamp = Math.floor(Date.now() / 1000) + 60;", + "const endTimestamp = startTimestamp + 86400 ; // 1 day staking period", + "", + "pm.variables.set('startTimestamp', startTimestamp);", + "pm.variables.set('endTimestamp', endTimestamp);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"operations\": [\n {\n \"operation_identifier\": {\n \"index\": 0\n },\n \"type\": \"ADD_DELEGATOR\",\n \"account\": {\n \"address\": \"{{PChainAddress}}\"\n },\n \"amount\": {\n \"value\": \"-1000000000\",\n \"currency\": {\n \"symbol\": \"AVAX\",\n \"decimals\": 9\n }\n },\n \"coin_change\": {\n \"coin_action\": \"coin_spent\",\n \"coin_identifier\": {\n \"identifier\": \"2c6AtXKfyZLxVKCUs7TS3bZN6uu1VvWC2ebV9RYdAFYDLh5izX:0\"\n }\n },\n \"metadata\": {\n \"type\": \"INPUT\"\n }\n },\n {\n \"operation_identifier\": {\n \"index\": 1\n },\n \"type\": \"ADD_DELEGATOR\",\n \"account\": {\n \"address\": \"{{PChainAddress}}\"\n },\n \"amount\": {\n \"value\": \"1000000000\",\n \"currency\": {\n \"symbol\": \"AVAX\",\n \"decimals\": 9\n }\n },\n \"metadata\": {\n \"type\": \"STAKE\"\n }\n }\n ],\n \"metadata\": {\n \"node_id\": \"NodeID-Bvsx89JttQqhqdgwtizAPoVSNW74Xcr2S\",\n\t \"start\": {{startTimestamp}},\n \"end\": {{endTimestamp}}\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/preprocess", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "preprocess" + ] + } + }, + "response": [] + }, + { + "name": "/construction/metadata", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.Metadata, response.metadata);", + "Utils.set(pm.environment, EnvVar.SuggestedFee, response.suggested_fee[0]);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('options', Utils.get(pm.environment, EnvVar.Options));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"options\": {{options}},\n \"public_keys\": [\n {\n \"hex_bytes\": \"{{PublicKey}}\",\n \"curve_type\": \"secp256k1\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/metadata", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "metadata" + ] + } + }, + "response": [] + }, + { + "name": "/construction/payloads", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.UnsignedTx, response.unsigned_transaction);", + "Utils.set(pm.environment, EnvVar.SigningPayloads, response.payloads);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('operations', Utils.get(pm.environment, EnvVar.Operations));", + "pm.variables.set('metadata', Utils.get(pm.environment, EnvVar.Metadata));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"operations\": {{operations}},\n \"metadata\": {{metadata}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/payloads", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "payloads" + ] + } + }, + "response": [] + }, + { + "name": "/construction/parse (unsigned)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.UnsignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"signed\": false,\n \"transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/parse", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "parse" + ] + } + }, + "response": [] + }, + { + "name": "test signing utility - /sign", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.Signatures, response.signatures);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SigningPayloads));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"payloads\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{SignerEndpoint}}/sign", + "host": [ + "{{SignerEndpoint}}" + ], + "path": [ + "sign" + ] + } + }, + "response": [] + }, + { + "name": "/construction/combine", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.SignedTx, response.signed_transaction);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.UnsignedTx));", + "pm.variables.set('signatures', Utils.get(pm.environment, EnvVar.Signatures));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"unsigned_transaction\": {{transaction}},\n \"signatures\": {{signatures}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/combine", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "combine" + ] + } + }, + "response": [] + }, + { + "name": "/construction/parse (signed)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const requestType = pm.environment.get('requestType')", + "", + "const response = pm.response.json();", + "const options = response.options;", + "", + "pm.environment.set(`${requestType}-metadata-options`, JSON.stringify(options, null, 2));", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"signed\": true,\n \"transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/parse", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "parse" + ] + } + }, + "response": [] + }, + { + "name": "/construction/hash", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"signed_transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/hash", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "hash" + ] + } + }, + "response": [] + }, + { + "name": "/construction/submit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"signed_transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/submit", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "submit" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "// This is used to namespace environment variables used for passing data around, not used in any Rosetta requests.", + "pm.environment.set('requestType', 'p-chain-add-delegator')", + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + } + ] + }, + { + "name": "C Chain Atomic Tx", + "item": [ + { + "name": "Export", + "item": [ + { + "name": "/construction/preprocess", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const requestBody = JSON.parse(pm.request.body.raw);", + "Utils.set(pm.environment, EnvVar.Operations, requestBody.operations);", + "", + "const response = pm.response.json();", + "Utils.set(pm.environment, EnvVar.Options, response.options);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"operations\": [\n {\n \"operation_identifier\": {\n \"index\": 0\n },\n \"type\": \"EXPORT\",\n \"account\": {\n \"address\": \"{{CChainEVMAddress}}\"\n },\n \"amount\": {\n \"value\": \"-1001280750\",\n \"currency\": {\n \"symbol\": \"AVAX\",\n \"decimals\": 9\n }\n },\n \"metadata\": {\n \"type\": \"INPUT\"\n }\n },\n {\n \"operation_identifier\": {\n \"index\": 1\n },\n \"type\": \"EXPORT\",\n \"account\": {\n \"address\": \"{{PChainAddress}}\"\n },\n \"amount\": {\n \"value\": \"1001000000\",\n \"currency\": {\n \"symbol\": \"AVAX\",\n \"decimals\": 9\n }\n },\n \"metadata\": {\n \"type\": \"EXPORT\"\n }\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/preprocess", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "preprocess" + ] + } + }, + "response": [] + }, + { + "name": "/construction/metadata", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.Metadata, response.metadata);", + "Utils.set(pm.environment, EnvVar.SuggestedFee, response.suggested_fee[0]);", + "", + "pm.test('success', pm.response.to.have.status(200))" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('options', Utils.get(pm.environment, EnvVar.Options));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"options\": {{options}},\n \"public_keys\": [\n {\n \"hex_bytes\": \"{{PublicKey}}\",\n \"curve_type\": \"secp256k1\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/metadata?", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "metadata" + ], + "query": [ + { + "key": "", + "value": null + } + ] + } + }, + "response": [] + }, + { + "name": "/construction/payloads", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.UnsignedTx, response.unsigned_transaction);", + "Utils.set(pm.environment, EnvVar.SigningPayloads, response.payloads);", + "", + "pm.test('success', pm.response.to.have.status(200))" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('operations', Utils.get(pm.environment, EnvVar.Operations));", + "pm.variables.set('metadata', Utils.get(pm.environment, EnvVar.Metadata));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"operations\": {{operations}},\n \"metadata\": {{metadata}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/payloads", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "payloads" + ] + } + }, + "response": [] + }, + { + "name": "/construction/parse (unsigned)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.UnsignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"signed\": false,\n \"transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/parse", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "parse" + ] + } + }, + "response": [] + }, + { + "name": "test signing utility - /sign", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.Signatures, response.signatures);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SigningPayloads));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"payloads\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{SignerEndpoint}}/sign", + "host": [ + "{{SignerEndpoint}}" + ], + "path": [ + "sign" + ] + } + }, + "response": [] + }, + { + "name": "/construction/combine", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.SignedTx, response.signed_transaction);", + "", + "pm.test('success', pm.response.to.have.status(200))" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.UnsignedTx));", + "pm.variables.set('signatures', Utils.get(pm.environment, EnvVar.Signatures));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"unsigned_transaction\": {{transaction}},\n \"signatures\": {{signatures}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/combine", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "combine" + ] + } + }, + "response": [] + }, + { + "name": "/construction/parse (signed)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const requestType = pm.environment.get('requestType')", + "", + "const response = pm.response.json();", + "const options = response.options;", + "", + "pm.environment.set(`${requestType}-metadata-options`, JSON.stringify(options, null, 2));", + "", + "pm.test('success', pm.response.to.have.status(200))" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"signed\": true,\n \"transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/parse", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "parse" + ] + } + }, + "response": [] + }, + { + "name": "/construction/hash", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"signed_transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/hash", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "hash" + ] + } + }, + "response": [] + }, + { + "name": "/construction/submit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"signed_transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/submit", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "submit" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "// This is used to namespace environment variables used for passing data around, not used in any Rosetta requests.", + "pm.environment.set('requestType', 'c-chain-export')", + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "Import", + "item": [ + { + "name": "/construction/preprocess", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const requestBody = JSON.parse(pm.request.body.raw);", + "Utils.set(pm.environment, EnvVar.Operations, requestBody.operations);", + "", + "const response = pm.response.json();", + "Utils.set(pm.environment, EnvVar.Options, response.options);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"operations\": [\n {\n \"operation_identifier\": {\n \"index\": 0\n },\n \"type\": \"IMPORT\",\n \"account\": {\n \"address\": \"{{PChainAddress}}\"\n },\n \"coin_change\": {\n \"coin_identifier\": {\n \"identifier\": \"XcmVCgMbsA7jhNc2njE1LZ8tY8EVXv5T2NNXKsMqQYWKg7cro:0\"\n },\n \"coin_action\": \"coin_spent\"\n },\n \"amount\": {\n \"value\": \"-8000000\",\n \"currency\": {\n \"symbol\": \"AVAX\",\n \"decimals\": 9\n }\n }\n },\n {\n \"operation_identifier\": {\n \"index\": 1\n },\n \"type\": \"IMPORT\",\n \"account\": {\n \"address\": \"{{CChainEVMAddress}}\"\n },\n \"amount\": {\n \"value\": \"7719250\",\n \"currency\": {\n \"symbol\": \"AVAX\",\n \"decimals\": 9\n }\n }\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/preprocess", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "preprocess" + ] + } + }, + "response": [] + }, + { + "name": "/construction/metadata", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.Metadata, response.metadata);", + "Utils.set(pm.environment, EnvVar.SuggestedFee, response.suggested_fee[0]);", + "", + "pm.test('success', pm.response.to.have.status(200))" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('options', Utils.get(pm.environment, EnvVar.Options));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"options\": {{options}},\n \"public_keys\": [\n {\n \"hex_bytes\": \"{{PublicKey}}\",\n \"curve_type\": \"secp256k1\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/metadata?", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "metadata" + ], + "query": [ + { + "key": "", + "value": null + } + ] + } + }, + "response": [] + }, + { + "name": "/construction/payloads", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.UnsignedTx, response.unsigned_transaction);", + "Utils.set(pm.environment, EnvVar.SigningPayloads, response.payloads);", + "", + "pm.test('success', pm.response.to.have.status(200))" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('operations', Utils.get(pm.environment, EnvVar.Operations));", + "pm.variables.set('metadata', Utils.get(pm.environment, EnvVar.Metadata));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"operations\": {{operations}},\n \"metadata\": {{metadata}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/payloads", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "payloads" + ] + } + }, + "response": [] + }, + { + "name": "/construction/parse (unsigned)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.UnsignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"signed\": false,\n \"transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/parse", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "parse" + ] + } + }, + "response": [] + }, + { + "name": "test signing utility - /sign", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.Signatures, response.signatures);", + "", + "pm.test('success', pm.response.to.have.status(200))", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SigningPayloads));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"payloads\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{SignerEndpoint}}/sign", + "host": [ + "{{SignerEndpoint}}" + ], + "path": [ + "sign" + ] + } + }, + "response": [] + }, + { + "name": "/construction/combine", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "Utils.set(pm.environment, EnvVar.SignedTx, response.signed_transaction);", + "", + "pm.test('success', pm.response.to.have.status(200))" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.UnsignedTx));", + "pm.variables.set('signatures', Utils.get(pm.environment, EnvVar.Signatures));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"unsigned_transaction\": {{transaction}},\n \"signatures\": {{signatures}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/combine", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "combine" + ] + } + }, + "response": [] + }, + { + "name": "/construction/parse (signed)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const requestType = pm.environment.get('requestType')", + "", + "const response = pm.response.json();", + "const options = response.options;", + "", + "pm.environment.set(`${requestType}-metadata-options`, JSON.stringify(options, null, 2));", + "", + "pm.test('success', pm.response.to.have.status(200))" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"signed\": true,\n \"transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/parse", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "parse" + ] + } + }, + "response": [] + }, + { + "name": "/construction/hash", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"signed_transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/hash", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "hash" + ] + } + }, + "response": [] + }, + { + "name": "/construction/submit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('success', pm.response.to.have.status(200))" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.variables.set('transaction', Utils.get(pm.environment, EnvVar.SignedTx));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"signed_transaction\": {{transaction}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/construction/submit", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "construction", + "submit" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "// This is used to namespace environment variables used for passing data around, not used in any Rosetta requests.", + "pm.environment.set('requestType', 'c-chain-import')", + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "debug = false", + "", + "Utils = {};", + "Utils.set = (env, name, value) => {", + " const requestType = env.get('requestType');", + "", + " const fullName = `${requestType}-${name}`;", + " const strValue = JSON.stringify(value, null, 2);", + " env.set(fullName, strValue);", + " debug && console.log(`Set ${fullName} to:`, strValue);", + "}", + "", + "Utils.get = (env, name) => {", + " const requestType = env.get('requestType');", + "", + " const fullName = `${requestType}-${name}`;", + " const value = env.get(fullName);", + " debug && console.log(`Received ${fullName}:`, value);", + " return value;", + "}", + "", + "", + "EnvVar = {", + " Operations: 'operations',", + " RequestParams: 'request-params',", + " PreprocessMetadata: 'preprocess-metadata',", + " Options: 'options',", + " Metadata: 'metadata',", + " SuggestedFee: 'suggested-fee',", + " UnsignedTx: 'unsigned-tx',", + " SigningPayloads: 'signing-payloads',", + " SignedTx: 'signed-tx',", + "}", + "", + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "Block", + "item": [ + { + "name": "P Chain", + "item": [ + { + "name": "/block by hash", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"block_identifier\": {\n \"hash\": \"iT4PgDmwhvyxZCrUzLTFfAwdR7Z6E4YyfRDdX1MvgmTTebSFT\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/block", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "block" + ] + } + }, + "response": [] + }, + { + "name": "/block by index", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"block_identifier\": {\n \"index\": 76543\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/block", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "block" + ] + } + }, + "response": [] + }, + { + "name": "/block/transaction", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"block_identifier\": {\n \"hash\": \"iT4PgDmwhvyxZCrUzLTFfAwdR7Z6E4YyfRDdX1MvgmTTebSFT\"\n },\n \"transaction_identifier\": {\n \"hash\": \"3jnHJZ2TWrZHiFN2FTg1ASxTuGwfqL9JMexKAkRuEcYNFE59K\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/block/transaction", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "block", + "transaction" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "C Chain", + "item": [ + { + "name": "/block by hash", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"block_identifier\": {\n \"hash\": \"0x5d4143e2d5282056412fec2b4a5899f34abd411c52b2860a975f7e0f21d4cbce\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/block", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "block" + ] + } + }, + "response": [] + }, + { + "name": "/block by index", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"block_identifier\": {\n \"index\": 12121212\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/block", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "block" + ] + } + }, + "response": [] + }, + { + "name": "/block/transaction", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"block_identifier\": {\n \"hash\": \"0x5d4143e2d5282056412fec2b4a5899f34abd411c52b2860a975f7e0f21d4cbce\"\n },\n \"transaction_identifier\": {\n \"hash\": \"0xbfdd4353dafbfacc43df18945fdefbfc374b6e224f96b7711749b306b316ce19\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/block/transaction", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "block", + "transaction" + ] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Account", + "item": [ + { + "name": "P Chain", + "item": [ + { + "name": "/account/balance", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"account_identifier\": {\n \"address\": \"{{PChainAddress}}\"\n },\n \"include_mempool\": true,\n \"currencies\": [\n {\n \"symbol\": \"AVAX\",\n \"decimals\": 9\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/account/balance", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "account", + "balance" + ] + } + }, + "response": [] + }, + { + "name": "/account/coins", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"account_identifier\": {\n \"address\": \"{{PChainAddress}}\"\n },\n \"currencies\": [\n {\n \"symbol\": \"AVAX\",\n \"decimals\": 9\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/account/coins", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "account", + "coins" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "C Chain", + "item": [ + { + "name": "/account/balance", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"account_identifier\": {\n \"address\": \"{{CChainEVMAddress}}\"\n },\n \"currencies\": [\n {\n \"symbol\": \"AVAX\",\n \"decimals\": 18\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/account/balance", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "account", + "balance" + ] + } + }, + "response": [] + }, + { + "name": "/account/coins", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"account_identifier\": {\n \"address\": \"{{CChainEVMAddress}}\"\n },\n \"currencies\": [\n {\n \"symbol\": \"AVAX\",\n \"decimals\": 18\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/account/coins", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "account", + "coins" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "C Chain Atomic Tx", + "item": [ + { + "name": "/account/balance", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\",\n \"sub_network_identifier\": {\n \"network\": \"P\"\n }\n },\n \"account_identifier\": {\n \"address\": \"{{CChainBech32Address}}\"\n },\n \"include_mempool\": true,\n \"currencies\": [\n {\n \"symbol\": \"AVAX\",\n \"decimals\": 9\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/account/balance", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "account", + "balance" + ] + } + }, + "response": [] + }, + { + "name": "/account/coins", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"Avalanche\",\n \"network\": \"{{Network}}\"\n },\n \"account_identifier\": {\n \"address\": \"{{CChainBech32Address}}\"\n },\n \"currencies\": [\n {\n \"symbol\": \"AVAX\",\n \"decimals\": 9\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Endpoint}}/account/coins", + "host": [ + "{{Endpoint}}" + ], + "path": [ + "account", + "coins" + ] + } + }, + "response": [] + } + ] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "Endpoint", + "value": "http://localhost:8080", + "type": "default" + }, + { + "key": "Network", + "value": "Fuji", + "type": "default" + }, + { + "key": "SignerEndpoint", + "value": "http://localhost:9898", + "type": "default" + }, + { + "key": "PublicKey", + "value": "038722f82a94ec3f730ea1e8863a4b63f4898442e6f93723fbdd7589d8b700fff9", + "type": "string" + }, + { + "key": "CChainEVMAddress", + "value": "0x3158e80abD5A1e1aa716003C9Db096792C379621", + "type": "string" + }, + { + "key": "CChainBech32Address", + "value": "C-fuji1wmd9dfrqpud6daq0cde47u0r7pkrr46ep60399", + "type": "string" + }, + { + "key": "PChainAddress", + "value": "P-fuji1wmd9dfrqpud6daq0cde47u0r7pkrr46ep60399", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/server/postman/README.md b/server/postman/README.md new file mode 100644 index 0000000..d1cfa66 --- /dev/null +++ b/server/postman/README.md @@ -0,0 +1,26 @@ +# Postman Collection + +During development, you can invoke the individual Rosetta endpoints using the Postman collection in `Avalanche-Rosetta.postman-collection.json`. + +The folder also contains a little utility that can be used to sign transactions using a provided private key. + +## Running transaction test signing utility + +```sh +go run postman/test_signing_server.go --private-key PrivateKey-abcd123... +``` + +## Data and Construction Derive Endpoints + +Both the data endpoints and construction derive endpoint requests are simple Postman requests. + +They are under Network, Block and Account folders in the collection, grouped by chain, and can be executed as-is. + +## Construction Endpoints +Construction Endpoints use Postman pre-request script and tests features to chain outputs of requests to one another. + +Requests can be found under `Construction` folder; grouped by chains first, then by transaction type. + +The operations from the `/construction/process` request body are copied to the corresponding `/construction/payloads` request automatically. + +Once the operations and preprocess metadata is set, the collection run feature of Postman can be used to execute the full transaction construction and broadcast, provided the test signing server is up and running as well. diff --git a/server/postman/test_signing_server.go b/server/postman/test_signing_server.go new file mode 100644 index 0000000..07d6f69 --- /dev/null +++ b/server/postman/test_signing_server.go @@ -0,0 +1,149 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "log" + "net/http" + "strings" + "time" + + "github.com/ava-labs/avalanchego/utils/cb58" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/coinbase/rosetta-sdk-go/types" +) + +type signRequest struct { + Payloads []*types.SigningPayload `json:"payloads"` +} + +type signResponse struct { + Signatures []*types.Signature `json:"signatures"` +} + +type signingServer struct { + port uint + privateKey *secp256k1.PrivateKey + publicKey *secp256k1.PublicKey +} + +func (s *signingServer) SignBytes(address string, bytes []byte) (*types.Signature, error) { + signatureBytes, err := s.privateKey.SignHash(bytes) + if err != nil { + return nil, err + } + + return &types.Signature{ + SigningPayload: &types.SigningPayload{ + AccountIdentifier: &types.AccountIdentifier{Address: address}, + Bytes: bytes, + SignatureType: types.EcdsaRecovery, + }, + PublicKey: &types.PublicKey{ + Bytes: s.publicKey.Bytes(), + CurveType: types.Secp256k1, + }, + SignatureType: types.EcdsaRecovery, + Bytes: signatureBytes, + }, nil +} + +func (s *signingServer) signHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + d := json.NewDecoder(r.Body) + signPayload := signRequest{} + err := d.Decode(&signPayload) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + log.Printf("Received signing request with %d payloads\n", len(signPayload.Payloads)) + + var signatures []*types.Signature + + for _, payload := range signPayload.Payloads { + signature, err := s.SignBytes(payload.AccountIdentifier.Address, payload.Bytes) + if err != nil { + http.Error(w, "unable to sign payload", http.StatusInternalServerError) + return + } + + signatures = append(signatures, signature) + } + + resp := signResponse{Signatures: signatures} + bytes, err := json.Marshal(resp) + if err != nil { + http.Error(w, "unable to marshal response", http.StatusInternalServerError) + return + } + + _, _ = w.Write(bytes) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func (s *signingServer) run() { + http.HandleFunc("/sign", s.signHandler) + + addr := fmt.Sprintf("localhost:%d", s.port) + log.Printf("Listening on %s\n", addr) + + server := &http.Server{ + Addr: addr, + ReadHeaderTimeout: 30 * time.Second, + } + + if err := server.ListenAndServe(); err != nil { + log.Fatal(err) + } +} + +func parsePrivateKeyString(keyStr string) (*secp256k1.PrivateKey, *secp256k1.PublicKey, error) { + parts := strings.Split(keyStr, "-") + pkBytes, err := cb58.Decode(parts[1]) + if err != nil { + return nil, nil, err + } + key, err := secp256k1.ToPrivateKey(pkBytes) + if err != nil { + return nil, nil, err + } + + pk := key.PublicKey() + return key, pk, nil +} + +func newSigningServer(port uint, privateKeyStr string) *signingServer { + privateKey, publicKey, err := parsePrivateKeyString(privateKeyStr) + if err != nil { + log.Fatal(err) + } + + return &signingServer{ + privateKey: privateKey, + publicKey: publicKey, + port: port, + } +} + +var opts struct { + port uint + privateKey string +} + +func init() { + flag.UintVar(&opts.port, "port", 9898, "signing server port") + flag.StringVar(&opts.privateKey, "private-key", "", "private key to sign with") + flag.Parse() +} + +func main() { + println("!!! DO NOT USE THIS UTILITY WITH PRIVATE KEYS THAT HAS ACCESS TO REAL FUNDS!!!\n") + println("This is a test utility used to help testing Rosetta AVAX implementation during development\n\n") + signingServer := newSigningServer(opts.port, opts.privateKey) + signingServer.run() +} diff --git a/server/rosetta-cli-conf/costwo/avalanche_p.ros b/server/rosetta-cli-conf/costwo/avalanche_p.ros new file mode 100644 index 0000000..581d0ec --- /dev/null +++ b/server/rosetta-cli-conf/costwo/avalanche_p.ros @@ -0,0 +1,69 @@ +c2p(10){ + c_chain_export{ + print_message({ + "adsf":"before we start" + }); + c_chain_export.network = {"network":"Fuji", "blockchain":"Avalanche", "sub_network_identifier": {"network": "P"}}; + currency = {"symbol":"AVAX", "decimals":18}; + sender = find_balance({ + "minimum_balance":{ + "value": "0", + "currency": {{currency}} + }, + "require_coin": true + }); + + + print_message({ + "sender":{{sender}} + }); + + fee = "1000000"; // 0.001 AVAX + sender_amount = 0 - {{sender.balance.value}}; + output_amount = {{sender.balance.value}} - {{fee}}; + + c_chain_export.confirmation_depth = "1"; + // c_chain_export.dry_run = true; + c_chain_export.operations = [ + { + "operation_identifier":{ + "index":0 + }, + "type":"IMPORT_AVAX", + "account": { + "address": "C-fuji1tupmzrrvpwn4nujq2j9rtdsl6fxaxueuul6u38" + }, + "amount":{ + "value":{{sender_amount}}, + "currency":{{currency}} + }, + "coin_change":{"coin_action":"coin_spent", "coin_identifier":{{sender.coin}}}, + "metadata":{ + "type":"IMPORT", + "sig_indices":[ + 0 + ] + } + }, + { + "operation_identifier":{ + "index":1 + }, + "type":"IMPORT_AVAX", + "account": {{sender.account_identifier}}, + "amount":{ + "value":{{output_amount}}, + "currency":{{currency}} + }, + "metadata":{ + "type":"OUTPUT" + } + } + ]; + c_chain_export.preprocess_metadata = {"source_chain": "C"}; + + print_message({ + "c_chain_export operations": {{c_chain_export.operations}} + }); + } +} \ No newline at end of file diff --git a/server/rosetta-cli-conf/costwo/avalanche_unwrap.ros b/server/rosetta-cli-conf/costwo/avalanche_unwrap.ros new file mode 100644 index 0000000..60aedb2 --- /dev/null +++ b/server/rosetta-cli-conf/costwo/avalanche_unwrap.ros @@ -0,0 +1,104 @@ +request_funds(1){ + find_account{ + currency = {"symbol":"AVAX", "decimals":18}; + + erc20_currency = {"symbol":"WETH.e", "decimals":18, "metadata": {"contractAddress": "0x7fCDc2C1EF3e4A0bCC8155a558bB20a7218f2b05"}}; + random_account = find_balance({ + "minimum_balance":{ + "value": "0", + "currency": {{currency}} + }, + "create_limit":1 + }); + }, + + // Create a separate scenario to request funds so that + // the address we are using to request funds does not + // get rolled back if funds do not yet exist. + request{ + loaded_account = find_balance({ + "account_identifier": {{random_account.account_identifier}}, + "minimum_balance":{ + "value": "10000000000000000", // 0.01 AVAX + "currency": {{currency}} + } + }); + + loaded_account2 = find_balance({ + "account_identifier": {{random_account.account_identifier}}, + "minimum_balance":{ + "value": "100000000000000", // 0.0001 WETH.e + "currency": {{erc20_currency}} + } + }); + } +} + +create_account(1){ + create{ + network = {"network":"Fuji", "blockchain":"Avalanche"}; + key = generate_key({"curve_type": "secp256k1"}); + account = derive({ + "network_identifier": {{network}}, + "public_key": {{key.public_key}} + }); + + // If the account is not saved, the key will be lost! + save_account({ + "account_identifier": {{account.account_identifier}}, + "keypair": {{key}} + }); + } +} + +simple_unwrap(1){ + transfer{ + transfer.network = {"network":"Fuji", "blockchain":"Avalanche"}; + avax_currency = {"symbol":"AVAX", "decimals":18}; + + currency = {"symbol":"WETH.e", "decimals":18, "metadata": {"contractAddress": "0x7fCDc2C1EF3e4A0bCC8155a558bB20a7218f2b05"}}; + sender = find_balance({ + "minimum_balance":{ + "value": "100000000000000", // 0.0001 WETH.e + "currency": {{currency}} + } + }); + unused = find_balance({ + "account_identifier": {{sender.account_identifier}}, + "minimum_balance":{ + "value": "10000000000000000", // 0.01 AVAX + "currency": {{avax_currency}} + } + }); + + recipient_amount = random_number({"minimum": "1", "maximum": {{sender.balance.value}}}); + print_message({"recipient_amount":{{recipient_amount}}}); + + // Find recipient and construct operations + sender_amount = 0 - {{recipient_amount}}; + recipient = find_balance({ + "not_account_identifier":[{{sender.account_identifier}}], + "minimum_balance":{ + "value": "0", + "currency": {{currency}} + }, + "create_limit": 100, + "create_probability": 50 + }); + transfer.preprocess_metadata = { + "bridge_unwrap": true + }; + transfer.confirmation_depth = "1"; + transfer.operations = [ + { + "operation_identifier":{"index":0}, + "type":"ERC20_BURN", + "account":{{sender.account_identifier}}, + "amount":{ + "value":{{sender_amount}}, + "currency":{{currency}} + } + } + ]; + } +} diff --git a/server/rosetta-cli-conf/costwo/config.json b/server/rosetta-cli-conf/costwo/config.json index 2bee9b4..ac470ad 100644 --- a/server/rosetta-cli-conf/costwo/config.json +++ b/server/rosetta-cli-conf/costwo/config.json @@ -24,9 +24,9 @@ }, "prefunded_accounts": [ { - "privkey": "", + "privkey": "private-key-for-that-address", "account_identifier": { - "address": "0xcb00566863A90DBa5fE56e9a7e4Cf310c24e6506" + "address": "public-address-prefunded-with-faucet" }, "curve_type": "secp256k1", "currency": { diff --git a/server/rosetta-cli-conf/costwo/config_unwrap.json b/server/rosetta-cli-conf/costwo/config_unwrap.json new file mode 100644 index 0000000..084a9a7 --- /dev/null +++ b/server/rosetta-cli-conf/costwo/config_unwrap.json @@ -0,0 +1,53 @@ +{ + "network": { + "blockchain": "Avalanche", + "network": "Fuji" + }, + "online_url": "http://localhost:8080", + "http_timeout": 500, + "max_retries": 50, + "retry_elapsed_time": 0, + "max_online_connections": 500, + "max_sync_concurrency": 128, + "tip_delay": 3600, + "log_configuration": false, + "compression_disabled": false, + "memory_limit_disabled": false, + "data_directory": "rosetta-data", + "construction": { + "offline_url": "http://localhost:8080", + "stale_depth": 5, + "broadcast_limit": 5, + "constructor_dsl_file": "./avalanche_unwrap.ros", + "end_conditions": { + "simple_unwrap": 1 + }, + "prefunded_accounts": [] + }, + "data": { + "active_reconciliation_concurrency": 16, + "inactive_reconciliation_concurrency": 4, + "inactive_reconciliation_frequency": 250, + "initial_balance_fetch_disabled": false, + "log_blocks": false, + "log_transactions": false, + "log_balance_changes": false, + "log_reconciliations": false, + "ignore_reconciliation_error": false, + "exempt_accounts": "", + "bootstrap_balances": "", + "interesting_accounts": "", + "reconciliation_disabled": false, + "balance_tracking_disabled": false, + "historical_balance_enabled": true, + "coin_tracking_disabled": false, + "results_output_file": "", + "end_conditions": { + "reconciliation_coverage": { + "coverage": 0.95, + "from_tip": true, + "tip": true + } + } + } + } \ No newline at end of file diff --git a/server/rosetta-cli-conf/costwo/costwo.ros b/server/rosetta-cli-conf/costwo/costwo.ros index feaff25..57cd4c4 100644 --- a/server/rosetta-cli-conf/costwo/costwo.ros +++ b/server/rosetta-cli-conf/costwo/costwo.ros @@ -55,7 +55,7 @@ transfer(10){ // Set the recipient_amount as some value <= sender.balance-max_fee max_fee = "4725000000000000"; // 0.004725 FLR available_amount = {{sender.balance.value}} - {{max_fee}}; - recipient_amount = random_number({"minimum": "1", "maximum": {{available_amount}}}); + recipient_amount = random_number({"minimum": "1", "maximum": "10000"}); print_message({"recipient_amount":{{recipient_amount}}}); // Find recipient and construct operations diff --git a/server/rosetta-cli-conf/costwo/server-config.json b/server/rosetta-cli-conf/costwo/server-config.json index 690e492..adb9d8e 100644 --- a/server/rosetta-cli-conf/costwo/server-config.json +++ b/server/rosetta-cli-conf/costwo/server-config.json @@ -1,6 +1,6 @@ { "mode": "online", - "rpc_endpoint": "http://127.0.0.1:9650", + "rpc_base_url": "http://127.0.0.1:9650", "network_name": "testnet", "genesis_block_hash": "0xc47d9c5d19d9cde5316780d1b0896ce2f20a0bc09c9ce2c86fbfafc0742b1e63", "chain_id": 114 diff --git a/server/rosetta-cli-conf/flare/exempt_accounts.json b/server/rosetta-cli-conf/flare/exempt_accounts.json index ca0e583..1963338 100644 --- a/server/rosetta-cli-conf/flare/exempt_accounts.json +++ b/server/rosetta-cli-conf/flare/exempt_accounts.json @@ -52,5 +52,32 @@ "symbol": "FLR", "decimals": 18 } + }, + { + "account_identifier": { + "address": "0xbe653C54DF337F13Fcb726101388F4a4803049F3" + }, + "currency": { + "symbol": "FLR", + "decimals": 18 + } + }, + { + "account_identifier": { + "address": "0xF09fc2658270ac5494fa365dc5e29E0a2c636413" + }, + "currency": { + "symbol": "FLR", + "decimals": 18 + } + }, + { + "account_identifier": { + "address": "0xbe653C54DF337F13Fcb726101388F4a4803049F3" + }, + "currency": { + "symbol": "FLR", + "decimals": 18 + } } ] \ No newline at end of file diff --git a/server/rosetta-cli-conf/flare/server-config.json b/server/rosetta-cli-conf/flare/server-config.json index 8d2e10b..ca678c5 100644 --- a/server/rosetta-cli-conf/flare/server-config.json +++ b/server/rosetta-cli-conf/flare/server-config.json @@ -1,6 +1,6 @@ { "mode": "online", - "rpc_endpoint": "http://127.0.0.1:9650", + "rpc_base_url": "http://127.0.0.1:9650", "network_name": "mainnet", "genesis_block_hash": "0xf501834f1cfce08939acb0feadb11ca0a94d806c5bedb6700a771fc27d2f1068", "chain_id": 14 diff --git a/server/rosetta-cli-conf/localflare/server-config.json b/server/rosetta-cli-conf/localflare/server-config.json index 14a5d30..ba6b4d9 100644 --- a/server/rosetta-cli-conf/localflare/server-config.json +++ b/server/rosetta-cli-conf/localflare/server-config.json @@ -1,6 +1,6 @@ { "mode": "online", - "rpc_endpoint": "http://127.0.0.1:9650", + "rpc_base_url": "http://127.0.0.1:9650", "network_name": "localflare", "genesis_block_hash": "0xc5d448ab94a1aafc9c18ad314ee306cb87d1f7030136d4eb072b145d772fd00c", "chain_id": 162 diff --git a/server/scripts/mock.gen.sh b/server/scripts/mock.gen.sh new file mode 100755 index 0000000..854bee4 --- /dev/null +++ b/server/scripts/mock.gen.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if ! [[ "$0" =~ scripts/mock.gen.sh ]]; then + echo "must be run from repository root" + exit 255 +fi + +# https://github.com/uber-go/mock +go install -v go.uber.org/mock/mockgen@v0.4.0 + +outputted_files=() + +# tuples of (source interface import path, comma-separated interface names, output file path) +input="scripts/mocks.mockgen.txt" +while IFS= read -r line +do + IFS='=' read -r src_import_path interface_name output_path <<< "${line}" + package_name="$(basename "$(dirname "$output_path")")" + echo "Generating ${output_path}..." + outputted_files+=("${output_path}") + mockgen -package="${package_name}" -destination="${output_path}" "${src_import_path}" "${interface_name}" + +done < "$input" + +mapfile -t all_generated_files < <(grep -Rl 'Code generated by MockGen. DO NOT EDIT.') + +# Exclude certain files +outputted_files+=('scripts/mock.gen.sh') # This file + +mapfile -t diff_files < <(echo "${all_generated_files[@]}" "${outputted_files[@]}" | tr ' ' '\n' | sort | uniq -u) + +if (( ${#diff_files[@]} )); then + printf "\nFAILURE\n" + echo "Detected MockGen generated files that are not in scripts/mocks.mockgen.txt:" + printf "%s\n" "${diff_files[@]}" + exit 255 +fi + +echo "SUCCESS" diff --git a/server/scripts/mocks.mockgen.txt b/server/scripts/mocks.mockgen.txt new file mode 100644 index 0000000..489b6aa --- /dev/null +++ b/server/scripts/mocks.mockgen.txt @@ -0,0 +1,3 @@ +github.com/ava-labs/avalanche-rosetta/client=Client,PChainClient=client/mock_client.go +github.com/ava-labs/avalanche-rosetta/service=AccountBackend,ConstructionBackend=service/mock_service.go +github.com/ava-labs/avalanche-rosetta/service/backend/pchain/indexer=Parser=service/backend/pchain/indexer/mock_parser.go diff --git a/server/service/backend/cchainatomictx/account.go b/server/service/backend/cchainatomictx/account.go new file mode 100644 index 0000000..b9f8df9 --- /dev/null +++ b/server/service/backend/cchainatomictx/account.go @@ -0,0 +1,194 @@ +package cchainatomictx + +import ( + "context" + "errors" + "fmt" + "math/big" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/utils/math" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/mapper" + "github.com/ava-labs/avalanche-rosetta/service" + "github.com/ava-labs/avalanche-rosetta/service/backend/common" +) + +var ( + errUnableToParseUTXO = errors.New("unable to parse UTXO") + errUnableToGetUTXOOutput = errors.New("unable to get UTXO output") +) + +// AccountBalance implements /account/balance endpoint for C-chain atomic transaction balance +// +// This endpoint is called if the account is in Bech32 format for a C-chain request +func (b *Backend) AccountBalance(ctx context.Context, req *types.AccountBalanceRequest) (*types.AccountBalanceResponse, *types.Error) { + if req.AccountIdentifier == nil { + return nil, service.WrapError(service.ErrInvalidInput, "account identifier is not provided") + } + blockIdentifier, coins, wrappedErr := b.getAccountCoins(ctx, req.AccountIdentifier.Address) + if wrappedErr != nil { + return nil, wrappedErr + } + + var balanceValue uint64 + + for _, coin := range coins { + amountValue, err := types.AmountValue(coin.Amount) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, "unable to extract amount from UTXO") + } + + balanceValue, err = math.Add64(balanceValue, amountValue.Uint64()) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, "overflow while calculating balance") + } + } + + balance := mapper.AtomicAvaxAmount(new(big.Int).SetUint64(balanceValue)) + return &types.AccountBalanceResponse{ + BlockIdentifier: blockIdentifier, + Balances: []*types.Amount{balance}, + }, nil +} + +// AccountCoins implements /account/coins endpoint for C-chain atomic transaction UTXOs +// +// This endpoint is called if the account is in Bech32 format for a C-chain request +func (b *Backend) AccountCoins(ctx context.Context, req *types.AccountCoinsRequest) (*types.AccountCoinsResponse, *types.Error) { + if req.AccountIdentifier == nil { + return nil, service.WrapError(service.ErrInvalidInput, "account identifier is not provided") + } + blockIdentifier, coins, wrappedErr := b.getAccountCoins(ctx, req.AccountIdentifier.Address) + if wrappedErr != nil { + return nil, wrappedErr + } + + return &types.AccountCoinsResponse{ + BlockIdentifier: blockIdentifier, + Coins: common.SortUnique(coins), + }, nil +} + +func (b *Backend) getAccountCoins(ctx context.Context, address string) (*types.BlockIdentifier, []*types.Coin, *types.Error) { + var coins []*types.Coin + sourceChains := []constants.ChainIDAlias{ + constants.PChain, + constants.XChain, + } + + preHeader, err := b.cClient.HeaderByNumber(ctx, nil) + if err != nil { + return nil, nil, service.WrapError(service.ErrInternalError, err) + } + + for _, chain := range sourceChains { + chainCoins, wrappedErr := b.fetchCoinsFromChain(ctx, address, chain) + if wrappedErr != nil { + return nil, nil, wrappedErr + } + coins = append(coins, chainCoins...) + } + + postHeader, err := b.cClient.HeaderByNumber(ctx, nil) + if err != nil { + return nil, nil, service.WrapError(service.ErrInternalError, err) + } + + // Since there is no API to return coins and block height at the time of query, we lookup block height before and after + // and fail the request if theyd differ since it means we don't know which block the coins are at + if preHeader.Number.Cmp(postHeader.Number) != 0 { + return nil, nil, service.WrapError(service.ErrInternalError, "new block received while fetching coins") + } + + blockIdentifier := &types.BlockIdentifier{ + Index: postHeader.Number.Int64(), + Hash: postHeader.Hash().String(), + } + + return blockIdentifier, coins, nil +} + +func (b *Backend) fetchCoinsFromChain(ctx context.Context, prefixAddress string, sourceChain constants.ChainIDAlias) ([]*types.Coin, *types.Error) { + var ( + coins []*types.Coin + + // Used for pagination + lastUtxoAddress ids.ShortID + lastUtxoID ids.ID + ) + + // Parse [prefixAddress] + chainIDAlias, _, addressBytes, err := address.Parse(prefixAddress) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, err) + } + if chainIDAlias != constants.CChain.String() { + return nil, service.WrapError( + service.ErrInternalError, + fmt.Errorf("invalid ChainID alias wanted=%s have=%s", constants.CChain.String(), chainIDAlias), + ) + } + addr, err := ids.ToShortID(addressBytes) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, err) + } + + // GetUTXOs controlled by [addr] + for { + utxos, newUtxoAddress, newUtxoID, err := b.cClient.GetAtomicUTXOs(ctx, []ids.ShortID{addr}, sourceChain.String(), b.getUTXOsPageSize, lastUtxoAddress, lastUtxoID) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, "unable to get UTXOs") + } + + // convert raw UTXO bytes to Rosetta Coins + coinsPage, err := b.processUtxos(sourceChain, utxos) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, err) + } + + coins = append(coins, coinsPage...) + + // Fetch next page only if there may be more UTXOs + if len(utxos) < int(b.getUTXOsPageSize) { + break + } + + lastUtxoAddress = newUtxoAddress + lastUtxoID = newUtxoID + } + + return coins, nil +} + +func (b *Backend) processUtxos(sourceChain constants.ChainIDAlias, utxos [][]byte) ([]*types.Coin, error) { + coins := make([]*types.Coin, 0) + for _, utxoBytes := range utxos { + utxo := avax.UTXO{} + _, err := b.codec.Unmarshal(utxoBytes, &utxo) + if err != nil { + return nil, errUnableToParseUTXO + } + + transferableOut, ok := utxo.Out.(avax.TransferableOut) + if !ok { + return nil, errUnableToGetUTXOOutput + } + + amount := mapper.AtomicAvaxAmount(new(big.Int).SetUint64(transferableOut.Amount())) + amount.Metadata = map[string]interface{}{ + "source_chain": sourceChain.String(), + } + + coin := &types.Coin{ + CoinIdentifier: &types.CoinIdentifier{Identifier: utxo.UTXOID.String()}, + Amount: amount, + } + coins = append(coins, coin) + } + return coins, nil +} diff --git a/server/service/backend/cchainatomictx/account_test.go b/server/service/backend/cchainatomictx/account_test.go new file mode 100644 index 0000000..36763e3 --- /dev/null +++ b/server/service/backend/cchainatomictx/account_test.go @@ -0,0 +1,164 @@ +package cchainatomictx + +import ( + "context" + "math/big" + "strconv" + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/coinbase/rosetta-sdk-go/types" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/mapper" + + ethtypes "github.com/ava-labs/coreth/core/types" +) + +type utxo struct { + id string + amount uint64 +} + +func (u *utxo) InputID() ids.ID { + uid, err := avax.UTXOIDFromString(u.id) + if err != nil { + panic(err) + } + return uid.InputID() +} + +var utxos = []utxo{ + {"23CLURk1Czf1aLui1VdcuWSiDeFskfp3Sn8TQG7t6NKfeQRYDj:2", 1_000_000}, + {"2QmMXKS6rKQMnEh2XYZ4ZWCJmy8RpD3LyVZWxBG25t4N1JJqxY:1", 1_500_000}, + {"2QmMXKS6rKQMnEh2XYZ4ZWCJmy8RpD3LyVZWxBG25t4N1JJqxY:1", 1_500_000}, // duplicate + {"23CLURk1Czf1aLui1VdcuWSiDeFskfp3Sn8TQG7t6NKfeQRYDj:4", 2_000_000}, // out of order +} + +var blockHeader = ðtypes.Header{ + Number: big.NewInt(42), +} + +func TestAccountBalance(t *testing.T) { + ctrl := gomock.NewController(t) + evmMock := client.NewMockClient(ctrl) + backend := NewBackend(evmMock, ids.Empty, avalancheNetworkID) + accountAddress := "C-fuji15f9g0h5xkr5cp47n6u3qxj6yjtzzzrdr23a3tl" + _, _, addressBytes, err := address.Parse(accountAddress) + require.NoError(t, err) + addr, err := ids.ToShortID(addressBytes) + require.NoError(t, err) + + t.Run("C-chain atomic tx balance is sum of UTXOs", func(t *testing.T) { + require := require.New(t) + + utxo0Bytes := makeUtxoBytes(t, backend, utxos[0].id, utxos[0].amount) + utxo1Bytes := makeUtxoBytes(t, backend, utxos[1].id, utxos[1].amount) + + utxos := [][]byte{utxo0Bytes, utxo1Bytes} + + var nilBigInt *big.Int + evmMock.EXPECT().HeaderByNumber(gomock.Any(), nilBigInt).Return(blockHeader, nil).Times(2) + evmMock.EXPECT(). + GetAtomicUTXOs(gomock.Any(), []ids.ShortID{addr}, constants.PChain.String(), backend.getUTXOsPageSize, ids.ShortEmpty, ids.Empty). + Return(utxos, ids.ShortEmpty, ids.Empty, nil) + evmMock.EXPECT(). + GetAtomicUTXOs(gomock.Any(), []ids.ShortID{addr}, constants.XChain.String(), backend.getUTXOsPageSize, ids.ShortEmpty, ids.Empty). + Return(nil, ids.ShortEmpty, ids.Empty, nil) + + resp, terr := backend.AccountBalance(context.Background(), &types.AccountBalanceRequest{ + NetworkIdentifier: &types.NetworkIdentifier{}, + AccountIdentifier: &types.AccountIdentifier{ + Address: accountAddress, + }, + }) + require.Nil(terr) + + require.Len(resp.Balances, 1) + require.Equal(mapper.AtomicAvaxCurrency, resp.Balances[0].Currency) + require.Equal("2500000", resp.Balances[0].Value) + }) +} + +func TestAccountCoins(t *testing.T) { + ctrl := gomock.NewController(t) + evmMock := client.NewMockClient(ctrl) + backend := NewBackend(evmMock, ids.Empty, avalancheNetworkID) + // changing page size to 2 to test pagination as well + backend.getUTXOsPageSize = 2 + accountAddress := "C-fuji15f9g0h5xkr5cp47n6u3qxj6yjtzzzrdr23a3tl" + _, _, addressBytes, err := address.Parse(accountAddress) + require.NoError(t, err) + addr, err := ids.ToShortID(addressBytes) + require.NoError(t, err) + + t.Run("C-chain atomic tx coins returns UTXOs", func(t *testing.T) { + require := require.New(t) + + utxo0Bytes := makeUtxoBytes(t, backend, utxos[0].id, utxos[0].amount) + utxo1Bytes := makeUtxoBytes(t, backend, utxos[1].id, utxos[1].amount) + utxo2Bytes := makeUtxoBytes(t, backend, utxos[2].id, utxos[2].amount) + utxo3Bytes := makeUtxoBytes(t, backend, utxos[3].id, utxos[3].amount) + + var nilBigInt *big.Int + evmMock.EXPECT().HeaderByNumber(gomock.Any(), nilBigInt).Return(blockHeader, nil).Times(2) + evmMock.EXPECT(). + GetAtomicUTXOs(gomock.Any(), []ids.ShortID{addr}, constants.PChain.String(), backend.getUTXOsPageSize, ids.ShortEmpty, ids.Empty). + Return([][]byte{utxo0Bytes, utxo1Bytes}, addr, utxos[1].InputID(), nil) + evmMock.EXPECT(). + GetAtomicUTXOs(gomock.Any(), []ids.ShortID{addr}, constants.PChain.String(), backend.getUTXOsPageSize, addr, utxos[1].InputID()). + Return([][]byte{utxo2Bytes, utxo3Bytes}, addr, utxos[3].InputID(), nil) + evmMock.EXPECT(). + GetAtomicUTXOs(gomock.Any(), []ids.ShortID{addr}, constants.PChain.String(), backend.getUTXOsPageSize, addr, utxos[3].InputID()). + Return([][]byte{utxo3Bytes}, addr, utxos[3].InputID(), nil) + evmMock.EXPECT(). + GetAtomicUTXOs(gomock.Any(), []ids.ShortID{addr}, constants.XChain.String(), backend.getUTXOsPageSize, ids.ShortEmpty, ids.Empty). + Return(nil, ids.ShortEmpty, ids.Empty, nil) + + resp, terr := backend.AccountCoins(context.Background(), &types.AccountCoinsRequest{ + NetworkIdentifier: &types.NetworkIdentifier{}, + AccountIdentifier: &types.AccountIdentifier{ + Address: accountAddress, + }, + }) + require.Nil(terr) + + require.Len(resp.Coins, 3) + + require.Equal(utxos[0].id, resp.Coins[0].CoinIdentifier.Identifier) + require.Equal(mapper.AtomicAvaxCurrency, resp.Coins[0].Amount.Currency) + require.Equal(strconv.FormatUint(utxos[0].amount, 10), resp.Coins[0].Amount.Value) + + require.Equal(utxos[3].id, resp.Coins[1].CoinIdentifier.Identifier) + require.Equal(mapper.AtomicAvaxCurrency, resp.Coins[1].Amount.Currency) + require.Equal(strconv.FormatUint(utxos[3].amount, 10), resp.Coins[1].Amount.Value) + + require.Equal(utxos[1].id, resp.Coins[2].CoinIdentifier.Identifier) + require.Equal(mapper.AtomicAvaxCurrency, resp.Coins[2].Amount.Currency) + require.Equal(strconv.FormatUint(utxos[1].amount, 10), resp.Coins[2].Amount.Value) + }) +} + +func makeUtxoBytes(t *testing.T, backend *Backend, utxoIDStr string, amount uint64) []byte { + utxoID, err := mapper.DecodeUTXOID(utxoIDStr) + if err != nil { + t.Fail() + return nil + } + + utxoBytes, err := backend.codec.Marshal(backend.codecVersion, &avax.UTXO{ + UTXOID: *utxoID, + Out: &secp256k1fx.TransferOutput{Amt: amount}, + }) + if err != nil { + t.Fail() + } + + return utxoBytes +} diff --git a/server/service/backend/cchainatomictx/backend.go b/server/service/backend/cchainatomictx/backend.go new file mode 100644 index 0000000..7f87d9b --- /dev/null +++ b/server/service/backend/cchainatomictx/backend.go @@ -0,0 +1,73 @@ +package cchainatomictx + +import ( + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/coreth/plugin/evm" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/mapper" + "github.com/ava-labs/avalanche-rosetta/service" + + cmapper "github.com/ava-labs/avalanche-rosetta/mapper/cchainatomictx" +) + +var ( + _ service.ConstructionBackend = &Backend{} + _ service.AccountBackend = &Backend{} +) + +type Backend struct { + avalancheNetworkID uint32 + cClient client.Client + getUTXOsPageSize uint32 + codec codec.Manager + codecVersion uint16 + avaxAssetID ids.ID +} + +// NewBackend creates a C-chain atomic transaction service backend +func NewBackend(cClient client.Client, avaxAssetID ids.ID, avalancheNetworkID uint32) *Backend { + return &Backend{ + avalancheNetworkID: avalancheNetworkID, + cClient: cClient, + getUTXOsPageSize: 1024, + codec: evm.Codec, + codecVersion: 0, + avaxAssetID: avaxAssetID, + } +} + +// ShouldHandleRequest returns whether a given request should be handled by this backend +func (b *Backend) ShouldHandleRequest(req interface{}) bool { + switch r := req.(type) { + case *types.AccountBalanceRequest: + return cmapper.IsCChainBech32Address(r.AccountIdentifier) + case *types.AccountCoinsRequest: + return cmapper.IsCChainBech32Address(r.AccountIdentifier) + case *types.ConstructionDeriveRequest: + return r.Metadata[mapper.MetadataAddressFormat] == mapper.AddressFormatBech32 + case *types.ConstructionMetadataRequest: + return r.Options[cmapper.MetadataAtomicTxGas] != nil + case *types.ConstructionPreprocessRequest: + return cmapper.IsAtomicOpType(r.Operations[0].Type) + case *types.ConstructionPayloadsRequest: + return cmapper.IsAtomicOpType(r.Operations[0].Type) + case *types.ConstructionParseRequest: + return b.isCchainAtomicTx(r.Transaction) + case *types.ConstructionCombineRequest: + return b.isCchainAtomicTx(r.UnsignedTransaction) + case *types.ConstructionHashRequest: + return b.isCchainAtomicTx(r.SignedTransaction) + case *types.ConstructionSubmitRequest: + return b.isCchainAtomicTx(r.SignedTransaction) + } + + return false +} + +func (b *Backend) isCchainAtomicTx(transaction string) bool { + _, err := b.parsePayloadTxFromString(transaction) + return err == nil +} diff --git a/server/service/backend/cchainatomictx/backend_test.go b/server/service/backend/cchainatomictx/backend_test.go new file mode 100644 index 0000000..94162ff --- /dev/null +++ b/server/service/backend/cchainatomictx/backend_test.go @@ -0,0 +1,146 @@ +package cchainatomictx + +import ( + "testing" + + "github.com/ava-labs/coreth/plugin/evm" + "github.com/coinbase/rosetta-sdk-go/types" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/mapper" + "github.com/ava-labs/avalanche-rosetta/service" + + cmapper "github.com/ava-labs/avalanche-rosetta/mapper/cchainatomictx" +) + +func TestShouldHandleRequest(t *testing.T) { + cChainNetworkIdentifier := &types.NetworkIdentifier{ + Blockchain: service.BlockchainName, + Network: constants.TestnetNetwork, + } + + bech32AccountIdentifier := &types.AccountIdentifier{Address: "C-avax1us3us4s4mv0g85vxjm8va04ewdl27wcwnqwejf"} + evmAccountIdentifier := &types.AccountIdentifier{Address: "0x30cE0c38f953eE9CD5fbc247e63DE68D3263144b"} + + atomicOperations := []*types.Operation{ + {Type: mapper.OpImport}, + } + + evmOperations := []*types.Operation{ + {Type: mapper.OpCall}, + } + + backend := &Backend{ + codec: evm.Codec, + codecVersion: 0, + } + + atomicTxString := `{"tx":"0x000000000000000000057fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d500000000000000000000000000000000000000000000000000000000000000000000000288ae5dd070e6d74f16c26358cd4a8f43746d4d338b5b75b668741c6d95816af5000000023d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa000000050000000000e4e1c00000000100000000b9a824340e1b94f27500cdfcbf8eaa9d4ee5e57b2823cb8b158de17689916c74000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000500000000004c4b400000000100000000000000013158e80abd5a1e1aa716003c9db096792c37962100000000012c7a123d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa000000020000000900000001a06d20d1d175b1e1d2b6e647ab5321717967de7e9367c28df8c0e20634ec7827019fe38e8d4f123f8e5286f3236db8dbb419e264628e2f17330a6c8da60d3424010000000900000001a06d20d1d175b1e1d2b6e647ab5321717967de7e9367c28df8c0e20634ec7827019fe38e8d4f123f8e5286f3236db8dbb419e264628e2f17330a6c8da60d342401dc68b1fc","signers":[{"coin_identifier":"23CLURk1Czf1aLui1VdcuWSiDeFskfp3Sn8TQG7t6NKfeQRYDj:2","account_identifier":{"address":"P-fuji1wmd9dfrqpud6daq0cde47u0r7pkrr46ep60399"}},{"coin_identifier":"2QmMXKS6rKQMnEh2XYZ4ZWCJmy8RpD3LyVZWxBG25t4N1JJqxY:1","account_identifier":{"address":"P-fuji1wmd9dfrqpud6daq0cde47u0r7pkrr46ep60399"}}]}` + + nonAtomicTxString := "evmtxstring" + + t.Run("return true for c-chain atomic tx requests", func(t *testing.T) { + require := require.New(t) + + require.True(backend.ShouldHandleRequest(&types.AccountBalanceRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + AccountIdentifier: bech32AccountIdentifier, + })) + require.True(backend.ShouldHandleRequest(&types.AccountCoinsRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + AccountIdentifier: bech32AccountIdentifier, + })) + require.True(backend.ShouldHandleRequest(&types.ConstructionDeriveRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + Metadata: map[string]interface{}{ + mapper.MetadataAddressFormat: mapper.AddressFormatBech32, + }, + })) + require.True(backend.ShouldHandleRequest(&types.ConstructionMetadataRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + Options: map[string]interface{}{ + cmapper.MetadataAtomicTxGas: 123, + }, + })) + require.True(backend.ShouldHandleRequest(&types.ConstructionPreprocessRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + Operations: atomicOperations, + })) + require.True(backend.ShouldHandleRequest(&types.ConstructionPayloadsRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + Operations: atomicOperations, + })) + require.True(backend.ShouldHandleRequest(&types.ConstructionParseRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + Transaction: atomicTxString, + Signed: true, + })) + require.True(backend.ShouldHandleRequest(&types.ConstructionCombineRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + UnsignedTransaction: atomicTxString, + })) + require.True(backend.ShouldHandleRequest(&types.ConstructionHashRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + SignedTransaction: atomicTxString, + })) + require.True(backend.ShouldHandleRequest(&types.ConstructionSubmitRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + SignedTransaction: atomicTxString, + })) + }) + + t.Run("return false for other c-chain requests", func(t *testing.T) { + require := require.New(t) + + require.False(backend.ShouldHandleRequest(&types.AccountBalanceRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + AccountIdentifier: evmAccountIdentifier, + })) + require.False(backend.ShouldHandleRequest(&types.AccountCoinsRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + AccountIdentifier: evmAccountIdentifier, + })) + require.False(backend.ShouldHandleRequest(&types.ConstructionDeriveRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + })) + require.False(backend.ShouldHandleRequest(&types.ConstructionMetadataRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + })) + require.False(backend.ShouldHandleRequest(&types.ConstructionPreprocessRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + Operations: evmOperations, + })) + require.False(backend.ShouldHandleRequest(&types.ConstructionPayloadsRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + Operations: evmOperations, + })) + require.False(backend.ShouldHandleRequest(&types.ConstructionParseRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + Transaction: nonAtomicTxString, + })) + require.False(backend.ShouldHandleRequest(&types.ConstructionCombineRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + UnsignedTransaction: nonAtomicTxString, + })) + require.False(backend.ShouldHandleRequest(&types.ConstructionHashRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + SignedTransaction: nonAtomicTxString, + })) + require.False(backend.ShouldHandleRequest(&types.ConstructionSubmitRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + SignedTransaction: nonAtomicTxString, + })) + + // Backend does not support /block and /network endpoints + require.False(backend.ShouldHandleRequest(&types.BlockRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + })) + require.False(backend.ShouldHandleRequest(&types.BlockTransactionRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + })) + require.False(backend.ShouldHandleRequest(&types.NetworkRequest{ + NetworkIdentifier: cChainNetworkIdentifier, + })) + }) +} diff --git a/server/service/backend/cchainatomictx/construction.go b/server/service/backend/cchainatomictx/construction.go new file mode 100644 index 0000000..831f5ff --- /dev/null +++ b/server/service/backend/cchainatomictx/construction.go @@ -0,0 +1,335 @@ +package cchainatomictx + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math/big" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/coreth/plugin/evm" + "github.com/coinbase/rosetta-sdk-go/parser" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/mapper" + "github.com/ava-labs/avalanche-rosetta/service" + "github.com/ava-labs/avalanche-rosetta/service/backend/common" + + cmapper "github.com/ava-labs/avalanche-rosetta/mapper/cchainatomictx" + ethcommon "github.com/ethereum/go-ethereum/common" +) + +var ( + errUnknownTxType = errors.New("unknown tx type") + errUndecodableTx = errors.New("undecodable transaction") +) + +// ConstructionDerive implements /construction/derive endpoint for C-chain atomic transactions +func (*Backend) ConstructionDerive(_ context.Context, req *types.ConstructionDeriveRequest) (*types.ConstructionDeriveResponse, *types.Error) { + return common.DeriveBech32Address(constants.CChain, req) +} + +// ConstructionPreprocess implements /construction/preprocess endpoint for C-chain atomic transactions +func (b *Backend) ConstructionPreprocess( + _ context.Context, + req *types.ConstructionPreprocessRequest, +) (*types.ConstructionPreprocessResponse, *types.Error) { + matches, err := common.MatchOperations(req.Operations) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + firstIn, _ := matches[0].First() + firstOut, _ := matches[1].First() + + if firstIn == nil || firstOut == nil { + return nil, service.WrapError(service.ErrInvalidInput, "both input and output operations must be specified") + } + + gasUsed, err := b.estimateGasUsed(firstIn.Type, matches) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + preprocessOptions := cmapper.Options{ + AtomicTxGas: new(big.Int).SetUint64(gasUsed), + } + + switch firstIn.Type { + case mapper.OpImport: + v, ok := req.Metadata[cmapper.MetadataSourceChain] + if !ok { + return nil, service.WrapError(service.ErrInvalidInput, "source_chain metadata must be provided") + } + chainAlias, ok := v.(string) + if !ok { + return nil, service.WrapError(service.ErrInvalidInput, "invalid source_chain value") + } + + preprocessOptions.SourceChain = chainAlias + case mapper.OpExport: + chain, _, _, err := address.Parse(firstOut.Account.Address) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, err) + } + + preprocessOptions.From = firstIn.Account.Address + preprocessOptions.DestinationChain = chain + + if v, ok := req.Metadata[cmapper.MetadataNonce]; ok { + stringObj, ok := v.(string) + if !ok { + return nil, service.WrapError(service.ErrInvalidInput, fmt.Errorf("%s is not a valid nonce string", v)) + } + bigObj, ok := new(big.Int).SetString(stringObj, 10) + if !ok { + return nil, service.WrapError(service.ErrInvalidInput, fmt.Errorf("%s is not a valid nonce", v)) + } + preprocessOptions.Nonce = bigObj + } + } + + optionsMap, err := mapper.MarshalJSONMap(preprocessOptions) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, err) + } + + return &types.ConstructionPreprocessResponse{ + Options: optionsMap, + }, nil +} + +func (b *Backend) estimateGasUsed(opType string, matches []*parser.Match) (uint64, error) { + // building tx with dummy data to get byte size for fee estimate + tx, _, err := cmapper.BuildTx( + opType, + matches, + cmapper.Metadata{ + SourceChainID: &ids.Empty, + DestinationChainID: &ids.Empty, + }, + b.codec, + b.avaxAssetID, + ) + if err != nil { + return 0, err + } + + return tx.GasUsed(true) +} + +// ConstructionMetadata implements /construction/metadata endpoint for C-chain atomic transactions +func (b *Backend) ConstructionMetadata( + ctx context.Context, + req *types.ConstructionMetadataRequest, +) (*types.ConstructionMetadataResponse, *types.Error) { + var input cmapper.Options + err := mapper.UnmarshalJSONMap(req.Options, &input) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + cChainID, err := b.cClient.GetBlockchainID(ctx, constants.CChain.String()) + if err != nil { + return nil, service.WrapError(service.ErrClientError, err) + } + + metadata := cmapper.Metadata{ + NetworkID: b.avalancheNetworkID, + CChainID: cChainID, + } + + if input.SourceChain != "" { + id, err := b.cClient.GetBlockchainID(ctx, input.SourceChain) + if err != nil { + return nil, service.WrapError(service.ErrClientError, err) + } + metadata.SourceChainID = &id + } + + if input.DestinationChain != "" { + id, err := b.cClient.GetBlockchainID(ctx, input.DestinationChain) + if err != nil { + return nil, service.WrapError(service.ErrClientError, err) + } + metadata.DestinationChain = input.DestinationChain + metadata.DestinationChainID = &id + } + + if input.From != "" { + var nonce uint64 + if input.Nonce == nil { + nonce, err = b.cClient.NonceAt(ctx, ethcommon.HexToAddress(input.From), nil) + if err != nil { + return nil, service.WrapError(service.ErrClientError, err) + } + } else { + nonce = input.Nonce.Uint64() + } + metadata.Nonce = nonce + } + + metadataMap, err := mapper.MarshalJSONMap(metadata) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, err) + } + + suggestedFeeAmount, err := b.calculateSuggestedFee(ctx, input.AtomicTxGas) + if err != nil { + return nil, service.WrapError(service.ErrClientError, err) + } + + return &types.ConstructionMetadataResponse{ + Metadata: metadataMap, + SuggestedFee: []*types.Amount{ + suggestedFeeAmount, + }, + }, nil +} + +func (b *Backend) calculateSuggestedFee(ctx context.Context, gasUsed *big.Int) (*types.Amount, error) { + baseFee, err := b.cClient.EstimateBaseFee(ctx) + if err != nil { + return nil, err + } + + suggestedFeeEth := new(big.Int).Mul(gasUsed, baseFee) + suggestedFee := new(big.Int).Div(suggestedFeeEth, mapper.X2crate) + return mapper.AtomicAvaxAmount(suggestedFee), nil +} + +// ConstructionPayloads implements /construction/payloads endpoint for C-chain atomic transactions +func (b *Backend) ConstructionPayloads(_ context.Context, req *types.ConstructionPayloadsRequest) (*types.ConstructionPayloadsResponse, *types.Error) { + builder := cAtomicTxBuilder{ + avaxAssetID: b.avaxAssetID, + codec: b.codec, + codecVersion: b.codecVersion, + } + return common.BuildPayloads(builder, req) +} + +// ConstructionParse implements /construction/parse endpoint for C-chain atomic transactions +func (b *Backend) ConstructionParse(_ context.Context, req *types.ConstructionParseRequest) (*types.ConstructionParseResponse, *types.Error) { + rosettaTx, err := b.parsePayloadTxFromString(req.Transaction) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + hrp, err := mapper.GetHRP(req.NetworkIdentifier) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, "incorrect network identifier") + } + + chainIDs := map[ids.ID]string{} + if rosettaTx.DestinationChainID != nil { + chainIDs[*rosettaTx.DestinationChainID] = rosettaTx.DestinationChain + } + + txParser := cAtomicTxParser{ + hrp: hrp, + chainIDs: chainIDs, + } + + return common.Parse(txParser, rosettaTx, req.Signed) +} + +func (b *Backend) parsePayloadTxFromString(transaction string) (*common.RosettaTx, error) { + // Unmarshal input transaction + payloadsTx := &common.RosettaTx{ + Tx: &cAtomicTx{ + Codec: b.codec, + CodecVersion: b.codecVersion, + }, + } + + err := json.Unmarshal([]byte(transaction), payloadsTx) + if err != nil { + return nil, errUndecodableTx + } + + return payloadsTx, payloadsTx.Tx.Initialize() +} + +// ConstructionCombine implements /construction/combine endpoint for C-chain atomic transactions +func (b *Backend) ConstructionCombine(_ context.Context, req *types.ConstructionCombineRequest) (*types.ConstructionCombineResponse, *types.Error) { + rosettaTx, err := b.parsePayloadTxFromString(req.UnsignedTransaction) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + return common.Combine(b, rosettaTx, req.Signatures) +} + +// CombineTx implements C-chain atomic transaction specific logic for combining unsigned transactions and signatures +func (*Backend) CombineTx(tx common.AvaxTx, signatures []*types.Signature) (common.AvaxTx, *types.Error) { + cTx, ok := tx.(*cAtomicTx) + if !ok { + return nil, service.WrapError(service.ErrInvalidInput, "invalid transaction") + } + + creds, err := getTxCreds(cTx.Tx.UnsignedAtomicTx, signatures) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, "unable attach signatures to transaction") + } + + unsignedBytes, err := cTx.Marshal() + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, "unable to encode unsigned transaction") + } + + cTx.Tx.Creds = creds + + signedBytes, err := cTx.Marshal() + if err != nil { + return nil, service.WrapError(service.ErrInternalError, "unable to marshal signed transaction") + } + + cTx.Tx.Initialize(unsignedBytes, signedBytes) + + return cTx, nil +} + +// getTxCreds fetches credentials based on the tx type +func getTxCreds( + unsignedAtomicTx evm.UnsignedAtomicTx, + signatures []*types.Signature, +) ([]verify.Verifiable, error) { + switch uat := unsignedAtomicTx.(type) { + case *evm.UnsignedImportTx: + return common.BuildCredentialList(uat.ImportedInputs, signatures) + case *evm.UnsignedExportTx: + return common.BuildSingletonCredentialList(signatures) + } + + return nil, errUnknownTxType +} + +// ConstructionHash implements /construction/hash endpoint for C-chain atomic transactions +func (b *Backend) ConstructionHash( + _ context.Context, + req *types.ConstructionHashRequest, +) (*types.TransactionIdentifierResponse, *types.Error) { + rosettaTx, err := b.parsePayloadTxFromString(req.SignedTransaction) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + return common.HashTx(rosettaTx) +} + +// ConstructionSubmit implements /construction/submit endpoint for C-chain atomic transactions +func (b *Backend) ConstructionSubmit( + ctx context.Context, + req *types.ConstructionSubmitRequest, +) (*types.TransactionIdentifierResponse, *types.Error) { + rosettaTx, err := b.parsePayloadTxFromString(req.SignedTransaction) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + return common.SubmitTx(ctx, b.cClient, rosettaTx) +} diff --git a/server/service/backend/cchainatomictx/construction_test.go b/server/service/backend/cchainatomictx/construction_test.go new file mode 100644 index 0000000..1bdd9d3 --- /dev/null +++ b/server/service/backend/cchainatomictx/construction_test.go @@ -0,0 +1,493 @@ +package cchainatomictx + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "math/big" + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/coinbase/rosetta-sdk-go/types" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/mapper" + "github.com/ava-labs/avalanche-rosetta/service" + "github.com/ava-labs/avalanche-rosetta/service/backend/common" + + avaconstants "github.com/ava-labs/avalanchego/utils/constants" + ethcommon "github.com/ethereum/go-ethereum/common" +) + +var ( + networkIdentifier = &types.NetworkIdentifier{ + Blockchain: service.BlockchainName, + Network: constants.TestnetNetwork, + } + + cAccountIdentifier = &types.AccountIdentifier{Address: "0x3158e80abD5A1e1aa716003C9Db096792C379621"} + cAccountBech32Identifier = &types.AccountIdentifier{Address: "C-fuji1wmd9dfrqpud6daq0cde47u0r7pkrr46ep60399"} + pAccountIdentifier = &types.AccountIdentifier{Address: "P-fuji1wmd9dfrqpud6daq0cde47u0r7pkrr46ep60399"} + + cChainID, _ = ids.FromString("yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp") + pChainID = ids.Empty + + avalancheNetworkID = avaconstants.FujiID + + avaxAssetID, _ = ids.FromString("U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK") +) + +func buildRosettaSignerJSON(coinIdentifiers []string, signers []*types.AccountIdentifier) string { + importSigners := []*common.Signer{} + for i, s := range signers { + importSigners = append(importSigners, &common.Signer{ + CoinIdentifier: coinIdentifiers[i], + AccountIdentifier: s, + }) + } + bytes, _ := json.Marshal(importSigners) + return string(bytes) +} + +func TestConstructionDerive(t *testing.T) { + backend := NewBackend(nil, ids.Empty, avalancheNetworkID) + + t.Run("c-chain address", func(t *testing.T) { + src := "02e0d4392cfa224d4be19db416b3cf62e90fb2b7015e7b62a95c8cb490514943f6" + b, _ := hex.DecodeString(src) + + resp, err := backend.ConstructionDerive( + context.Background(), + &types.ConstructionDeriveRequest{ + NetworkIdentifier: networkIdentifier, + PublicKey: &types.PublicKey{ + Bytes: b, + CurveType: types.Secp256k1, + }, + }, + ) + require.Nil(t, err) + require.Equal( + t, + "C-fuji15f9g0h5xkr5cp47n6u3qxj6yjtzzzrdr23a3tl", + resp.AccountIdentifier.Address, + ) + }) +} + +func TestExportTxConstruction(t *testing.T) { + nonce := uint64(48) + + opExport := "EXPORT" + + exportOperations := []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{Index: 0}, + RelatedOperations: nil, + Type: opExport, + Account: cAccountIdentifier, + Amount: mapper.AtomicAvaxAmount(big.NewInt(-10_000_000)), + }, + { + OperationIdentifier: &types.OperationIdentifier{Index: 1}, + RelatedOperations: []*types.OperationIdentifier{ + {Index: 0}, + }, + Type: opExport, + Account: pAccountIdentifier, + Amount: mapper.AtomicAvaxAmount(big.NewInt(9_719_250)), + }, + } + + metadataOptions := map[string]interface{}{ + "atomic_tx_gas": 11230., + "from": cAccountIdentifier.Address, + "destination_chain": constants.PChain.String(), + } + + suggestedFeeValue := "280750" + + payloadsMetadata := map[string]interface{}{ + "network_id": float64(avalancheNetworkID), + "c_chain_id": cChainID.String(), + "destination_chain": constants.PChain.String(), + "destination_chain_id": pChainID.String(), + "nonce": float64(nonce), + } + + signers := []*types.AccountIdentifier{cAccountIdentifier} + exportSigners := buildRosettaSignerJSON([]string{""}, signers) + + unsignedExportTx := "0x000000000001000000057fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d50000000000000000000000000000000000000000000000000000000000000000000000013158e80abd5a1e1aa716003c9db096792c37962100000000009896803d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000000000030000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa000000070000000000944dd20000000000000000000000010000000176da56a4600f1ba6f40fc3735f71e3f06c31d7590000000024739402" + unsignedExportTxHash, _ := hex.DecodeString("75afdcba5bf36457ba9edd65b07f40dcd3111d3c98a53550025af931b7500a7b") + + signingPayloads := []*types.SigningPayload{ + { + AccountIdentifier: cAccountIdentifier, + Bytes: unsignedExportTxHash, + SignatureType: types.EcdsaRecovery, + }, + } + + wrappedTxFormat := `{"tx":"%s","signers":%s,"destination_chain":"%s","destination_chain_id":"%s"}` + wrappedUnsignedExportTx := fmt.Sprintf(wrappedTxFormat, unsignedExportTx, exportSigners, constants.PChain.String(), pChainID.String()) + + signedExportTxSignature, _ := hex.DecodeString("2acfc2cedd3c42978728518b13cc84a64f23784af591516e8dfe0cce544bc63c370ca6d64b2550f12f56a800b8a73ff8573131bf54e584de38c91fc14dd7336801") + signedExportTx := "0x000000000001000000057fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d50000000000000000000000000000000000000000000000000000000000000000000000013158e80abd5a1e1aa716003c9db096792c37962100000000009896803d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000000000030000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa000000070000000000944dd20000000000000000000000010000000176da56a4600f1ba6f40fc3735f71e3f06c31d7590000000100000009000000012acfc2cedd3c42978728518b13cc84a64f23784af591516e8dfe0cce544bc63c370ca6d64b2550f12f56a800b8a73ff8573131bf54e584de38c91fc14dd733680149056c11" + signedExportTxHash := "pkSEF4YvVo6YjirHfmWvt9j2zrWdzEAGckDKrbyq1WbPtWdAX" + + signatures := []*types.Signature{ + { + SigningPayload: &types.SigningPayload{ + AccountIdentifier: cAccountIdentifier, + Bytes: unsignedExportTxHash, + SignatureType: types.EcdsaRecovery, + }, + SignatureType: types.EcdsaRecovery, + Bytes: signedExportTxSignature, + }, + } + + wrappedSignedExportTx := fmt.Sprintf(wrappedTxFormat, signedExportTx, exportSigners, constants.PChain.String(), pChainID.String()) + + ctx := context.Background() + ctrl := gomock.NewController(t) + clientMock := client.NewMockClient(ctrl) + backend := NewBackend(clientMock, avaxAssetID, avalancheNetworkID) + + t.Run("preprocess endpoint", func(t *testing.T) { + req := &types.ConstructionPreprocessRequest{ + NetworkIdentifier: networkIdentifier, + Operations: exportOperations, + } + + resp, terr := backend.ConstructionPreprocess(ctx, req) + + require.Nil(t, terr) + require.Equal(t, metadataOptions, resp.Options) + }) + + t.Run("metadata endpoint", func(t *testing.T) { + req := &types.ConstructionMetadataRequest{ + NetworkIdentifier: networkIdentifier, + Options: metadataOptions, + } + + clientMock.EXPECT().GetBlockchainID(ctx, constants.CChain.String()).Return(cChainID, nil) + clientMock.EXPECT().GetBlockchainID(ctx, constants.PChain.String()).Return(pChainID, nil) + clientMock.EXPECT(). + NonceAt(ctx, ethcommon.HexToAddress(cAccountIdentifier.Address), (*big.Int)(nil)). + Return(nonce, nil) + clientMock.EXPECT().EstimateBaseFee(ctx).Return(big.NewInt(25_000_000_000), nil) + + resp, terr := backend.ConstructionMetadata(ctx, req) + + require.Nil(t, terr) + require.Equal(t, payloadsMetadata, resp.Metadata) + require.Equal(t, suggestedFeeValue, resp.SuggestedFee[0].Value) + }) + + t.Run("payloads endpoint", func(t *testing.T) { + req := &types.ConstructionPayloadsRequest{ + NetworkIdentifier: networkIdentifier, + Metadata: payloadsMetadata, + Operations: exportOperations, + } + + resp, terr := backend.ConstructionPayloads(ctx, req) + + require.Nil(t, terr) + require.Equal(t, wrappedUnsignedExportTx, resp.UnsignedTransaction) + require.Equal(t, signingPayloads, resp.Payloads) + }) + + t.Run("parse endpoint (unsigned)", func(t *testing.T) { + req := &types.ConstructionParseRequest{ + NetworkIdentifier: networkIdentifier, + Transaction: wrappedUnsignedExportTx, + Signed: false, + } + + resp, terr := backend.ConstructionParse(ctx, req) + + require.Nil(t, terr) + require.Nil(t, resp.AccountIdentifierSigners) + require.Equal(t, exportOperations, resp.Operations) + }) + + t.Run("combine endpoint", func(t *testing.T) { + req := &types.ConstructionCombineRequest{ + NetworkIdentifier: networkIdentifier, + UnsignedTransaction: wrappedUnsignedExportTx, + Signatures: signatures, + } + + resp, terr := backend.ConstructionCombine(ctx, req) + + require.Nil(t, terr) + require.Equal(t, wrappedSignedExportTx, resp.SignedTransaction) + }) + + t.Run("parse (signed) endpoint", func(t *testing.T) { + req := &types.ConstructionParseRequest{ + NetworkIdentifier: networkIdentifier, + Transaction: wrappedSignedExportTx, + Signed: true, + } + + resp, terr := backend.ConstructionParse(ctx, req) + + require.Nil(t, terr) + require.Equal(t, signers, resp.AccountIdentifierSigners) + require.Equal(t, exportOperations, resp.Operations) + }) + + t.Run("hash endpoint", func(t *testing.T) { + resp, err := backend.ConstructionHash(ctx, &types.ConstructionHashRequest{ + NetworkIdentifier: networkIdentifier, + SignedTransaction: wrappedSignedExportTx, + }) + require.Nil(t, err) + require.Equal(t, signedExportTxHash, resp.TransactionIdentifier.Hash) + }) + + t.Run("submit endpoint", func(t *testing.T) { + require := require.New(t) + + signedTxBytes, err := mapper.DecodeToBytes(signedExportTx) + require.NoError(err) + txID, err := ids.FromString(signedExportTxHash) + require.NoError(err) + + clientMock.EXPECT().IssueTx(ctx, signedTxBytes).Return(txID, nil) + + resp, terr := backend.ConstructionSubmit(ctx, &types.ConstructionSubmitRequest{ + NetworkIdentifier: networkIdentifier, + SignedTransaction: wrappedSignedExportTx, + }) + + require.Nil(terr) + require.Equal(signedExportTxHash, resp.TransactionIdentifier.Hash) + }) +} + +func TestImportTxConstruction(t *testing.T) { + opImport := "IMPORT" + + coinID1 := "23CLURk1Czf1aLui1VdcuWSiDeFskfp3Sn8TQG7t6NKfeQRYDj:2" + coinID2 := "2QmMXKS6rKQMnEh2XYZ4ZWCJmy8RpD3LyVZWxBG25t4N1JJqxY:1" + + importOperations := []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{Index: 0}, + Type: opImport, + Account: cAccountBech32Identifier, + Amount: mapper.AtomicAvaxAmount(big.NewInt(-15_000_000)), + CoinChange: &types.CoinChange{ + CoinIdentifier: &types.CoinIdentifier{Identifier: coinID1}, + CoinAction: types.CoinSpent, + }, + }, + { + OperationIdentifier: &types.OperationIdentifier{Index: 1}, + Type: opImport, + Account: cAccountBech32Identifier, + Amount: mapper.AtomicAvaxAmount(big.NewInt(-5_000_000)), + CoinChange: &types.CoinChange{ + CoinIdentifier: &types.CoinIdentifier{Identifier: coinID2}, + CoinAction: types.CoinSpent, + }, + }, + { + OperationIdentifier: &types.OperationIdentifier{Index: 2}, + RelatedOperations: []*types.OperationIdentifier{ + {Index: 0}, + {Index: 1}, + }, + Type: opImport, + Account: cAccountIdentifier, + Amount: mapper.AtomicAvaxAmount(big.NewInt(19_692_050)), + }, + } + + preprocessMetadata := map[string]interface{}{ + "source_chain": constants.PChain.String(), + } + + metadataOptions := map[string]interface{}{ + "atomic_tx_gas": 12318., + "source_chain": constants.PChain.String(), + } + + suggestedFeeValue := "307950" + + payloadsMetadata := map[string]interface{}{ + "nonce": 0., + "network_id": float64(avalancheNetworkID), + "c_chain_id": cChainID.String(), + "source_chain_id": pChainID.String(), + } + + unsignedImportTx := "0x000000000000000000057fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d500000000000000000000000000000000000000000000000000000000000000000000000288ae5dd070e6d74f16c26358cd4a8f43746d4d338b5b75b668741c6d95816af5000000023d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa000000050000000000e4e1c00000000100000000b9a824340e1b94f27500cdfcbf8eaa9d4ee5e57b2823cb8b158de17689916c74000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000500000000004c4b400000000100000000000000013158e80abd5a1e1aa716003c9db096792c37962100000000012c7a123d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa00000000c67b0534" + + unsignedImportTxHash, _ := hex.DecodeString("33f98143f7f061e262e0fabca57b7f0dc110a79073ed263fc900ebdd0c1fe6fc") + + signingPayloads := []*types.SigningPayload{ + { + AccountIdentifier: cAccountBech32Identifier, + Bytes: unsignedImportTxHash, + SignatureType: types.EcdsaRecovery, + }, + { + AccountIdentifier: cAccountBech32Identifier, + Bytes: unsignedImportTxHash, + SignatureType: types.EcdsaRecovery, + }, + } + + signers := []*types.AccountIdentifier{cAccountBech32Identifier, cAccountBech32Identifier} + importSigners := buildRosettaSignerJSON([]string{coinID1, coinID2}, signers) + + wrappedUnsignedImportTx := `{"tx":"` + unsignedImportTx + `","signers":` + importSigners + `}` + + signedImportTxSignature, _ := hex.DecodeString("a06d20d1d175b1e1d2b6e647ab5321717967de7e9367c28df8c0e20634ec7827019fe38e8d4f123f8e5286f3236db8dbb419e264628e2f17330a6c8da60d342401") + signedImportTx := "0x000000000000000000057fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d500000000000000000000000000000000000000000000000000000000000000000000000288ae5dd070e6d74f16c26358cd4a8f43746d4d338b5b75b668741c6d95816af5000000023d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa000000050000000000e4e1c00000000100000000b9a824340e1b94f27500cdfcbf8eaa9d4ee5e57b2823cb8b158de17689916c74000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000500000000004c4b400000000100000000000000013158e80abd5a1e1aa716003c9db096792c37962100000000012c7a123d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa000000020000000900000001a06d20d1d175b1e1d2b6e647ab5321717967de7e9367c28df8c0e20634ec7827019fe38e8d4f123f8e5286f3236db8dbb419e264628e2f17330a6c8da60d3424010000000900000001a06d20d1d175b1e1d2b6e647ab5321717967de7e9367c28df8c0e20634ec7827019fe38e8d4f123f8e5286f3236db8dbb419e264628e2f17330a6c8da60d342401dc68b1fc" + signedImportTxHash := "2Rz6T1gteozqm5sCG52hDHk6m4iMY65R1LWfBCuPo3f595yrT7" + + wrappedSignedImportTx := `{"tx":"` + signedImportTx + `","signers":` + importSigners + `}` + + signature := &types.Signature{ + SigningPayload: &types.SigningPayload{ + AccountIdentifier: pAccountIdentifier, + Bytes: unsignedImportTxHash, + SignatureType: types.EcdsaRecovery, + }, + SignatureType: types.EcdsaRecovery, + Bytes: signedImportTxSignature, + } + + // two signatures, one for each input + signatures := []*types.Signature{signature, signature} + + ctx := context.Background() + ctrl := gomock.NewController(t) + clientMock := client.NewMockClient(ctrl) + backend := NewBackend(clientMock, avaxAssetID, avalancheNetworkID) + + t.Run("preprocess endpoint", func(t *testing.T) { + req := &types.ConstructionPreprocessRequest{ + NetworkIdentifier: networkIdentifier, + Operations: importOperations, + Metadata: preprocessMetadata, + } + + resp, terr := backend.ConstructionPreprocess(ctx, req) + + require.Nil(t, terr) + require.Equal(t, metadataOptions, resp.Options) + }) + + t.Run("metadata endpoint", func(t *testing.T) { + req := &types.ConstructionMetadataRequest{ + NetworkIdentifier: networkIdentifier, + Options: metadataOptions, + } + + clientMock.EXPECT().GetBlockchainID(ctx, constants.CChain.String()).Return(cChainID, nil) + clientMock.EXPECT().GetBlockchainID(ctx, constants.PChain.String()).Return(pChainID, nil) + clientMock.EXPECT().EstimateBaseFee(ctx).Return(big.NewInt(25_000_000_000), nil) + + resp, terr := backend.ConstructionMetadata(ctx, req) + + require.Nil(t, terr) + require.Equal(t, payloadsMetadata, resp.Metadata) + require.Equal(t, suggestedFeeValue, resp.SuggestedFee[0].Value) + }) + + t.Run("payloads endpoint", func(t *testing.T) { + req := &types.ConstructionPayloadsRequest{ + NetworkIdentifier: networkIdentifier, + Metadata: payloadsMetadata, + Operations: importOperations, + } + + resp, terr := backend.ConstructionPayloads(ctx, req) + + require.Nil(t, terr) + require.Equal(t, wrappedUnsignedImportTx, resp.UnsignedTransaction) + require.Equal(t, signingPayloads, resp.Payloads) + }) + + t.Run("parse endpoint (unsigned)", func(t *testing.T) { + req := &types.ConstructionParseRequest{ + NetworkIdentifier: networkIdentifier, + Transaction: wrappedUnsignedImportTx, + Signed: false, + } + + resp, terr := backend.ConstructionParse(ctx, req) + + require.Nil(t, terr) + require.Nil(t, resp.AccountIdentifierSigners) + require.Equal(t, importOperations, resp.Operations) + }) + + t.Run("combine endpoint", func(t *testing.T) { + req := &types.ConstructionCombineRequest{ + NetworkIdentifier: networkIdentifier, + UnsignedTransaction: wrappedUnsignedImportTx, + Signatures: signatures, + } + + resp, terr := backend.ConstructionCombine(ctx, req) + + require.Nil(t, terr) + require.Equal(t, wrappedSignedImportTx, resp.SignedTransaction) + }) + + t.Run("parse (signed) endpoint", func(t *testing.T) { + req := &types.ConstructionParseRequest{ + NetworkIdentifier: networkIdentifier, + Transaction: wrappedSignedImportTx, + Signed: true, + } + + resp, terr := backend.ConstructionParse(ctx, req) + + require.Nil(t, terr) + require.Equal(t, signers, resp.AccountIdentifierSigners) + require.Equal(t, importOperations, resp.Operations) + }) + + t.Run("hash endpoint", func(t *testing.T) { + resp, err := backend.ConstructionHash(ctx, &types.ConstructionHashRequest{ + NetworkIdentifier: networkIdentifier, + SignedTransaction: wrappedSignedImportTx, + }) + require.Nil(t, err) + require.Equal(t, signedImportTxHash, resp.TransactionIdentifier.Hash) + }) + + t.Run("submit endpoint", func(t *testing.T) { + require := require.New(t) + + signedTxBytes, err := mapper.DecodeToBytes(signedImportTx) + require.NoError(err) + txID, err := ids.FromString(signedImportTxHash) + require.NoError(err) + + clientMock.EXPECT().IssueTx(ctx, signedTxBytes).Return(txID, nil) + + resp, terr := backend.ConstructionSubmit(ctx, &types.ConstructionSubmitRequest{ + NetworkIdentifier: networkIdentifier, + SignedTransaction: wrappedSignedImportTx, + }) + + require.Nil(terr) + require.Equal(signedImportTxHash, resp.TransactionIdentifier.Hash) + }) +} diff --git a/server/service/backend/cchainatomictx/types.go b/server/service/backend/cchainatomictx/types.go new file mode 100644 index 0000000..c0f953a --- /dev/null +++ b/server/service/backend/cchainatomictx/types.go @@ -0,0 +1,107 @@ +package cchainatomictx + +import ( + "errors" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/hashing" + "github.com/ava-labs/coreth/plugin/evm" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/ava-labs/avalanche-rosetta/mapper" + "github.com/ava-labs/avalanche-rosetta/service" + "github.com/ava-labs/avalanche-rosetta/service/backend/common" + + cmapper "github.com/ava-labs/avalanche-rosetta/mapper/cchainatomictx" +) + +var ( + _ common.AvaxTx = &cAtomicTx{} + _ common.TxBuilder = &cAtomicTxBuilder{} + _ common.TxParser = &cAtomicTxParser{} +) + +type cAtomicTx struct { + Tx *evm.Tx + Codec codec.Manager + CodecVersion uint16 +} + +func (c *cAtomicTx) Initialize() error { + if c.Tx == nil { + return common.ErrNoTxGiven + } + return c.Tx.Sign(c.Codec, nil) +} + +func (c *cAtomicTx) Marshal() ([]byte, error) { + return c.Codec.Marshal(c.CodecVersion, c.Tx) +} + +func (c *cAtomicTx) Unmarshal(bytes []byte) error { + tx := evm.Tx{} + _, err := c.Codec.Unmarshal(bytes, &tx) + if err != nil { + return err + } + if err := tx.Sign(c.Codec, nil); err != nil { + return err + } + c.Tx = &tx + + return c.Initialize() +} + +func (c *cAtomicTx) SigningPayload() []byte { + return hashing.ComputeHash256(c.Tx.Bytes()) +} + +func (c *cAtomicTx) Hash() ids.ID { + return c.Tx.ID() +} + +type cAtomicTxBuilder struct { + avaxAssetID ids.ID + codec codec.Manager + codecVersion uint16 +} + +func (c cAtomicTxBuilder) BuildTx(operations []*types.Operation, metadata map[string]interface{}) (common.AvaxTx, []*types.AccountIdentifier, *types.Error) { + cMetadata := cmapper.Metadata{} + err := mapper.UnmarshalJSONMap(metadata, &cMetadata) + if err != nil { + return nil, nil, service.WrapError(service.ErrInvalidInput, err) + } + + matches, err := common.MatchOperations(operations) + if err != nil { + return nil, nil, service.WrapError(service.ErrInvalidInput, err) + } + + opType := matches[0].Operations[0].Type + tx, signers, err := cmapper.BuildTx(opType, matches, cMetadata, c.codec, c.avaxAssetID) + if err != nil { + return nil, nil, service.WrapError(service.ErrInternalError, err) + } + + return &cAtomicTx{ + Tx: tx, + Codec: c.codec, + CodecVersion: c.codecVersion, + }, signers, nil +} + +type cAtomicTxParser struct { + hrp string + chainIDs map[ids.ID]string +} + +func (c cAtomicTxParser) ParseTx(tx *common.RosettaTx, inputAddresses map[string]*types.AccountIdentifier) ([]*types.Operation, error) { + cTx, ok := tx.Tx.(*cAtomicTx) + if !ok { + return nil, errors.New("invalid transaction") + } + parser := cmapper.NewTxParser(c.hrp, c.chainIDs, inputAddresses) + return parser.Parse(*cTx.Tx) +} diff --git a/server/service/backend/common/account.go b/server/service/backend/common/account.go new file mode 100644 index 0000000..9a66a0b --- /dev/null +++ b/server/service/backend/common/account.go @@ -0,0 +1,32 @@ +package common + +import ( + "sort" + + "github.com/coinbase/rosetta-sdk-go/types" +) + +// SortUnique deduplicates given slice of coins and sorts them by UTXO id in ascending order for consistency +// +// Per https://docs.avax.network/apis/avalanchego/apis/p-chain#platformgetutxos +// and https://docs.avax.network/apis/avalanchego/apis/c-chain#avaxgetutxos +// paginated getUTXOs calls may have duplicate UTXOs in different pages, this helper eliminates them. +func SortUnique(coins []*types.Coin) []*types.Coin { + coinsMap := make(map[string]*types.Coin) + for _, coin := range coins { + coinsMap[coin.CoinIdentifier.Identifier] = coin + } + + uniqueCoinIdentifiers := make([]string, 0, len(coinsMap)) + for identifier := range coinsMap { + uniqueCoinIdentifiers = append(uniqueCoinIdentifiers, identifier) + } + sort.Strings(uniqueCoinIdentifiers) + + uniqueCoins := make([]*types.Coin, 0, len(coinsMap)) + for _, identifier := range uniqueCoinIdentifiers { + uniqueCoins = append(uniqueCoins, coinsMap[identifier]) + } + + return uniqueCoins +} diff --git a/server/service/backend/common/construction.go b/server/service/backend/common/construction.go new file mode 100644 index 0000000..1954b12 --- /dev/null +++ b/server/service/backend/common/construction.go @@ -0,0 +1,358 @@ +package common + +import ( + "context" + "encoding/json" + "errors" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/utils/rpc" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/coinbase/rosetta-sdk-go/parser" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/mapper" + "github.com/ava-labs/avalanche-rosetta/service" + + pmapper "github.com/ava-labs/avalanche-rosetta/mapper/pchain" +) + +var ( + errNoOperationsToMatch = errors.New("no operations were passed to match") + errInvalidInput = errors.New("invalid input") + errInvalidInputSignatureLen = errors.New("input signature length doesn't match credentials needed") + errInsufficientSignatures = errors.New("insufficient signatures") + errInvalidSignatureLen = errors.New("invalid signature length") +) + +// DeriveBech32Address derives Bech32 addresses for the given chain using public key and hrp provided in the request +func DeriveBech32Address(chainIDAlias constants.ChainIDAlias, req *types.ConstructionDeriveRequest) (*types.ConstructionDeriveResponse, *types.Error) { + pub, err := secp256k1.ToPublicKey(req.PublicKey.Bytes) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + hrp, getErr := mapper.GetHRP(req.NetworkIdentifier) + if getErr != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + addr, err := address.Format(chainIDAlias.String(), hrp, pub.Address().Bytes()) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + return &types.ConstructionDeriveResponse{ + AccountIdentifier: &types.AccountIdentifier{ + Address: addr, + }, + }, nil +} + +// MatchOperations defines the operation Rosetta parser matching rules and parses the input operations +// +// We require 2 types of operations; inputs with negative amounts and outputs with positive amounts +// parser guarantees there will be 2 matches. +func MatchOperations(operations []*types.Operation) ([]*parser.Match, error) { + if len(operations) == 0 { + return nil, errNoOperationsToMatch + } + opType := operations[0].Type + + var coinAction types.CoinAction + var allowRepeatOutputs bool + + switch opType { + case mapper.OpExport: + coinAction = "" + allowRepeatOutputs = false + case mapper.OpImport: + coinAction = types.CoinSpent + allowRepeatOutputs = false + default: + coinAction = types.CoinSpent + allowRepeatOutputs = true + } + + descriptions := &parser.Descriptions{ + OperationDescriptions: []*parser.OperationDescription{ + { + Type: opType, + Account: &parser.AccountDescription{ + Exists: true, + }, + Amount: &parser.AmountDescription{ + Exists: true, + Sign: parser.NegativeAmountSign, + }, + AllowRepeats: true, + CoinAction: coinAction, + }, + { + Type: opType, + Account: &parser.AccountDescription{ + Exists: true, + }, + Amount: &parser.AmountDescription{ + Exists: true, + Sign: parser.PositiveAmountSign, + }, + AllowRepeats: allowRepeatOutputs, + }, + }, + ErrUnmatched: true, + } + + return parser.MatchOperations(descriptions, operations) +} + +// TxBuilder implements backend specific transaction construction logic +type TxBuilder interface { + BuildTx(matches []*types.Operation, rawMetadata map[string]interface{}) (AvaxTx, []*types.AccountIdentifier, *types.Error) +} + +// BuildPayloads performs transaction construction in /construction/payloads call and returns the unsigned transaction as well as the signing payloads. +// Chain specific logic is abstracted using the TxBuilder interface's BuildTx method. +func BuildPayloads( + txBuilder TxBuilder, + req *types.ConstructionPayloadsRequest, +) (*types.ConstructionPayloadsResponse, *types.Error) { + tx, signers, tErr := txBuilder.BuildTx(req.Operations, req.Metadata) + if tErr != nil { + return nil, tErr + } + + accountIdentifierSigners := make([]Signer, 0, len(req.Operations)) + for _, o := range req.Operations { + // Skip positive amounts + if o.Amount.Value[0] != '-' { + continue + } + + var coinIdentifier string + + if o.CoinChange != nil && o.CoinChange.CoinIdentifier != nil { + coinIdentifier = o.CoinChange.CoinIdentifier.Identifier + } + + accountIdentifierSigners = append(accountIdentifierSigners, Signer{ + CoinIdentifier: coinIdentifier, + AccountIdentifier: o.Account, + }) + } + + rosettaTx := &RosettaTx{ + Tx: tx, + AccountIdentifierSigners: accountIdentifierSigners, + } + + payloads := make([]*types.SigningPayload, len(signers)) + for i, signer := range signers { + payloads[i] = &types.SigningPayload{ + AccountIdentifier: signer, + Bytes: tx.SigningPayload(), + SignatureType: types.EcdsaRecovery, + } + } + + var metadata pmapper.Metadata + err := mapper.UnmarshalJSONMap(req.Metadata, &metadata) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, err) + } + + if metadata.ExportMetadata != nil { + rosettaTx.DestinationChain = metadata.DestinationChain + rosettaTx.DestinationChainID = &metadata.DestinationChainID + } + + txJSON, err := json.Marshal(rosettaTx) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, err) + } + + return &types.ConstructionPayloadsResponse{ + UnsignedTransaction: string(txJSON), + Payloads: payloads, + }, nil +} + +// TxParser implements backend specific transaction parsing logic +type TxParser interface { + ParseTx(tx *RosettaTx, inputAddresses map[string]*types.AccountIdentifier) ([]*types.Operation, error) +} + +// Parse contains transaction parsing logic for /construction/parse endpoint +// Chain specific logic is abstracted using TxParser interface's ParseTx method +func Parse(parser TxParser, payloadsTx *RosettaTx, isSigned bool) (*types.ConstructionParseResponse, *types.Error) { + // Convert input tx into operations + inputAddresses := getInputAddresses(payloadsTx) + operations, err := parser.ParseTx(payloadsTx, inputAddresses) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, "incorrect transaction input") + } + + // Generate AccountIdentifierSigners if request is signed + var signers []*types.AccountIdentifier + if isSigned { + payloadSigners, err := payloadsTx.GetAccountIdentifiers(operations) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + signers = payloadSigners + } + + return &types.ConstructionParseResponse{ + Operations: operations, + AccountIdentifierSigners: signers, + }, nil +} + +func getInputAddresses(tx *RosettaTx) map[string]*types.AccountIdentifier { + addresses := make(map[string]*types.AccountIdentifier) + + for _, signer := range tx.AccountIdentifierSigners { + addresses[signer.CoinIdentifier] = signer.AccountIdentifier + } + + return addresses +} + +// TxCombiner implements backend specific transaction submission logic +type TxCombiner interface { + CombineTx(tx AvaxTx, signatures []*types.Signature) (AvaxTx, *types.Error) +} + +// Combine combines unsigned transactions with the provided signatures as part of /construction/combine call. +// Chain spacific logic is abstracted in TxCombiner interface's CombineTx method. +func Combine( + combiner TxCombiner, + rosettaTx *RosettaTx, + signatures []*types.Signature, +) (*types.ConstructionCombineResponse, *types.Error) { + combinedTx, tErr := combiner.CombineTx(rosettaTx.Tx, signatures) + if tErr != nil { + return nil, tErr + } + + signedTransaction, err := json.Marshal(&RosettaTx{ + Tx: combinedTx, + AccountIdentifierSigners: rosettaTx.AccountIdentifierSigners, + DestinationChain: rosettaTx.DestinationChain, + DestinationChainID: rosettaTx.DestinationChainID, + }) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, "unable to encode signed transaction") + } + + return &types.ConstructionCombineResponse{ + SignedTransaction: string(signedTransaction), + }, nil +} + +// BuildCredentialList builds a list of *secp256k1fx.Credentials using the given signatures +// +// Based on tx inputs, we can determine the number of signatures +// required by each input and put correct number of signatures to +// construct the signed tx. +// +// See https://github.com/ava-labs/avalanchego/blob/v1.9.0/vms/platformvm/txs/tx.go#L104 +// for more details. +func BuildCredentialList(ins []*avax.TransferableInput, signatures []*types.Signature) ([]verify.Verifiable, error) { + creds := make([]verify.Verifiable, len(ins)) + sigOffset := 0 + for i, transferInput := range ins { + input, ok := transferInput.In.(*secp256k1fx.TransferInput) + if !ok { + return nil, errInvalidInput + } + + cred, err := buildCredential(len(input.SigIndices), &sigOffset, signatures) + if err != nil { + return nil, err + } + + creds[i] = cred + } + + if sigOffset != len(signatures) { + return nil, errInvalidInputSignatureLen + } + + return creds, nil +} + +// BuildSingletonCredentialList builds a list of a single *secp256k1fx.Credential using the given signatures +func BuildSingletonCredentialList(signatures []*types.Signature) ([]verify.Verifiable, error) { + offset := 0 + cred, err := buildCredential(1, &offset, signatures) + if err != nil { + return nil, err + } + + return []verify.Verifiable{cred}, nil +} + +func buildCredential(numSigs int, sigOffset *int, signatures []*types.Signature) (*secp256k1fx.Credential, error) { + cred := &secp256k1fx.Credential{} + cred.Sigs = make([][secp256k1.SignatureLen]byte, numSigs) + for j := 0; j < numSigs; j++ { + if *sigOffset >= len(signatures) { + return nil, errInsufficientSignatures + } + + if len(signatures[*sigOffset].Bytes) != secp256k1.SignatureLen { + return nil, errInvalidSignatureLen + } + copy(cred.Sigs[j][:], signatures[*sigOffset].Bytes) + *sigOffset++ + } + return cred, nil +} + +// HashTx generates a transaction id for the given RosettaTx +func HashTx(rosettaTx *RosettaTx) (*types.TransactionIdentifierResponse, *types.Error) { + txID := rosettaTx.Tx.Hash() + hash := txID.String() + + return &types.TransactionIdentifierResponse{ + TransactionIdentifier: &types.TransactionIdentifier{ + Hash: hash, + }, + }, nil +} + +// TransactionIssuer implements chain specific transaction submission logic +type TransactionIssuer interface { + IssueTx(ctx context.Context, txByte []byte, options ...rpc.Option) (ids.ID, error) +} + +// SubmitTx broadcasts given Rosetta tx on chain and returns the transaction id. +// Chain specific logic is abstracted using the TransactionIssuer interface's IssueTx method. +func SubmitTx( + ctx context.Context, + issuer TransactionIssuer, + rosettaTx *RosettaTx, +) (*types.TransactionIdentifierResponse, *types.Error) { + bytes, err := rosettaTx.Tx.Marshal() + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + txID, err := issuer.IssueTx(ctx, bytes) + if err != nil { + return nil, service.WrapError(service.ErrClientError, err) + } + + return &types.TransactionIdentifierResponse{ + TransactionIdentifier: &types.TransactionIdentifier{ + Hash: txID.String(), + }, + }, nil +} diff --git a/server/service/backend/common/types.go b/server/service/backend/common/types.go new file mode 100644 index 0000000..ca4e9de --- /dev/null +++ b/server/service/backend/common/types.go @@ -0,0 +1,122 @@ +package common + +import ( + "encoding/json" + "errors" + + "github.com/ava-labs/avalanchego/ids" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/ava-labs/avalanche-rosetta/mapper" +) + +var ErrNoTxGiven = errors.New("no transaction was given") + +// AvaxTx encapsulates P-chain and C-chain atomic transactions in order to reuse common logic between them +type AvaxTx interface { + Initialize() error + Marshal() ([]byte, error) + Unmarshal([]byte) error + SigningPayload() []byte + Hash() ids.ID +} + +// RosettaTx wraps a transaction along with the input addresses and destination chain information. +// It is used during construction between /construction/payloads and /construction/parse since parse needs this information +// but C-chain atomic and P-chain tx formats strip this information and only retain UTXO ids. +type RosettaTx struct { + Tx AvaxTx + AccountIdentifierSigners []Signer + DestinationChain string + DestinationChainID *ids.ID +} + +// Signer contains details of coin identifiers and the accounts signing those coins +type Signer struct { + CoinIdentifier string `json:"coin_identifier,omitempty"` + AccountIdentifier *types.AccountIdentifier `json:"account_identifier"` +} + +type rosettaTxWire struct { + Tx string `json:"tx"` + Signers []Signer `json:"signers"` + DestinationChain string `json:"destination_chain,omitempty"` + DestinationChainID *ids.ID `json:"destination_chain_id,omitempty"` +} + +func (t *RosettaTx) MarshalJSON() ([]byte, error) { + bytes, err := t.Tx.Marshal() + if err != nil { + return nil, err + } + + str, err := mapper.EncodeBytes(bytes) + if err != nil { + return nil, err + } + + txWire := &rosettaTxWire{ + Tx: str, + Signers: t.AccountIdentifierSigners, + DestinationChain: t.DestinationChain, + DestinationChainID: t.DestinationChainID, + } + return json.Marshal(txWire) +} + +func (t *RosettaTx) UnmarshalJSON(data []byte) error { + if t.Tx == nil { + return errors.New("tx must be initialized before unmarshalling") + } + txWire := &rosettaTxWire{} + err := json.Unmarshal(data, txWire) + if err != nil { + return err + } + + bytes, err := mapper.DecodeToBytes(txWire.Tx) + if err != nil { + return err + } + + err = t.Tx.Unmarshal(bytes) + if err != nil { + return err + } + + t.AccountIdentifierSigners = txWire.Signers + t.DestinationChain = txWire.DestinationChain + t.DestinationChainID = txWire.DestinationChainID + + return nil +} + +// GetAccountIdentifiers extracts input account identifiers from given Rosetta operations +func (t *RosettaTx) GetAccountIdentifiers(operations []*types.Operation) ([]*types.AccountIdentifier, error) { + signers := []*types.AccountIdentifier{} + + operationToAccountMap := make(map[string]*types.AccountIdentifier) + for _, data := range t.AccountIdentifierSigners { + operationToAccountMap[data.CoinIdentifier] = data.AccountIdentifier + } + + for _, op := range operations { + // Skip positive amounts + if op.Amount.Value[0] != '-' { + continue + } + + var coinIdentifier string + if op.CoinChange != nil && op.CoinChange.CoinIdentifier != nil { + coinIdentifier = op.CoinChange.CoinIdentifier.Identifier + } + + signer := operationToAccountMap[coinIdentifier] + if signer == nil { + return nil, errors.New("not all operations have signers") + } + signers = append(signers, signer) + } + + return signers, nil +} diff --git a/server/service/backend/pchain/account.go b/server/service/backend/pchain/account.go new file mode 100644 index 0000000..69dc771 --- /dev/null +++ b/server/service/backend/pchain/account.go @@ -0,0 +1,545 @@ +package pchain + +import ( + "context" + "errors" + "math/big" + "slices" + "strconv" + "strings" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/utils/math" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/mapper" + "github.com/ava-labs/avalanche-rosetta/service" + "github.com/ava-labs/avalanche-rosetta/service/backend/common" + + pmapper "github.com/ava-labs/avalanche-rosetta/mapper/pchain" + avaconstants "github.com/ava-labs/avalanchego/utils/constants" +) + +var ( + errUnableToGetUTXOs = errors.New("unable to get UTXOs") + errUnableToParseUTXO = errors.New("unable to parse UTXO") + errUnableToGetUTXOOut = errors.New("unable to get UTXO output") + errTotalOverflow = errors.New("overflow while calculating total balance") + errUnlockedOverflow = errors.New("overflow while calculating unlocked balance") + errLockedOverflow = errors.New("overflow while calculating locked balance") + errNotStakeableOverflow = errors.New("overflow while calculating locked not stakeable balance") + errLockedNotStakeableOverflow = errors.New("overflow while calculating locked not stakeable balance") + errUnlockedStakeableOverflow = errors.New("overflow while calculating unlocked stakeable balance") +) + +// AccountBalance implements /account/balance endpoint for P-chain +func (b *Backend) AccountBalance(ctx context.Context, req *types.AccountBalanceRequest) (*types.AccountBalanceResponse, *types.Error) { + if req.AccountIdentifier == nil { + return nil, service.WrapError(service.ErrInvalidInput, "account identifier is not provided") + } + if req.BlockIdentifier != nil { + return nil, service.WrapError(service.ErrNotSupported, "historical balance lookups are not supported") + } + + currencyAssetIDs, wrappedErr := b.buildCurrencyAssetIDs(ctx, req.Currencies) + if wrappedErr != nil { + return nil, wrappedErr + } + + var balanceType string + if req.AccountIdentifier.SubAccount != nil { + balanceType = req.AccountIdentifier.SubAccount.Address + } + + if strings.HasPrefix(balanceType, ids.NodeIDPrefix) { + return b.getPendingRewardsBalance(ctx, req) + } + + fetchImportable := balanceType == pmapper.SubAccountTypeSharedMemory + + height, balance, typedErr := b.fetchBalance(ctx, req.AccountIdentifier.Address, fetchImportable, currencyAssetIDs) + if typedErr != nil { + return nil, typedErr + } + + var balanceValue uint64 + switch balanceType { + case pmapper.SubAccountTypeUnlocked: + balanceValue = balance.Unlocked + case pmapper.SubAccountTypeLockedStakeable: + balanceValue = balance.LockedStakeable + case pmapper.SubAccountTypeLockedNotStakeable: + balanceValue = balance.LockedNotStakeable + case pmapper.SubAccountTypeStaked: + balanceValue = balance.Staked + case pmapper.SubAccountTypeSharedMemory: + balanceValue = balance.Total + case "": // Defaults to total balance + balanceValue = balance.Total + default: + return nil, service.WrapError(service.ErrInvalidInput, "unknown account type "+balanceType) + } + + block, err := b.indexerParser.ParseNonGenesisBlock(ctx, "", height) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, "unable to get height") + } + + return &types.AccountBalanceResponse{ + BlockIdentifier: &types.BlockIdentifier{ + Index: int64(height), + Hash: block.BlockID.String(), + }, + Balances: []*types.Amount{ + { + Value: strconv.FormatUint(balanceValue, 10), + Currency: mapper.AtomicAvaxCurrency, + }, + }, + }, nil +} + +// AccountCoins implements /account/coins endpoint for P-chain +func (b *Backend) AccountCoins(ctx context.Context, req *types.AccountCoinsRequest) (*types.AccountCoinsResponse, *types.Error) { + if req.AccountIdentifier == nil { + return nil, service.WrapError(service.ErrInvalidInput, "account identifier is not provided") + } + addr, err := address.ParseToID(req.AccountIdentifier.Address) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, "unable to convert address") + } + + assetIDs, wrappedErr := b.buildCurrencyAssetIDs(ctx, req.Currencies) + if err != nil { + return nil, wrappedErr + } + + var subAccountAddress string + if req.AccountIdentifier.SubAccount != nil { + subAccountAddress = req.AccountIdentifier.SubAccount.Address + } + fetchSharedMemory := subAccountAddress == pmapper.SubAccountTypeSharedMemory + + // utxos from fetchUTXOsAndStakedOutputs are guarateed to: + // 1. be unique (no duplicates) + // 2. containt only assetIDs + // 3. have not multisign utxos + // by parseAndFilterUTXOs call in fetchUTXOsAndStakedOutputs + height, utxos, _, typedErr := b.fetchUTXOsAndStakedOutputs(ctx, addr, false, fetchSharedMemory, assetIDs) + if typedErr != nil { + return nil, typedErr + } + + // convert UTXOs to Rosetta Coins + coins := []*types.Coin{} + for _, utxo := range utxos { + amounter, ok := utxo.Out.(avax.Amounter) + if !ok { + return nil, service.WrapError(service.ErrInternalError, errUnableToGetUTXOOut) + } + coin := &types.Coin{ + CoinIdentifier: &types.CoinIdentifier{Identifier: utxo.UTXOID.String()}, + Amount: &types.Amount{ + Value: strconv.FormatUint(amounter.Amount(), 10), + Currency: mapper.AtomicAvaxCurrency, + }, + } + coins = append(coins, coin) + } + + block, err := b.indexerParser.ParseNonGenesisBlock(ctx, "", height) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, "unable to get height") + } + + // this is needed just for sorting. Uniqueness is guaranteed by utxos uniqueness + coins = common.SortUnique(coins) + return &types.AccountCoinsResponse{ + BlockIdentifier: &types.BlockIdentifier{ + Index: int64(height), + Hash: block.BlockID.String(), + }, + Coins: coins, + }, nil +} + +func (b *Backend) getPendingRewardsBalance(ctx context.Context, req *types.AccountBalanceRequest) (*types.AccountBalanceResponse, *types.Error) { + addr, err := address.ParseToID(req.AccountIdentifier.Address) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, "malformed address") + } + + nodeIDStr := req.AccountIdentifier.SubAccount.Address + validatorNodeID, err := ids.NodeIDFromString(nodeIDStr) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, "malformed validator_node_id ") + } + + var nodeIDs []ids.NodeID + nodeIDs = append(nodeIDs, validatorNodeID) + + validators, err := b.pClient.GetCurrentValidators(ctx, avaconstants.PrimaryNetworkID, nodeIDs) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, "unable to fetch validators") + } + + validatorRewards := big.NewInt(0) + delegationFeeRewards := big.NewInt(0) + delegationRewards := big.NewInt(0) + + for _, v := range validators { + isValidatorOwner := false + + if slices.Contains(v.ValidationRewardOwner.Addresses, addr) { + validatorRewards.Add(validatorRewards, new(big.Int).SetUint64(*v.PotentialReward)) + isValidatorOwner = true + } + + for _, d := range v.Delegators { + delegationReward := new(big.Int).SetUint64(*d.PotentialReward) + // Representing delegation fee as percentage of 10^6, the same way as + // DelegatorShare parameter of AddValidator transaction + delegationFeePct := big.NewInt(int64(v.DelegationFee * 10_000)) + delegationFee := new(big.Int).Div(new(big.Int).Mul(delegationReward, delegationFeePct), big.NewInt(1_000_000)) + + // if the address we are searching is the validator owner, add the delegation fee + if isValidatorOwner { + delegationFeeRewards.Add(delegationFeeRewards, delegationFee) + } + + // ff the address delegated to the current validator, add the potential reward minus fee + if slices.Contains(d.RewardOwner.Addresses, addr) { + delegateReward := new(big.Int).Sub(delegationReward, delegationFee) + delegationRewards.Add(delegationRewards, delegateReward) + } + } + } + + totalRewards := big.NewInt(0) + totalRewards.Add(totalRewards, validatorRewards) + totalRewards.Add(totalRewards, delegationFeeRewards) + totalRewards.Add(totalRewards, delegationRewards) + + height, err := b.pClient.GetHeight(ctx) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, "unable to get block height") + } + + block, err := b.indexerParser.ParseNonGenesisBlock(ctx, "", height) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, "unable to get current block") + } + + return &types.AccountBalanceResponse{ + BlockIdentifier: &types.BlockIdentifier{ + Index: int64(height), + Hash: block.BlockID.String(), + }, + Balances: []*types.Amount{{ + Value: totalRewards.String(), + Currency: mapper.AtomicAvaxCurrency, + Metadata: map[string]interface{}{ + pmapper.MetadataValidatorRewards: validatorRewards.String(), + pmapper.MetadataDelegationRewards: delegationRewards.String(), + pmapper.MetadataDelegationFeeRewards: delegationFeeRewards.String(), + }, + }}, + }, nil +} + +func (b *Backend) fetchBalance(ctx context.Context, addrString string, fetchImportable bool, assetIds set.Set[ids.ID]) (uint64, *AccountBalance, *types.Error) { + addr, err := address.ParseToID(addrString) + if err != nil { + return 0, nil, service.WrapError(service.ErrInvalidInput, "unable to convert address") + } + + // utxos from fetchUTXOsAndStakedOutputs are guarateed to: + // 1. be unique (no duplicates) + // 2. containt only assetIDs + // 3. have not multisign utxos + // by parseAndFilterUTXOs call in fetchUTXOsAndStakedOutputs + height, utxos, stakedUTXOBytes, typedErr := b.fetchUTXOsAndStakedOutputs(ctx, addr, !fetchImportable, fetchImportable, assetIds) + if typedErr != nil { + return 0, nil, typedErr + } + + balance, err := getBalancesWithoutMultisig(utxos) + if err != nil { + return 0, nil, service.WrapError(service.ErrInternalError, err) + } + + // parse staked UTXO bytes to UTXO structs + stakedAmount, err := b.calculateStakedAmount(stakedUTXOBytes) + if err != nil { + return 0, nil, service.WrapError(service.ErrInternalError, err) + } + + balance.Staked = stakedAmount + balance.Total += stakedAmount + + return height, balance, nil +} + +// Copy of the platformvm service's GetBalance implementation. +// This is needed as multisig UTXOs are cleaned in parseUTXOs and its output must be used for the calculations. Ref: +// https://github.com/ava-labs/avalanchego/blob/0950acab667e0c16a55e9a9bb72bcbe25c3b88cf/vms/platformvm/service.go#L184 +func getBalancesWithoutMultisig(utxos []avax.UTXO) (*AccountBalance, error) { + currentTime := uint64(time.Now().Unix()) + + accountBalance := &AccountBalance{ + Total: 0, + Staked: 0, + Unlocked: 0, + LockedStakeable: 0, + LockedNotStakeable: 0, + } + +utxoFor: + for _, utxo := range utxos { + switch out := utxo.Out.(type) { + case *secp256k1fx.TransferOutput: + if out.Locktime <= currentTime { + newBalance, err := math.Add64(accountBalance.Unlocked, out.Amount()) + if err != nil { + return nil, errUnlockedOverflow + } + accountBalance.Unlocked = newBalance + } else { + newBalance, err := math.Add64(accountBalance.LockedNotStakeable, out.Amount()) + if err != nil { + return nil, errNotStakeableOverflow + } + accountBalance.LockedNotStakeable = newBalance + } + case *stakeable.LockOut: + innerOut, ok := out.TransferableOut.(*secp256k1fx.TransferOutput) + switch { + case !ok: + continue utxoFor + case innerOut.Locktime > currentTime: + newBalance, err := math.Add64(accountBalance.LockedNotStakeable, out.Amount()) + if err != nil { + return nil, errLockedNotStakeableOverflow + } + accountBalance.LockedNotStakeable = newBalance + case out.Locktime <= currentTime: + newBalance, err := math.Add64(accountBalance.Unlocked, out.Amount()) + if err != nil { + return nil, errUnlockedOverflow + } + accountBalance.Unlocked = newBalance + default: + newBalance, err := math.Add64(accountBalance.LockedStakeable, out.Amount()) + if err != nil { + return nil, errUnlockedStakeableOverflow + } + accountBalance.LockedStakeable = newBalance + } + default: + continue utxoFor + } + } + + lockedBalance, err := math.Add64(accountBalance.LockedStakeable, accountBalance.LockedNotStakeable) + if err != nil { + return nil, errLockedOverflow + } + + totalBalance, err := math.Add64(accountBalance.Unlocked, lockedBalance) + if err != nil { + return nil, errTotalOverflow + } + + accountBalance.Total = totalBalance + + return accountBalance, nil +} + +func (b *Backend) buildCurrencyAssetIDs(ctx context.Context, currencies []*types.Currency) (set.Set[ids.ID], *types.Error) { + assetIDs := set.NewSet[ids.ID](len(currencies)) + for _, reqCurrency := range currencies { + description, err := b.pClient.GetAssetDescription(ctx, reqCurrency.Symbol) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, "unable to get asset description") + } + if int32(description.Denomination) != reqCurrency.Decimals { + return nil, service.WrapError(service.ErrInvalidInput, "incorrect currency decimals") + } + assetIDs.Add(description.AssetID) + } + + return assetIDs, nil +} + +// Fetches UTXOs and staked outputs for the given account. +// +// Since these APIs don't return the corresponding block height or hash, +// which is needed for both /account/balance and /account/coins, chain height is checked before and after +// and if they differ, an error is returned. +func (b *Backend) fetchUTXOsAndStakedOutputs(ctx context.Context, addr ids.ShortID, fetchStaked bool, fetchSharedMemory bool, assetIds set.Set[ids.ID]) (uint64, []avax.UTXO, [][]byte, *types.Error) { + // fetch preHeight before the balance fetch + preHeight, err := b.pClient.GetHeight(ctx) + if err != nil { + return 0, nil, nil, service.WrapError(service.ErrInvalidInput, "unable to get chain height pre-lookup") + } + + sourceChains := []constants.ChainIDAlias{constants.AnyChain} + if fetchSharedMemory { + sourceChains = []constants.ChainIDAlias{ + constants.CChain, + constants.XChain, + } + } + + var utxoBytes [][]byte + + for _, sc := range sourceChains { + // fetch all UTXOs for addr + chainUtxoBytes, err := b.getAccountUTXOs(ctx, addr, sc) + if err != nil { + return 0, nil, nil, service.WrapError(service.ErrInternalError, err) + } + utxoBytes = append(utxoBytes, chainUtxoBytes...) + } + + if err != nil { + return 0, nil, nil, service.WrapError(service.ErrInternalError, err) + } + + var stakedUTXOBytes [][]byte + if fetchStaked { + // fetch staked outputs for addr + _, stakedUTXOBytes, err = b.pClient.GetStake(ctx, []ids.ShortID{addr}, false) + if err != nil { + return 0, nil, nil, service.WrapError(service.ErrInvalidInput, "unable to get stake") + } + } + + // fetch postHeight after the balance fetch and compare with preHeight + postHeight, err := b.pClient.GetHeight(ctx) + if err != nil { + return 0, nil, nil, service.WrapError(service.ErrInvalidInput, "unable to get chain height post-lookup") + } + if postHeight != preHeight { + return 0, nil, nil, service.WrapError(service.ErrInternalError, "new block added while fetching utxos") + } + + // parse UTXO bytes to UTXO structs + utxos, err := b.parseAndFilterUTXOs(utxoBytes, assetIds) + if err != nil { + return 0, nil, nil, service.WrapError(service.ErrInternalError, err) + } + + return postHeight, utxos, stakedUTXOBytes, nil +} + +func (b *Backend) calculateStakedAmount(stakeUTXOs [][]byte) (uint64, error) { + staked := uint64(0) + + for _, utxoBytes := range stakeUTXOs { + utxo := avax.TransferableOutput{} + + _, err := b.codec.Unmarshal(utxoBytes, &utxo) + if err != nil { + return 0, errUnableToParseUTXO + } + + outIntf := utxo.Out + if lockedOut, ok := outIntf.(*stakeable.LockOut); ok { + outIntf = lockedOut.TransferableOut + } + + out, ok := outIntf.(*secp256k1fx.TransferOutput) + if !ok { + return 0, errUnableToParseUTXO + } + + // ignore multisig + if len(out.OutputOwners.Addrs) > 1 { + continue + } + + staked += out.Amt + } + + return staked, nil +} + +func (b *Backend) parseAndFilterUTXOs(utxoBytes [][]byte, assetIDs set.Set[ids.ID]) ([]avax.UTXO, error) { + utxos := []avax.UTXO{} + + // when results are paginated, duplicate UTXOs may be provided. guarantee uniqueness + utxoIDs := set.NewSet[ids.ID](len(utxoBytes)) + for _, bytes := range utxoBytes { + utxo := avax.UTXO{} + _, err := b.codec.Unmarshal(bytes, &utxo) + if err != nil { + return nil, errUnableToParseUTXO + } + + // Skip UTXO if req.Currencies is specified, but it doesn't contain the UTXOs asset + if assetIDs.Len() > 0 && !assetIDs.Contains(utxo.AssetID()) { + continue + } + + // remove duplicates + if utxoIDs.Contains(utxo.UTXOID.InputID()) { + continue + } + utxoIDs.Add(utxo.UTXOID.InputID()) + + // Skip multisig UTXOs + addressable, ok := utxo.Out.(avax.Addressable) + if !ok { + return nil, errUnableToGetUTXOOut + } + if len(addressable.Addresses()) > 1 { + continue + } + + utxos = append(utxos, utxo) + } + + return utxos, nil +} + +func (b *Backend) getAccountUTXOs(ctx context.Context, addr ids.ShortID, sourceChain constants.ChainIDAlias) ([][]byte, error) { + utxos := [][]byte{} + + // Used for pagination + var startAddr ids.ShortID + var startUTXOID ids.ID + for { + var utxoPage [][]byte + var err error + + // GetUTXOs controlled by addr + utxoPage, startAddr, startUTXOID, err = b.pClient.GetAtomicUTXOs( + ctx, + []ids.ShortID{addr}, + sourceChain.String(), + b.getUTXOsPageSize, + startAddr, + startUTXOID, + ) + if err != nil { + return nil, errUnableToGetUTXOs + } + + utxos = append(utxos, utxoPage...) + + // Fetch next page only if there may be more UTXOs + if len(utxoPage) < int(b.getUTXOsPageSize) { + break + } + } + + return utxos, nil +} diff --git a/server/service/backend/pchain/account_test.go b/server/service/backend/pchain/account_test.go new file mode 100644 index 0000000..7b28b12 --- /dev/null +++ b/server/service/backend/pchain/account_test.go @@ -0,0 +1,544 @@ +package pchain + +import ( + "context" + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/vms/avm" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/coinbase/rosetta-sdk-go/types" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/mapper" + "github.com/ava-labs/avalanche-rosetta/service" + "github.com/ava-labs/avalanche-rosetta/service/backend/pchain/indexer" + + pmapper "github.com/ava-labs/avalanche-rosetta/mapper/pchain" +) + +type utxo struct { + id string + amount uint64 +} + +var ( + utxos = []utxo{ + {"NGcWaGCzBUtUsD85wDuX1DwbHFkvMHwJ9tDFiN7HCCnVcB9B8:0", 1000000000}, + {"pyQfA1Aq9vLaDETjeQe5DAwVxr2KAYdHg4CHzawmaj9oA6ppn:0", 2000000000}, + } + blockID, _ = ids.FromString("mq1enPCRAwWyRjFNY8nSmkLde6U5huUcp9PXueF2h7Kjb2csd") + blockHeight = uint64(42) + parsedBlock = &indexer.ParsedBlock{BlockID: blockID} + + pChainAddr = "P-avax1yp8v6x7kf7ar2q5g0cs0a9jk4cmt0sgam72zfz" + + dummyGenesis = &indexer.ParsedGenesisBlock{} + + mockAssetDescription = &avm.GetAssetDescriptionReply{ + Name: "Avalanche", + Symbol: mapper.AtomicAvaxCurrency.Symbol, + Denomination: 9, + } +) + +func TestAccountBalance(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + pChainMock := client.NewMockPChainClient(ctrl) + parserMock := indexer.NewMockParser(ctrl) + parserMock.EXPECT().GetGenesisBlock(ctx).Return(dummyGenesis, nil) + parserMock.EXPECT().ParseNonGenesisBlock(ctx, "", blockHeight).Return(parsedBlock, nil).AnyTimes() + backend, err := NewBackend( + service.ModeOnline, + pChainMock, + parserMock, + avaxAssetID, + pChainNetworkIdentifier, + avalancheNetworkID, + ) + require.NoError(t, err) + backend.getUTXOsPageSize = 2 + + t.Run("Account Balance Test", func(t *testing.T) { + require := require.New(t) + + addr, err := address.ParseToID(pChainAddr) + require.NoError(err) + utxo0Bytes := makeUtxoBytes(t, backend, utxos[0].id, utxos[0].amount) + utxo1Bytes := makeUtxoBytes(t, backend, utxos[1].id, utxos[1].amount) + utxo1ID, err := mapper.DecodeUTXOID(utxos[1].id) + require.NoError(err) + stakeUtxoBytes := makeStakeUtxoBytes(t, backend, utxos[1].amount) + + // Mock on GetAssetDescription + pChainMock.EXPECT().GetAssetDescription(ctx, mapper.AtomicAvaxCurrency.Symbol).Return(mockAssetDescription, nil).AnyTimes() + + // once before other calls, once after + pChainMock.EXPECT().GetHeight(ctx).Return(blockHeight, nil).Times(2) + // Make sure pagination works as well + pageSize := uint32(2) + backend.getUTXOsPageSize = pageSize + pChainMock.EXPECT().GetAtomicUTXOs(ctx, []ids.ShortID{addr}, "", pageSize, ids.ShortEmpty, ids.Empty). + Return([][]byte{utxo0Bytes, utxo1Bytes}, addr, utxo1ID.InputID(), nil) + pChainMock.EXPECT().GetAtomicUTXOs(ctx, []ids.ShortID{addr}, "", pageSize, addr, utxo1ID.InputID()). + Return([][]byte{utxo1Bytes}, addr, utxo1ID.InputID(), nil) + pChainMock.EXPECT().GetStake(ctx, []ids.ShortID{addr}, false).Return(map[ids.ID]uint64{}, [][]byte{stakeUtxoBytes}, nil) + + resp, terr := backend.AccountBalance( + ctx, + &types.AccountBalanceRequest{ + NetworkIdentifier: &types.NetworkIdentifier{ + Network: constants.TestnetNetwork, + SubNetworkIdentifier: &types.SubNetworkIdentifier{ + Network: constants.PChain.String(), + }, + }, + AccountIdentifier: &types.AccountIdentifier{ + Address: pChainAddr, + }, + Currencies: []*types.Currency{ + mapper.AtomicAvaxCurrency, + }, + }, + ) + + require.Nil(terr) + require.Equal([]*types.Amount{ + { + Value: "5000000000", // 1B + 2B from UTXOs, 1B from staked + Currency: mapper.AtomicAvaxCurrency, + }, + }, resp.Balances) + }) + + t.Run("Account Balance should return total of shared memory balance", func(t *testing.T) { + require := require.New(t) + + // Mock on GetUTXOs + utxo0Bytes := makeUtxoBytes(t, backend, utxos[0].id, utxos[0].amount) + utxo1Bytes := makeUtxoBytes(t, backend, utxos[1].id, utxos[1].amount) + utxo1ID, err := mapper.DecodeUTXOID(utxos[1].id) + require.NoError(err) + pChainAddrID, err := address.ParseToID(pChainAddr) + require.NoError(err) + + // once before other calls, once after + pChainMock.EXPECT().GetHeight(ctx).Return(blockHeight, nil).Times(2) + pageSize := uint32(1024) + backend.getUTXOsPageSize = pageSize + pChainMock.EXPECT().GetAtomicUTXOs(ctx, []ids.ShortID{pChainAddrID}, constants.CChain.String(), pageSize, ids.ShortEmpty, ids.Empty). + Return([][]byte{utxo0Bytes, utxo1Bytes}, pChainAddrID, utxo1ID.InputID(), nil) + pChainMock.EXPECT().GetAtomicUTXOs(ctx, []ids.ShortID{pChainAddrID}, constants.XChain.String(), pageSize, ids.ShortEmpty, ids.Empty). + Return([][]byte{}, pChainAddrID, ids.Empty, nil) + + resp, terr := backend.AccountBalance( + ctx, + &types.AccountBalanceRequest{ + NetworkIdentifier: &types.NetworkIdentifier{ + Network: constants.TestnetNetwork, + SubNetworkIdentifier: &types.SubNetworkIdentifier{ + Network: constants.PChain.String(), + }, + }, + AccountIdentifier: &types.AccountIdentifier{ + Address: pChainAddr, + SubAccount: &types.SubAccountIdentifier{Address: pmapper.SubAccountTypeSharedMemory}, + }, + Currencies: []*types.Currency{ + mapper.AtomicAvaxCurrency, + }, + }) + require.Nil(terr) + require.Equal(&types.AccountBalanceResponse{ + BlockIdentifier: &types.BlockIdentifier{ + Index: int64(blockHeight), + Hash: parsedBlock.BlockID.String(), + }, + Balances: []*types.Amount{{ + Value: "3000000000", + Currency: mapper.AtomicAvaxCurrency, + }}, + }, resp) + }) + + t.Run("Account Balance should error if new block was added while fetching UTXOs", func(t *testing.T) { + require := require.New(t) + addr, err := address.ParseToID(pChainAddr) + require.NoError(err) + + pageSize := uint32(2) + backend.getUTXOsPageSize = pageSize + pChainMock.EXPECT().GetHeight(ctx).Return(blockHeight, nil) + pChainMock.EXPECT().GetAtomicUTXOs(ctx, []ids.ShortID{addr}, "", pageSize, ids.ShortEmpty, ids.Empty). + Return([][]byte{}, addr, ids.Empty, nil) + pChainMock.EXPECT().GetStake(ctx, []ids.ShortID{addr}, false).Return(map[ids.ID]uint64{}, [][]byte{}, nil) + // return blockHeight + 1 to indicate a new block arrival + pChainMock.EXPECT().GetHeight(ctx).Return(blockHeight+1, nil) + + resp, terr := backend.AccountBalance( + ctx, + &types.AccountBalanceRequest{ + NetworkIdentifier: &types.NetworkIdentifier{ + Network: constants.TestnetNetwork, + SubNetworkIdentifier: &types.SubNetworkIdentifier{ + Network: constants.PChain.String(), + }, + }, + AccountIdentifier: &types.AccountIdentifier{ + Address: pChainAddr, + }, + Currencies: []*types.Currency{ + mapper.AtomicAvaxCurrency, + }, + }, + ) + + require.Nil(resp) + require.Equal("Internal server error", terr.Message) + require.Equal("new block added while fetching utxos", terr.Details["error"]) + }) +} + +func TestAccountPendingRewardsBalance(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + pChainMock := client.NewMockPChainClient(ctrl) + parserMock := indexer.NewMockParser(ctrl) + + parserMock.EXPECT().GetGenesisBlock(ctx).Return(dummyGenesis, nil) + parserMock.EXPECT().ParseNonGenesisBlock(ctx, "", blockHeight).Return(parsedBlock, nil).AnyTimes() + + validator1NodeID, err := ids.NodeIDFromString("NodeID-Bvsx89JttQqhqdgwtizAPoVSNW74Xcr2S") + require.NoError(t, err) + validator1Reward := uint64(100000) + validator1AddressStr := "P-fuji1csj0hzu7rtljuhqnzp8m9shawlcefuvyl0m3e9" + validator1Address, err := address.ParseToID(validator1AddressStr) + require.NoError(t, err) + validator1ValidationRewardOwner := &platformvm.ClientOwner{Addresses: []ids.ShortID{validator1Address}} + + delegate1Reward := uint64(20000) + delegate1AddressStr := "P-fuji1raffss40pyr7hdhyp7p4hs6p049hjlc60xxwks" + delegate1Address, err := address.ParseToID(delegate1AddressStr) + require.NoError(t, err) + delegate1RewardOwner := &platformvm.ClientOwner{Addresses: []ids.ShortID{delegate1Address}} + + delegate2Reward := uint64(30000) + delegate2AddressStr := "P-fuji1tlt564kc8mqwr575lyg539r8h6xg7hfmgxnkcg" + delegate2Address, err := address.ParseToID(delegate2AddressStr) + require.NoError(t, err) + delegate2RewardOwner := &platformvm.ClientOwner{Addresses: []ids.ShortID{delegate2Address}} + + validators := []platformvm.ClientPermissionlessValidator{ + { + ClientStaker: platformvm.ClientStaker{NodeID: validator1NodeID}, + ValidationRewardOwner: validator1ValidationRewardOwner, + PotentialReward: &validator1Reward, + DelegationFee: 10, + Delegators: []platformvm.ClientDelegator{ + { + RewardOwner: delegate1RewardOwner, + PotentialReward: &delegate1Reward, + }, + { + RewardOwner: delegate2RewardOwner, + PotentialReward: &delegate2Reward, + }, + }, + }, + } + + backend, err := NewBackend( + service.ModeOnline, + pChainMock, + parserMock, + avaxAssetID, + pChainNetworkIdentifier, + avalancheNetworkID, + ) + require.NoError(t, err) + + t.Run("Pending Rewards Validator By NodeID", func(t *testing.T) { + pChainMock.EXPECT().GetCurrentValidators(ctx, ids.Empty, []ids.NodeID{validator1NodeID}).Return(validators, nil) + pChainMock.EXPECT().GetHeight(ctx).Return(blockHeight, nil) + + resp, err := backend.AccountBalance( + ctx, + &types.AccountBalanceRequest{ + NetworkIdentifier: &types.NetworkIdentifier{ + Network: constants.TestnetNetwork, + SubNetworkIdentifier: &types.SubNetworkIdentifier{ + Network: constants.PChain.String(), + }, + }, + AccountIdentifier: &types.AccountIdentifier{ + Address: validator1AddressStr, + SubAccount: &types.SubAccountIdentifier{ + Address: validator1NodeID.String(), + }, + }, + }, + ) + + expected := &types.AccountBalanceResponse{ + BlockIdentifier: &types.BlockIdentifier{ + Index: int64(blockHeight), + Hash: parsedBlock.BlockID.String(), + }, + Balances: []*types.Amount{ + { + Value: "105000", + Currency: mapper.AtomicAvaxCurrency, + Metadata: map[string]interface{}{ + pmapper.MetadataValidatorRewards: "100000", // 100000 from validation + pmapper.MetadataDelegationFeeRewards: "5000", // 10% fee of total 50000 delegation + pmapper.MetadataDelegationRewards: "0", + }, + }, + }, + } + + require.Nil(t, err) + require.Equal(t, expected, resp) + }) + + t.Run("Pending Rewards Delegate by NodeID", func(t *testing.T) { + pChainMock.EXPECT().GetCurrentValidators(ctx, ids.Empty, []ids.NodeID{validator1NodeID}).Return(validators, nil) + pChainMock.EXPECT().GetHeight(ctx).Return(blockHeight, nil) + + resp, err := backend.AccountBalance( + ctx, + &types.AccountBalanceRequest{ + NetworkIdentifier: &types.NetworkIdentifier{ + Network: constants.TestnetNetwork, + SubNetworkIdentifier: &types.SubNetworkIdentifier{ + Network: constants.PChain.String(), + }, + }, + AccountIdentifier: &types.AccountIdentifier{ + Address: delegate1AddressStr, + SubAccount: &types.SubAccountIdentifier{ + Address: validator1NodeID.String(), + }, + }, + }, + ) + + expected := &types.AccountBalanceResponse{ + BlockIdentifier: &types.BlockIdentifier{ + Index: int64(blockHeight), + Hash: parsedBlock.BlockID.String(), + }, + Balances: []*types.Amount{ + { + Value: "18000", + Currency: mapper.AtomicAvaxCurrency, + Metadata: map[string]interface{}{ + pmapper.MetadataDelegationRewards: "18000", // 10 percent goes to validator, remaining is here + pmapper.MetadataValidatorRewards: "0", + pmapper.MetadataDelegationFeeRewards: "0", + }, + }, + }, + } + + require.Nil(t, err) + require.Equal(t, expected, resp) + }) +} + +func TestAccountCoins(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + pChainMock := client.NewMockPChainClient(ctrl) + parserMock := indexer.NewMockParser(ctrl) + parserMock.EXPECT().GetGenesisBlock(ctx).Return(dummyGenesis, nil) + parserMock.EXPECT().ParseNonGenesisBlock(ctx, "", blockHeight).Return(parsedBlock, nil).AnyTimes() + backend, err := NewBackend( + service.ModeOnline, + pChainMock, + parserMock, + avaxAssetID, + pChainNetworkIdentifier, + avalancheNetworkID, + ) + require.NoError(t, err) + + t.Run("Account Coins Test regular coins", func(t *testing.T) { + require := require.New(t) + + // Mock on GetAssetDescription + pChainMock.EXPECT().GetAssetDescription(ctx, mapper.AtomicAvaxCurrency.Symbol).Return(mockAssetDescription, nil) + + // Mock on GetUTXOs + utxo0Bytes := makeUtxoBytes(t, backend, utxos[0].id, utxos[0].amount) + utxo1Bytes := makeUtxoBytes(t, backend, utxos[1].id, utxos[1].amount) + utxo1ID, err := mapper.DecodeUTXOID(utxos[1].id) + require.NoError(err) + pChainAddrID, err := address.ParseToID(pChainAddr) + require.NoError(err) + + // once before other calls, once after + pChainMock.EXPECT().GetHeight(ctx).Return(blockHeight, nil).Times(2) + // Make sure pagination works as well + pageSize := uint32(2) + backend.getUTXOsPageSize = pageSize + pChainMock.EXPECT().GetAtomicUTXOs(ctx, []ids.ShortID{pChainAddrID}, "", pageSize, ids.ShortEmpty, ids.Empty). + Return([][]byte{utxo0Bytes, utxo1Bytes}, pChainAddrID, utxo1ID.InputID(), nil) + pChainMock.EXPECT().GetAtomicUTXOs(ctx, []ids.ShortID{pChainAddrID}, "", pageSize, pChainAddrID, utxo1ID.InputID()). + Return([][]byte{utxo1Bytes}, pChainAddrID, utxo1ID.InputID(), nil) + + resp, terr := backend.AccountCoins( + ctx, + &types.AccountCoinsRequest{ + NetworkIdentifier: &types.NetworkIdentifier{ + Network: constants.TestnetNetwork, + SubNetworkIdentifier: &types.SubNetworkIdentifier{ + Network: constants.PChain.String(), + }, + }, + AccountIdentifier: &types.AccountIdentifier{ + Address: pChainAddr, + }, + Currencies: []*types.Currency{ + mapper.AtomicAvaxCurrency, + }, + }) + + require.Nil(terr) + require.Equal(&types.AccountCoinsResponse{ + BlockIdentifier: &types.BlockIdentifier{ + Index: int64(blockHeight), + Hash: parsedBlock.BlockID.String(), + }, + Coins: []*types.Coin{ + { + CoinIdentifier: &types.CoinIdentifier{ + Identifier: "NGcWaGCzBUtUsD85wDuX1DwbHFkvMHwJ9tDFiN7HCCnVcB9B8:0", + }, + Amount: &types.Amount{ + Value: "1000000000", + Currency: mapper.AtomicAvaxCurrency, + }, + }, + { + CoinIdentifier: &types.CoinIdentifier{ + Identifier: "pyQfA1Aq9vLaDETjeQe5DAwVxr2KAYdHg4CHzawmaj9oA6ppn:0", + }, + Amount: &types.Amount{ + Value: "2000000000", + Currency: mapper.AtomicAvaxCurrency, + }, + }, + }, + }, resp) + }) + + t.Run("Account Coins Test shared memory coins", func(t *testing.T) { + require := require.New(t) + // Mock on GetAssetDescription + pChainMock.EXPECT().GetAssetDescription(ctx, mapper.AtomicAvaxCurrency.Symbol).Return(mockAssetDescription, nil) + + // Mock on GetUTXOs + utxo0Bytes := makeUtxoBytes(t, backend, utxos[0].id, utxos[0].amount) + utxo0ID, err := mapper.DecodeUTXOID(utxos[0].id) + require.NoError(err) + utxo1Bytes := makeUtxoBytes(t, backend, utxos[1].id, utxos[1].amount) + utxo1ID, err := mapper.DecodeUTXOID(utxos[1].id) + require.NoError(err) + pChainAddrID, err := address.ParseToID(pChainAddr) + require.NoError(err) + + // once before other calls, once after + pChainMock.EXPECT().GetHeight(ctx).Return(blockHeight, nil).Times(2) + pageSize := uint32(1024) + backend.getUTXOsPageSize = pageSize + pChainMock.EXPECT().GetAtomicUTXOs(ctx, []ids.ShortID{pChainAddrID}, constants.CChain.String(), pageSize, ids.ShortEmpty, ids.Empty). + Return([][]byte{utxo0Bytes}, pChainAddrID, utxo0ID.InputID(), nil) + pChainMock.EXPECT().GetAtomicUTXOs(ctx, []ids.ShortID{pChainAddrID}, constants.XChain.String(), pageSize, ids.ShortEmpty, ids.Empty). + Return([][]byte{utxo1Bytes}, pChainAddrID, utxo1ID.InputID(), nil) + + resp, terr := backend.AccountCoins( + ctx, + &types.AccountCoinsRequest{ + NetworkIdentifier: &types.NetworkIdentifier{ + Network: constants.TestnetNetwork, + SubNetworkIdentifier: &types.SubNetworkIdentifier{ + Network: constants.PChain.String(), + }, + }, + AccountIdentifier: &types.AccountIdentifier{ + Address: pChainAddr, + SubAccount: &types.SubAccountIdentifier{Address: pmapper.SubAccountTypeSharedMemory}, + }, + Currencies: []*types.Currency{ + mapper.AtomicAvaxCurrency, + }, + }) + + expected := &types.AccountCoinsResponse{ + BlockIdentifier: &types.BlockIdentifier{ + Index: int64(blockHeight), + Hash: parsedBlock.BlockID.String(), + }, + Coins: []*types.Coin{ + { + CoinIdentifier: &types.CoinIdentifier{ + Identifier: "NGcWaGCzBUtUsD85wDuX1DwbHFkvMHwJ9tDFiN7HCCnVcB9B8:0", + }, + Amount: &types.Amount{ + Value: "1000000000", + Currency: mapper.AtomicAvaxCurrency, + }, + }, + { + CoinIdentifier: &types.CoinIdentifier{ + Identifier: "pyQfA1Aq9vLaDETjeQe5DAwVxr2KAYdHg4CHzawmaj9oA6ppn:0", + }, + Amount: &types.Amount{ + Value: "2000000000", + Currency: mapper.AtomicAvaxCurrency, + }, + }, + }, + } + + require.Nil(terr) + require.Equal(expected, resp) + }) +} + +func makeUtxoBytes(t *testing.T, backend *Backend, utxoIDStr string, amount uint64) []byte { + utxoID, err := mapper.DecodeUTXOID(utxoIDStr) + if err != nil { + t.Fail() + return nil + } + + utxoBytes, err := backend.codec.Marshal(0, &avax.UTXO{ + UTXOID: *utxoID, + Out: &secp256k1fx.TransferOutput{Amt: amount}, + }) + if err != nil { + t.Fail() + } + + return utxoBytes +} + +func makeStakeUtxoBytes(t *testing.T, backend *Backend, amount uint64) []byte { + utxoBytes, err := backend.codec.Marshal(0, &avax.TransferableOutput{ + Out: &secp256k1fx.TransferOutput{Amt: amount}, + }) + if err != nil { + t.Fail() + } + + return utxoBytes +} diff --git a/server/service/backend/pchain/backend.go b/server/service/backend/pchain/backend.go new file mode 100644 index 0000000..22da846 --- /dev/null +++ b/server/service/backend/pchain/backend.go @@ -0,0 +1,122 @@ +package pchain + +import ( + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/platformvm/block" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/mapper" + "github.com/ava-labs/avalanche-rosetta/service" + "github.com/ava-labs/avalanche-rosetta/service/backend/pchain/indexer" + + pmapper "github.com/ava-labs/avalanche-rosetta/mapper/pchain" +) + +var ( + _ service.ConstructionBackend = &Backend{} + _ service.NetworkBackend = &Backend{} + _ service.AccountBackend = &Backend{} + _ service.BlockBackend = &Backend{} +) + +type Backend struct { + genesisHandler + networkID *types.NetworkIdentifier + networkHRP string + avalancheNetworkID uint32 + pClient client.PChainClient + indexerParser indexer.Parser + getUTXOsPageSize uint32 + codec codec.Manager + codecVersion uint16 + avaxAssetID ids.ID + txParserCfg pmapper.TxParserConfig +} + +// NewBackend creates a P-chain service backend +func NewBackend( + nodeMode string, + pClient client.PChainClient, + indexerParser indexer.Parser, + assetID ids.ID, + networkIdentifier *types.NetworkIdentifier, + avalancheNetworkID uint32, +) (*Backend, error) { + genHandler, err := newGenesisHandler(indexerParser) + if err != nil { + return nil, err + } + + b := &Backend{ + genesisHandler: genHandler, + networkID: networkIdentifier, + pClient: pClient, + getUTXOsPageSize: 1024, + codec: block.Codec, + codecVersion: block.CodecVersion, + indexerParser: indexerParser, + avaxAssetID: assetID, + avalancheNetworkID: avalancheNetworkID, + } + + if nodeMode == service.ModeOnline { + var err error + if b.networkHRP, err = mapper.GetHRP(b.networkID); err != nil { + return nil, err + } + } + + b.txParserCfg = pmapper.TxParserConfig{ + IsConstruction: false, + Hrp: b.networkHRP, + ChainIDs: nil, + AvaxAssetID: b.avaxAssetID, + PChainClient: b.pClient, + } + + return b, nil +} + +// ShouldHandleRequest returns whether a given request should be handled by this backend +func (*Backend) ShouldHandleRequest(req interface{}) bool { + switch r := req.(type) { + case *types.AccountBalanceRequest: + return isPChain(r.NetworkIdentifier) + case *types.AccountCoinsRequest: + return isPChain(r.NetworkIdentifier) + case *types.BlockRequest: + return isPChain(r.NetworkIdentifier) + case *types.BlockTransactionRequest: + return isPChain(r.NetworkIdentifier) + case *types.ConstructionDeriveRequest: + return isPChain(r.NetworkIdentifier) + case *types.ConstructionMetadataRequest: + return isPChain(r.NetworkIdentifier) + case *types.ConstructionPreprocessRequest: + return isPChain(r.NetworkIdentifier) + case *types.ConstructionPayloadsRequest: + return isPChain(r.NetworkIdentifier) + case *types.ConstructionParseRequest: + return isPChain(r.NetworkIdentifier) + case *types.ConstructionCombineRequest: + return isPChain(r.NetworkIdentifier) + case *types.ConstructionHashRequest: + return isPChain(r.NetworkIdentifier) + case *types.ConstructionSubmitRequest: + return isPChain(r.NetworkIdentifier) + case *types.NetworkRequest: + return isPChain(r.NetworkIdentifier) + } + + return false +} + +// isPChain checks network identifier to make sure sub-network identifier set to "P" +func isPChain(reqNetworkID *types.NetworkIdentifier) bool { + return reqNetworkID != nil && + reqNetworkID.SubNetworkIdentifier != nil && + reqNetworkID.SubNetworkIdentifier.Network == constants.PChain.String() +} diff --git a/server/service/backend/pchain/backend_test.go b/server/service/backend/pchain/backend_test.go new file mode 100644 index 0000000..a28197f --- /dev/null +++ b/server/service/backend/pchain/backend_test.go @@ -0,0 +1,79 @@ +package pchain + +import ( + "context" + "fmt" + "testing" + + "github.com/coinbase/rosetta-sdk-go/types" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/service" + "github.com/ava-labs/avalanche-rosetta/service/backend/pchain/indexer" +) + +func TestShouldHandleRequest(t *testing.T) { + pChainNetworkIdentifier := &types.NetworkIdentifier{ + Blockchain: service.BlockchainName, + Network: constants.TestnetNetwork, + SubNetworkIdentifier: &types.SubNetworkIdentifier{ + Network: constants.PChain.String(), + }, + } + + cChainNetworkIdentifier := &types.NetworkIdentifier{ + Blockchain: service.BlockchainName, + Network: constants.TestnetNetwork, + } + + ctx := context.Background() + ctrl := gomock.NewController(t) + clientMock := client.NewMockPChainClient(ctrl) + parserMock := indexer.NewMockParser(ctrl) + parserMock.EXPECT().GetGenesisBlock(ctx).Return(dummyGenesis, nil) + backend, err := NewBackend( + service.ModeOnline, + clientMock, + parserMock, + avaxAssetID, + pChainNetworkIdentifier, + avalancheNetworkID, + ) + require.NoError(t, err) + + testData := []struct { + name string + networkIdentifier *types.NetworkIdentifier + expected bool + }{ + {"p-chain", pChainNetworkIdentifier, true}, + {"c-chain", cChainNetworkIdentifier, false}, + } + + for _, tc := range testData { + t.Run(fmt.Sprintf("should handle request for %s should return %t", tc.name, tc.expected), func(t *testing.T) { + requests := []interface{}{ + &types.ConstructionDeriveRequest{NetworkIdentifier: tc.networkIdentifier}, + &types.ConstructionPreprocessRequest{NetworkIdentifier: tc.networkIdentifier}, + &types.ConstructionMetadataRequest{NetworkIdentifier: tc.networkIdentifier}, + &types.ConstructionPayloadsRequest{NetworkIdentifier: tc.networkIdentifier}, + &types.ConstructionCombineRequest{NetworkIdentifier: tc.networkIdentifier}, + &types.ConstructionHashRequest{NetworkIdentifier: tc.networkIdentifier}, + &types.ConstructionSubmitRequest{NetworkIdentifier: tc.networkIdentifier}, + &types.AccountBalanceRequest{NetworkIdentifier: tc.networkIdentifier}, + &types.AccountCoinsRequest{NetworkIdentifier: tc.networkIdentifier}, + &types.AccountBalanceRequest{NetworkIdentifier: tc.networkIdentifier}, + &types.AccountCoinsRequest{NetworkIdentifier: tc.networkIdentifier}, + &types.BlockRequest{NetworkIdentifier: tc.networkIdentifier}, + &types.BlockTransactionRequest{NetworkIdentifier: tc.networkIdentifier}, + &types.NetworkRequest{NetworkIdentifier: tc.networkIdentifier}, + } + for _, r := range requests { + require.Equal(t, tc.expected, backend.ShouldHandleRequest(r)) + } + }) + } +} diff --git a/server/service/backend/pchain/block.go b/server/service/backend/pchain/block.go new file mode 100644 index 0000000..6755abb --- /dev/null +++ b/server/service/backend/pchain/block.go @@ -0,0 +1,239 @@ +package pchain + +import ( + "context" + + "github.com/ava-labs/avalanchego/api" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/formatting" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/coinbase/rosetta-sdk-go/types" + "golang.org/x/sync/errgroup" + + "github.com/ava-labs/avalanche-rosetta/service" + + pmapper "github.com/ava-labs/avalanche-rosetta/mapper/pchain" +) + +// Block implements the /block endpoint +func (b *Backend) Block(ctx context.Context, request *types.BlockRequest) (*types.BlockResponse, *types.Error) { + var blockIndex int64 + if request.BlockIdentifier.Index != nil { + blockIndex = *request.BlockIdentifier.Index + } + + var hash string + if request.BlockIdentifier.Hash != nil { + hash = *request.BlockIdentifier.Hash + } + + var ( + blkIdentifier *types.BlockIdentifier + parentBlkIdentifier *types.BlockIdentifier + blkTime int64 + rTxs []*types.Transaction + metadata map[string]interface{} + ) + + isGenesisReq, err := b.isGenesisBlockRequest(blockIndex, hash) + switch { + case err != nil: + // avalanchego node may be not ready or reachable + return nil, service.WrapError(service.ErrClientError, err) + + case isGenesisReq: + genesisTxs, err := b.getFullGenesisTxs() + if err != nil { + return nil, service.WrapError(service.ErrClientError, err) + } + rosettaTxs, err := pmapper.ParseRosettaTxs(b.txParserCfg, genesisTxs, nil) + if err != nil { + return nil, service.WrapError(service.ErrClientError, err) + } + genesisBlock := b.getGenesisBlock() + + blkIdentifier = b.getGenesisIdentifier() + + // Parent block identifier of genesis block is set to itself instead of the hash of the genesis state + // This is done as the genesis state hash cannot be used as a transaction id for the /block apis + // and the operations found in the genesis state are returned as operations of the genesis block. + parentBlkIdentifier = b.getGenesisIdentifier() + blkTime = genesisBlock.Timestamp + rTxs = rosettaTxs + metadata = map[string]interface{}{ + pmapper.MetadataMessage: genesisBlock.Message, + } + + default: + block, err := b.indexerParser.ParseNonGenesisBlock(ctx, hash, uint64(blockIndex)) + if err != nil { + return nil, service.WrapError(service.ErrClientError, err) + } + blockIndex = int64(block.Height) + + blkDeps, err := b.fetchBlkDependencies(ctx, block.Txs) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, err) + } + + rosettaTxs, err := pmapper.ParseRosettaTxs(b.txParserCfg, block.Txs, blkDeps) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, err) + } + + blkIdentifier = &types.BlockIdentifier{ + Index: blockIndex, + Hash: block.BlockID.String(), + } + parentBlkIdentifier = &types.BlockIdentifier{ + Index: blockIndex - 1, + Hash: block.ParentID.String(), + } + blkTime = block.Timestamp + rTxs = rosettaTxs + metadata = nil + } + + resp := &types.BlockResponse{ + Block: &types.Block{ + BlockIdentifier: blkIdentifier, + ParentBlockIdentifier: parentBlkIdentifier, + Timestamp: blkTime, + Transactions: rTxs, + Metadata: metadata, + }, + } + return resp, nil +} + +// BlockTransaction implements the /block/transaction endpoint. +func (b *Backend) BlockTransaction(ctx context.Context, request *types.BlockTransactionRequest) (*types.BlockTransactionResponse, *types.Error) { + var ( + targetTxs []*txs.Tx + dependencyTxs pmapper.BlockTxDependencies + ) + + isGenesisReq, err := b.isGenesisBlockRequest(request.BlockIdentifier.Index, request.BlockIdentifier.Hash) + switch { + case err != nil: + // avalanchego node may be not ready or reachable + return nil, service.WrapError(service.ErrClientError, err) + + case isGenesisReq: + genesisTxs, err := b.getFullGenesisTxs() + if err != nil { + return nil, service.WrapError(service.ErrClientError, err) + } + + targetTxs = genesisTxs + dependencyTxs = nil + + default: + block, err := b.indexerParser.ParseNonGenesisBlock(ctx, request.BlockIdentifier.Hash, uint64(request.BlockIdentifier.Index)) + if err != nil { + return nil, service.WrapError(service.ErrClientError, err) + } + deps, err := b.fetchBlkDependencies(ctx, block.Txs) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, err) + } + + targetTxs = block.Txs + dependencyTxs = deps + } + + rosettaTxs, err := pmapper.ParseRosettaTxs(b.txParserCfg, targetTxs, dependencyTxs) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, err) + } + for _, rTx := range rosettaTxs { + if rTx.TransactionIdentifier.Hash == request.TransactionIdentifier.Hash { + return &types.BlockTransactionResponse{ + Transaction: rTx, + }, nil + } + } + + return nil, service.ErrTransactionNotFound +} + +func (b *Backend) fetchBlkDependencies(ctx context.Context, txs []*txs.Tx) (pmapper.BlockTxDependencies, error) { + blockDeps := make(pmapper.BlockTxDependencies) + depsTxIDs := []ids.ID{} + for _, tx := range txs { + inputTxsIds, err := pmapper.GetTxDependenciesIDs(tx.Unsigned) + if err != nil { + return nil, err + } + depsTxIDs = append(depsTxIDs, inputTxsIds...) + } + + dependencyTxChan := make(chan *pmapper.SingleTxDependency, len(depsTxIDs)) + eg, ctx := errgroup.WithContext(ctx) + + for _, txID := range depsTxIDs { + txID := txID + eg.Go(func() error { + return b.fetchDependencyTx(ctx, txID, dependencyTxChan) + }) + } + if err := eg.Wait(); err != nil { + return nil, err + } + close(dependencyTxChan) + + for dTx := range dependencyTxChan { + blockDeps[dTx.Tx.ID()] = dTx + } + + return blockDeps, nil +} + +func (b *Backend) fetchDependencyTx(ctx context.Context, txID ids.ID, out chan *pmapper.SingleTxDependency) error { + // Genesis state contains initial allocation UTXOs. These are not technically part of a transaction. + // As a result, their UTXO id uses zero value transaction id. In that case, return genesis allocation data + if txID == ids.Empty { + allocationTx, err := b.buildGenesisAllocationTx() + if allocationTx != nil { + out <- &pmapper.SingleTxDependency{ + Tx: allocationTx, + } + } + return err + } + + txBytes, err := b.pClient.GetTx(ctx, txID) + if err != nil { + return err + } + + tx, err := txs.Parse(txs.Codec, txBytes) + if err != nil { + return err + } + + utxoBytes, err := b.pClient.GetRewardUTXOs(ctx, &api.GetTxArgs{ + TxID: txID, + Encoding: formatting.Hex, + }) + if err != nil { + return err + } + + utxos := []*avax.UTXO{} + for _, bytes := range utxoBytes { + utxo := avax.UTXO{} + _, err = b.codec.Unmarshal(bytes, &utxo) + if err != nil { + return err + } + utxos = append(utxos, &utxo) + } + out <- &pmapper.SingleTxDependency{ + Tx: tx, + RewardUTXOs: utxos, + } + + return nil +} diff --git a/server/service/backend/pchain/block_test.go b/server/service/backend/pchain/block_test.go new file mode 100644 index 0000000..ad42913 --- /dev/null +++ b/server/service/backend/pchain/block_test.go @@ -0,0 +1,142 @@ +package pchain + +import ( + "context" + "testing" + + "github.com/ava-labs/avalanchego/api" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/formatting" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/platformvm/block" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/coinbase/rosetta-sdk-go/types" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/service" + "github.com/ava-labs/avalanche-rosetta/service/backend/pchain/indexer" + + avaconstants "github.com/ava-labs/avalanchego/utils/constants" + avatypes "github.com/ava-labs/avalanchego/vms/types" +) + +func TestFetchBlkDependencies(t *testing.T) { + dummyGenesis = &indexer.ParsedGenesisBlock{} + + ctrl := gomock.NewController(t) + mockPClient := client.NewMockPChainClient(ctrl) + mockIndexerParser := indexer.NewMockParser(ctrl) + + ctx := context.Background() + + networkID := avaconstants.MainnetID + networkIdentifier := &types.NetworkIdentifier{ + Blockchain: service.BlockchainName, + Network: constants.MainnetNetwork, + SubNetworkIdentifier: &types.SubNetworkIdentifier{ + Network: constants.PChain.String(), + }, + } + + signedImportTx, err := makeImportTx(t, networkID) + require.NoError(t, err) + + genesisTxID := ids.Empty + nonGenesisTxID := signedImportTx.ID() + + tx := &txs.Tx{ + Unsigned: &txs.ExportTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: avalancheNetworkID, + BlockchainID: pChainID, + Ins: []*avax.TransferableInput{ + { + UTXOID: avax.UTXOID{ + // Genesis allocation input + TxID: genesisTxID, + OutputIndex: 1234, + }, + Asset: avax.Asset{ + ID: avaxAssetID, + }, + In: &secp256k1fx.TransferInput{ + Amt: 1000, + Input: secp256k1fx.Input{}, + }, + }, + { + UTXOID: avax.UTXOID{ + TxID: nonGenesisTxID, + OutputIndex: 1, + }, + Asset: avax.Asset{ + ID: avaxAssetID, + }, + In: &secp256k1fx.TransferInput{ + Amt: 2000, + Input: secp256k1fx.Input{}, + }, + }, + }, + }, + }, + DestinationChain: cChainID, + ExportedOutputs: nil, + }, + } + + mockIndexerParser.EXPECT().GetGenesisBlock(ctx).Return(dummyGenesis, nil) + mockPClient.EXPECT().GetTx(gomock.Any(), nonGenesisTxID).Return(signedImportTx.Bytes(), nil) + + mockPClient.EXPECT().GetRewardUTXOs(gomock.Any(), &api.GetTxArgs{ + TxID: nonGenesisTxID, + Encoding: formatting.Hex, + }).Return(nil, nil) + + backend, err := NewBackend(service.ModeOnline, mockPClient, mockIndexerParser, avaxAssetID, networkIdentifier, networkID) + require.NoError(t, err) + + deps, err := backend.fetchBlkDependencies(ctx, []*txs.Tx{tx}) + require.NoError(t, err) + + require.Len(t, deps, 2) + require.Equal(t, ids.Empty, deps[genesisTxID].Tx.ID()) + require.NotEqual(t, ids.Empty, deps[nonGenesisTxID].Tx.ID()) + require.Equal(t, signedImportTx, deps[nonGenesisTxID].Tx) +} + +func makeImportTx(t *testing.T, networkID uint32) (*txs.Tx, error) { + importTx := &txs.ImportTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: networkID, + Outs: []*avax.TransferableOutput{ + { + Asset: avax.Asset{ + ID: avaxAssetID, + }, + Out: &secp256k1fx.TransferOutput{ + Amt: 2000, + OutputOwners: secp256k1fx.OutputOwners{Addrs: []ids.ShortID{}}, + }, + }, + }, + Ins: []*avax.TransferableInput{}, + Memo: avatypes.JSONByteSlice{}, + }, + SyntacticallyVerified: false, + }, + SourceChain: cChainID, + ImportedInputs: []*avax.TransferableInput{}, + } + signedImportTx, err := txs.NewSigned(importTx, block.Codec, nil) + require.NoError(t, err) + signedImportTx.Creds = []verify.Verifiable{} + return signedImportTx, err +} diff --git a/server/service/backend/pchain/construction.go b/server/service/backend/pchain/construction.go new file mode 100644 index 0000000..fbaba57 --- /dev/null +++ b/server/service/backend/pchain/construction.go @@ -0,0 +1,350 @@ +package pchain + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math/big" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/rpc" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/mapper" + "github.com/ava-labs/avalanche-rosetta/service" + "github.com/ava-labs/avalanche-rosetta/service/backend/common" + + pmapper "github.com/ava-labs/avalanche-rosetta/mapper/pchain" +) + +var ( + errUnknownTxType = errors.New("unknown tx type") + errUndecodableTx = errors.New("undecodable transaction") +) + +// ConstructionDerive implements /construction/derive endpoint for P-chain +func (*Backend) ConstructionDerive(_ context.Context, req *types.ConstructionDeriveRequest) (*types.ConstructionDeriveResponse, *types.Error) { + return common.DeriveBech32Address(constants.PChain, req) +} + +// ConstructionPreprocess implements /construction/preprocess endpoint for P-chain +func (*Backend) ConstructionPreprocess( + _ context.Context, + req *types.ConstructionPreprocessRequest, +) (*types.ConstructionPreprocessResponse, *types.Error) { + matches, err := common.MatchOperations(req.Operations) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + reqMetadata := req.Metadata + if reqMetadata == nil { + reqMetadata = make(map[string]interface{}) + } + reqMetadata[pmapper.MetadataOpType] = matches[0].Operations[0].Type + + return &types.ConstructionPreprocessResponse{ + Options: reqMetadata, + }, nil +} + +// ConstructionMetadata implements /construction/metadata endpoint for P-chain +func (b *Backend) ConstructionMetadata( + ctx context.Context, + req *types.ConstructionMetadataRequest, +) (*types.ConstructionMetadataResponse, *types.Error) { + opMetadata, err := pmapper.ParseOpMetadata(req.Options) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + var suggestedFee *types.Amount + var metadata *pmapper.Metadata + switch opMetadata.Type { + case pmapper.OpImportAvax: + metadata, suggestedFee, err = b.buildImportMetadata(ctx, req.Options) + case pmapper.OpExportAvax: + metadata, suggestedFee, err = b.buildExportMetadata(ctx, req.Options) + case pmapper.OpAddValidator, pmapper.OpAddDelegator, pmapper.OpAddPermissionlessDelegator, pmapper.OpAddPermissionlessValidator: + metadata, suggestedFee, err = buildStakingMetadata(req.Options) + metadata.Threshold = opMetadata.Threshold + metadata.Locktime = opMetadata.Locktime + + default: + return nil, service.WrapError( + service.ErrInternalError, + fmt.Errorf("invalid tx type for building metadata: %s", opMetadata.Type), + ) + } + if err != nil { + return nil, service.WrapError(service.ErrInternalError, err) + } + + pChainID, err := b.pClient.GetBlockchainID(ctx, constants.PChain.String()) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + metadata.NetworkID = b.avalancheNetworkID + metadata.BlockchainID = pChainID + + metadataMap, err := mapper.MarshalJSONMap(metadata) + if err != nil { + return nil, service.WrapError(service.ErrInternalError, err) + } + + return &types.ConstructionMetadataResponse{ + Metadata: metadataMap, + SuggestedFee: []*types.Amount{suggestedFee}, + }, nil +} + +func (b *Backend) buildImportMetadata(ctx context.Context, options map[string]interface{}) (*pmapper.Metadata, *types.Amount, error) { + var preprocessOptions pmapper.ImportExportOptions + if err := mapper.UnmarshalJSONMap(options, &preprocessOptions); err != nil { + return nil, nil, err + } + + sourceChainID, err := b.pClient.GetBlockchainID(ctx, preprocessOptions.SourceChain) + if err != nil { + return nil, nil, err + } + + suggestedFee, err := b.getBaseTxFee(ctx) + if err != nil { + return nil, nil, err + } + + importMetadata := &pmapper.ImportMetadata{ + SourceChainID: sourceChainID, + } + + return &pmapper.Metadata{ImportMetadata: importMetadata}, suggestedFee, nil +} + +func (b *Backend) buildExportMetadata(ctx context.Context, options map[string]interface{}) (*pmapper.Metadata, *types.Amount, error) { + var preprocessOptions pmapper.ImportExportOptions + if err := mapper.UnmarshalJSONMap(options, &preprocessOptions); err != nil { + return nil, nil, err + } + + destinationChainID, err := b.pClient.GetBlockchainID(ctx, preprocessOptions.DestinationChain) + if err != nil { + return nil, nil, err + } + + suggestedFee, err := b.getBaseTxFee(ctx) + if err != nil { + return nil, nil, err + } + + exportMetadata := &pmapper.ExportMetadata{ + DestinationChain: preprocessOptions.DestinationChain, + DestinationChainID: destinationChainID, + } + + return &pmapper.Metadata{ExportMetadata: exportMetadata}, suggestedFee, nil +} + +func buildStakingMetadata(options map[string]interface{}) (*pmapper.Metadata, *types.Amount, error) { + var preprocessOptions pmapper.StakingOptions + if err := mapper.UnmarshalJSONMap(options, &preprocessOptions); err != nil { + return nil, nil, err + } + zeroAvax := mapper.AtomicAvaxAmount(big.NewInt(0)) + + return &pmapper.Metadata{ + StakingMetadata: &pmapper.StakingMetadata{ + NodeID: preprocessOptions.NodeID, + BLSPublicKey: preprocessOptions.BLSPublicKey, + BLSProofOfPossession: preprocessOptions.BLSProofOfPossession, + ValidationRewardsOwners: preprocessOptions.ValidationRewardsOwners, + DelegationRewardsOwners: preprocessOptions.DelegationRewardsOwners, + Start: preprocessOptions.Start, + End: preprocessOptions.End, + Subnet: preprocessOptions.Subnet, + Shares: preprocessOptions.Shares, + Locktime: preprocessOptions.Locktime, + Threshold: preprocessOptions.Threshold, + }, + }, zeroAvax, nil +} + +func (b *Backend) getBaseTxFee(ctx context.Context) (*types.Amount, error) { + fees, err := b.pClient.GetTxFee(ctx) + if err != nil { + return nil, err + } + + feeAmount := new(big.Int).SetUint64(uint64(fees.TxFee)) + suggestedFee := mapper.AtomicAvaxAmount(feeAmount) + return suggestedFee, nil +} + +// ConstructionPayloads implements /construction/payloads endpoint for P-chain +func (b *Backend) ConstructionPayloads(_ context.Context, req *types.ConstructionPayloadsRequest) (*types.ConstructionPayloadsResponse, *types.Error) { + builder := pTxBuilder{ + avaxAssetID: b.avaxAssetID, + codec: b.codec, + codecVersion: b.codecVersion, + } + return common.BuildPayloads(builder, req) +} + +// ConstructionParse implements /construction/parse endpoint for P-chain +func (b *Backend) ConstructionParse(_ context.Context, req *types.ConstructionParseRequest) (*types.ConstructionParseResponse, *types.Error) { + rosettaTx, err := b.parsePayloadTxFromString(req.Transaction) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + netID, _ := constants.FromString(rosettaTx.DestinationChain) + chainIDs := map[ids.ID]constants.ChainIDAlias{} + if rosettaTx.DestinationChainID != nil { + chainIDs[*rosettaTx.DestinationChainID] = netID + } + + txParser := pTxParser{ + hrp: b.networkHRP, + chainIDs: chainIDs, + avaxAssetID: b.avaxAssetID, + } + + return common.Parse(txParser, rosettaTx, req.Signed) +} + +// ConstructionCombine implements /construction/combine endpoint for P-chain +func (b *Backend) ConstructionCombine(_ context.Context, req *types.ConstructionCombineRequest) (*types.ConstructionCombineResponse, *types.Error) { + rosettaTx, err := b.parsePayloadTxFromString(req.UnsignedTransaction) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + return common.Combine(b, rosettaTx, req.Signatures) +} + +// CombineTx implements P-chain specific logic for combining unsigned transactions and signatures +func (*Backend) CombineTx(tx common.AvaxTx, signatures []*types.Signature) (common.AvaxTx, *types.Error) { + pTx, ok := tx.(*pTx) + if !ok { + return nil, service.WrapError(service.ErrInvalidInput, "invalid transaction") + } + + ins, err := getTxInputs(pTx.Tx.Unsigned) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + creds, err := common.BuildCredentialList(ins, signatures) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + unsignedBytes, err := pTx.Marshal() + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + pTx.Tx.Creds = creds + + signedBytes, err := pTx.Marshal() + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + pTx.Tx.SetBytes(unsignedBytes, signedBytes) + + return pTx, nil +} + +// getTxInputs fetches tx inputs based on the tx type. +func getTxInputs( + unsignedTx txs.UnsignedTx, +) ([]*avax.TransferableInput, error) { + // TODO: Move to using [txs.Visitor] from AvalancheGo + // Ref: https://github.com/ava-labs/avalanchego/blob/master/vms/platformvm/txs/visitor.go + switch utx := unsignedTx.(type) { + case *txs.AddValidatorTx: + return utx.Ins, nil + case *txs.AddSubnetValidatorTx: + return utx.Ins, nil + case *txs.AddDelegatorTx: + return utx.Ins, nil + case *txs.CreateChainTx: + return utx.Ins, nil + case *txs.CreateSubnetTx: + return utx.Ins, nil + case *txs.ImportTx: + return utx.ImportedInputs, nil + case *txs.ExportTx: + return utx.Ins, nil + case *txs.AdvanceTimeTx: + return nil, nil + case *txs.RewardValidatorTx: + return nil, nil + case *txs.TransformSubnetTx: + return utx.Ins, nil + case *txs.AddPermissionlessValidatorTx: + return utx.Ins, nil + case *txs.AddPermissionlessDelegatorTx: + return utx.Ins, nil + case *txs.TransferSubnetOwnershipTx: + return utx.Ins, nil + case *txs.BaseTx: + return utx.Ins, nil + default: + return nil, errUnknownTxType + } +} + +// ConstructionHash implements /construction/hash endpoint for P-chain +func (b *Backend) ConstructionHash( + _ context.Context, + req *types.ConstructionHashRequest, +) (*types.TransactionIdentifierResponse, *types.Error) { + rosettaTx, err := b.parsePayloadTxFromString(req.SignedTransaction) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + return common.HashTx(rosettaTx) +} + +// ConstructionSubmit implements /construction/submit endpoint for P-chain +func (b *Backend) ConstructionSubmit( + ctx context.Context, + req *types.ConstructionSubmitRequest, +) (*types.TransactionIdentifierResponse, *types.Error) { + rosettaTx, err := b.parsePayloadTxFromString(req.SignedTransaction) + if err != nil { + return nil, service.WrapError(service.ErrInvalidInput, err) + } + + return common.SubmitTx(ctx, b, rosettaTx) +} + +// IssueTx broadcasts given transaction on P-chain +func (b *Backend) IssueTx(ctx context.Context, txByte []byte, options ...rpc.Option) (ids.ID, error) { + return b.pClient.IssueTx(ctx, txByte, options...) +} + +func (b *Backend) parsePayloadTxFromString(transaction string) (*common.RosettaTx, error) { + // Unmarshal input transaction + payloadsTx := &common.RosettaTx{ + Tx: &pTx{ + Codec: b.codec, + CodecVersion: b.codecVersion, + }, + } + + err := json.Unmarshal([]byte(transaction), payloadsTx) + if err != nil { + return nil, errUndecodableTx + } + + return payloadsTx, payloadsTx.Tx.Initialize() +} diff --git a/server/service/backend/pchain/construction_test.go b/server/service/backend/pchain/construction_test.go new file mode 100644 index 0000000..fbd80af --- /dev/null +++ b/server/service/backend/pchain/construction_test.go @@ -0,0 +1,1014 @@ +package pchain + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "math/big" + "testing" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + "github.com/coinbase/rosetta-sdk-go/types" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/mapper" + "github.com/ava-labs/avalanche-rosetta/service" + "github.com/ava-labs/avalanche-rosetta/service/backend/common" + "github.com/ava-labs/avalanche-rosetta/service/backend/pchain/indexer" + + pmapper "github.com/ava-labs/avalanche-rosetta/mapper/pchain" + avaconstants "github.com/ava-labs/avalanchego/utils/constants" + avajson "github.com/ava-labs/avalanchego/utils/json" +) + +var ( + pChainNetworkIdentifier = &types.NetworkIdentifier{ + Blockchain: service.BlockchainName, + Network: constants.TestnetNetwork, + SubNetworkIdentifier: &types.SubNetworkIdentifier{ + Network: constants.PChain.String(), + }, + } + + cAccountIdentifier = &types.AccountIdentifier{Address: "C-fuji123zu6qwhtd9qdd45ryu3j0qtr325gjgddys6u8"} + pAccountIdentifier = &types.AccountIdentifier{Address: "P-fuji123zu6qwhtd9qdd45ryu3j0qtr325gjgddys6u8"} + stakeRewardAccount = &types.AccountIdentifier{Address: "P-fuji1ea7dxk8zazpyf8tgc8yg3xyfatey0deqvg9pv2"} + + cChainID, _ = ids.FromString("yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp") + pChainID = ids.Empty + + nodeID = "NodeID-Bvsx89JttQqhqdgwtizAPoVSNW74Xcr2S" + + avalancheNetworkID = avaconstants.FujiID + + avaxAssetID, _ = ids.FromString("U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK") + + txFee = 1_000_000 + + coinID1 = "2ryRVCwNSjEinTViuvDkzX41uQzx3g4babXxZMD46ZV1a9X4Eg:0" +) + +func buildRosettaSignerJSON(coinIdentifiers []string, signers []*types.AccountIdentifier) string { + importSigners := []*common.Signer{} + for i, s := range signers { + importSigners = append(importSigners, &common.Signer{ + CoinIdentifier: coinIdentifiers[i], + AccountIdentifier: s, + }) + } + bytes, _ := json.Marshal(importSigners) + return string(bytes) +} + +func TestConstructionDerive(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + pChainMock := client.NewMockPChainClient(ctrl) + parserMock := indexer.NewMockParser(ctrl) + parserMock.EXPECT().GetGenesisBlock(ctx).Return(dummyGenesis, nil) + backend, err := NewBackend( + service.ModeOnline, + pChainMock, + parserMock, + avaxAssetID, + pChainNetworkIdentifier, + avalancheNetworkID, + ) + require.NoError(t, err) + + t.Run("p-chain address", func(t *testing.T) { + require := require.New(t) + + src := "02e0d4392cfa224d4be19db416b3cf62e90fb2b7015e7b62a95c8cb490514943f6" + b, err := hex.DecodeString(src) + require.NoError(err) + + resp, terr := backend.ConstructionDerive( + context.Background(), + &types.ConstructionDeriveRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + PublicKey: &types.PublicKey{ + Bytes: b, + CurveType: types.Secp256k1, + }, + }, + ) + require.Nil(terr) + require.Equal( + "P-fuji15f9g0h5xkr5cp47n6u3qxj6yjtzzzrdr23a3tl", + resp.AccountIdentifier.Address, + ) + }) +} + +func TestExportTxConstruction(t *testing.T) { + exportOperations := []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{Index: 0}, + RelatedOperations: nil, + Type: pmapper.OpExportAvax, + Account: pAccountIdentifier, + Amount: mapper.AtomicAvaxAmount(big.NewInt(-1_000_000_000)), + CoinChange: &types.CoinChange{ + CoinIdentifier: &types.CoinIdentifier{Identifier: coinID1}, + CoinAction: types.CoinSpent, + }, + Metadata: map[string]interface{}{ + "type": pmapper.OpTypeInput, + "sig_indices": []interface{}{0.0}, + "locktime": 0.0, + }, + }, + { + OperationIdentifier: &types.OperationIdentifier{Index: 1}, + Type: pmapper.OpExportAvax, + Account: cAccountIdentifier, + Amount: mapper.AtomicAvaxAmount(big.NewInt(999_000_000)), + Metadata: map[string]interface{}{ + "type": pmapper.OpTypeExport, + "threshold": 1.0, + "locktime": 0.0, + }, + }, + } + + preprocessMetadata := map[string]interface{}{ + "destination_chain": constants.CChain.String(), + } + + metadataOptions := map[string]interface{}{ + "destination_chain": constants.CChain.String(), + "type": pmapper.OpExportAvax, + } + + payloadsMetadata := map[string]interface{}{ + "network_id": float64(avalancheNetworkID), + "destination_chain": constants.CChain.String(), + "destination_chain_id": cChainID.String(), + "blockchain_id": pChainID.String(), + } + + signers := []*types.AccountIdentifier{pAccountIdentifier} + exportSigners := buildRosettaSignerJSON([]string{coinID1}, signers) + + unsignedExportTx := "0x0000000000120000000500000000000000000000000000000000000000000000000000000000000000000000000000000001f52a5a6dd8f1b3fe05204bdab4f6bcb5a7059f88d0443c636f6c158f838dd1a8000000003d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa00000005000000003b9aca000000000100000000000000007fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d5000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa00000007000000003b8b87c0000000000000000000000001000000015445cd01d75b4a06b6b41939193c0b1c5544490d0000000065e8045f" + unsignedExportTxHash, err := hex.DecodeString("44d579f5cb3c83f4137223a0368721734b622ec392007760eed97f3f1a40c595") + require.NoError(t, err) + + signingPayloads := []*types.SigningPayload{ + { + AccountIdentifier: pAccountIdentifier, + Bytes: unsignedExportTxHash, + SignatureType: types.EcdsaRecovery, + }, + } + + signedExportTx := "0x0000000000120000000500000000000000000000000000000000000000000000000000000000000000000000000000000001f52a5a6dd8f1b3fe05204bdab4f6bcb5a7059f88d0443c636f6c158f838dd1a8000000003d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa00000005000000003b9aca000000000100000000000000007fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d5000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa00000007000000003b8b87c0000000000000000000000001000000015445cd01d75b4a06b6b41939193c0b1c5544490d0000000100000009000000017403e32bb967e71902a988b7da635b4bca2475eedbfd23176610a88162f3a92f20b61f2185825b04b7f8ee8c76427c8dc80eb6091f9e594ef259a59856e5401b0137dc0dc4" + signedExportTxSignature, err := hex.DecodeString("7403e32bb967e71902a988b7da635b4bca2475eedbfd23176610a88162f3a92f20b61f2185825b04b7f8ee8c76427c8dc80eb6091f9e594ef259a59856e5401b01") + require.NoError(t, err) + signedExportTxHash := "bG7jzw16x495XSFdhEavHWR836Ya5teoB1YxRC1inN3HEtqbs" + + wrappedTxFormat := `{"tx":"%s","signers":%s,"destination_chain":"%s","destination_chain_id":"%s"}` + wrappedUnsignedExportTx := fmt.Sprintf(wrappedTxFormat, unsignedExportTx, exportSigners, constants.CChain.String(), cChainID.String()) + wrappedSignedExportTx := fmt.Sprintf(wrappedTxFormat, signedExportTx, exportSigners, constants.CChain.String(), cChainID.String()) + + signatures := []*types.Signature{{ + SigningPayload: &types.SigningPayload{ + AccountIdentifier: pAccountIdentifier, + Bytes: unsignedExportTxHash, + SignatureType: types.EcdsaRecovery, + }, + SignatureType: types.EcdsaRecovery, + Bytes: signedExportTxSignature, + }} + + ctx := context.Background() + ctrl := gomock.NewController(t) + clientMock := client.NewMockPChainClient(ctrl) + parserMock := indexer.NewMockParser(ctrl) + parserMock.EXPECT().GetGenesisBlock(ctx).Return(dummyGenesis, nil) + backend, err := NewBackend( + service.ModeOnline, + clientMock, + parserMock, + avaxAssetID, + pChainNetworkIdentifier, + avalancheNetworkID, + ) + require.NoError(t, err) + + t.Run("preprocess endpoint", func(t *testing.T) { + resp, err := backend.ConstructionPreprocess( + ctx, + &types.ConstructionPreprocessRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Operations: exportOperations, + Metadata: preprocessMetadata, + }, + ) + require.Nil(t, err) + require.Equal(t, metadataOptions, resp.Options) + }) + + t.Run("metadata endpoint", func(t *testing.T) { + clientMock.EXPECT().GetTxFee(ctx).Return(&info.GetTxFeeResponse{TxFee: avajson.Uint64(txFee)}, nil) + clientMock.EXPECT().GetBlockchainID(ctx, constants.PChain.String()).Return(pChainID, nil) + clientMock.EXPECT().GetBlockchainID(ctx, constants.CChain.String()).Return(cChainID, nil) + + resp, err := backend.ConstructionMetadata( + ctx, + &types.ConstructionMetadataRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Options: metadataOptions, + }, + ) + require.Nil(t, err) + require.Equal(t, payloadsMetadata, resp.Metadata) + }) + + t.Run("payloads endpoint", func(t *testing.T) { + resp, err := backend.ConstructionPayloads( + ctx, + &types.ConstructionPayloadsRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Operations: exportOperations, + Metadata: payloadsMetadata, + }, + ) + require.Nil(t, err) + require.Equal(t, wrappedUnsignedExportTx, resp.UnsignedTransaction) + require.Equal(t, signingPayloads, resp.Payloads, + "signing payloads mismatch: %s %s", + marshalSigningPayloads(signingPayloads), + marshalSigningPayloads(resp.Payloads)) + }) + + t.Run("parse endpoint (unsigned)", func(t *testing.T) { + resp, err := backend.ConstructionParse( + ctx, + &types.ConstructionParseRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Transaction: wrappedUnsignedExportTx, + Signed: false, + }, + ) + require.Nil(t, err) + require.Nil(t, resp.AccountIdentifierSigners) + require.Equal(t, exportOperations, resp.Operations) + }) + + t.Run("combine endpoint", func(t *testing.T) { + resp, err := backend.ConstructionCombine( + ctx, + &types.ConstructionCombineRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + UnsignedTransaction: wrappedUnsignedExportTx, + Signatures: signatures, + }, + ) + + require.Nil(t, err) + require.Equal(t, wrappedSignedExportTx, resp.SignedTransaction) + }) + + t.Run("parse endpoint (signed)", func(t *testing.T) { + resp, err := backend.ConstructionParse( + ctx, + &types.ConstructionParseRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Transaction: wrappedSignedExportTx, + Signed: true, + }, + ) + require.Nil(t, err) + require.Equal(t, signers, resp.AccountIdentifierSigners) + require.Equal(t, exportOperations, resp.Operations) + }) + + t.Run("hash endpoint", func(t *testing.T) { + resp, err := backend.ConstructionHash(ctx, &types.ConstructionHashRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + SignedTransaction: wrappedSignedExportTx, + }) + require.Nil(t, err) + require.Equal(t, signedExportTxHash, resp.TransactionIdentifier.Hash) + }) + + t.Run("submit endpoint", func(t *testing.T) { + require := require.New(t) + + signedTxBytes, err := mapper.DecodeToBytes(signedExportTx) + require.NoError(err) + txID, err := ids.FromString(signedExportTxHash) + require.NoError(err) + + clientMock.EXPECT().IssueTx(ctx, signedTxBytes).Return(txID, nil) + + resp, terr := backend.ConstructionSubmit(ctx, &types.ConstructionSubmitRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + SignedTransaction: wrappedSignedExportTx, + }) + + require.Nil(terr) + require.Equal(signedExportTxHash, resp.TransactionIdentifier.Hash) + }) +} + +func TestImportTxConstruction(t *testing.T) { + importOperations := []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{Index: 0}, + RelatedOperations: nil, + Type: pmapper.OpImportAvax, + Account: cAccountIdentifier, + Amount: mapper.AtomicAvaxAmount(big.NewInt(-1_000_000_000)), + CoinChange: &types.CoinChange{ + CoinIdentifier: &types.CoinIdentifier{Identifier: coinID1}, + CoinAction: types.CoinSpent, + }, + Metadata: map[string]interface{}{ + "type": pmapper.OpTypeImport, + "sig_indices": []interface{}{0.0}, + "locktime": 0.0, + }, + }, + { + OperationIdentifier: &types.OperationIdentifier{Index: 1}, + Type: pmapper.OpImportAvax, + Account: pAccountIdentifier, + Amount: mapper.AtomicAvaxAmount(big.NewInt(999_000_000)), + Metadata: map[string]interface{}{ + "type": pmapper.OpTypeOutput, + "threshold": 1.0, + "locktime": 0.0, + }, + }, + } + + preprocessMetadata := map[string]interface{}{ + "source_chain": constants.CChain.String(), + } + + metadataOptions := map[string]interface{}{ + "source_chain": constants.CChain.String(), + "type": pmapper.OpImportAvax, + } + + payloadsMetadata := map[string]interface{}{ + "network_id": float64(avalancheNetworkID), + "source_chain_id": cChainID.String(), + "blockchain_id": pChainID.String(), + } + + signers := []*types.AccountIdentifier{cAccountIdentifier} + importSigners := buildRosettaSignerJSON([]string{coinID1}, signers) + + unsignedImportTx := "0x000000000011000000050000000000000000000000000000000000000000000000000000000000000000000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa00000007000000003b8b87c0000000000000000000000001000000015445cd01d75b4a06b6b41939193c0b1c5544490d00000000000000007fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d500000001f52a5a6dd8f1b3fe05204bdab4f6bcb5a7059f88d0443c636f6c158f838dd1a8000000003d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa00000005000000003b9aca000000000100000000000000004ce8b27d" + unsignedImportTxHash, err := hex.DecodeString("e9114ae12065d1f8631bc40729c806a3a4793de714001bfee66482f520dc1865") + require.NoError(t, err) + wrappedUnsignedImportTx := `{"tx":"` + unsignedImportTx + `","signers":` + importSigners + `}` //nolint:goconst + + signingPayloads := []*types.SigningPayload{ + { + AccountIdentifier: cAccountIdentifier, + Bytes: unsignedImportTxHash, + SignatureType: types.EcdsaRecovery, + }, + } + + signedImportTx := "0x000000000011000000050000000000000000000000000000000000000000000000000000000000000000000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa00000007000000003b8b87c0000000000000000000000001000000015445cd01d75b4a06b6b41939193c0b1c5544490d00000000000000007fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d500000001f52a5a6dd8f1b3fe05204bdab4f6bcb5a7059f88d0443c636f6c158f838dd1a8000000003d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa00000005000000003b9aca0000000001000000000000000100000009000000017403e32bb967e71902a988b7da635b4bca2475eedbfd23176610a88162f3a92f20b61f2185825b04b7f8ee8c76427c8dc80eb6091f9e594ef259a59856e5401b018ac25b4e" + signedImportTxSignature, err := hex.DecodeString("7403e32bb967e71902a988b7da635b4bca2475eedbfd23176610a88162f3a92f20b61f2185825b04b7f8ee8c76427c8dc80eb6091f9e594ef259a59856e5401b01") + require.NoError(t, err) + signedImportTxHash := "byyEVU6RL7PQNSVT8qEnybWGV5BbBfJwFV6bEDV5mkymXRz62" + + wrappedSignedImportTx := `{"tx":"` + signedImportTx + `","signers":` + importSigners + `}` + + signatures := []*types.Signature{{ + SigningPayload: &types.SigningPayload{ + AccountIdentifier: cAccountIdentifier, + Bytes: unsignedImportTxHash, + SignatureType: types.EcdsaRecovery, + }, + SignatureType: types.EcdsaRecovery, + Bytes: signedImportTxSignature, + }} + + ctx := context.Background() + ctrl := gomock.NewController(t) + clientMock := client.NewMockPChainClient(ctrl) + parserMock := indexer.NewMockParser(ctrl) + parserMock.EXPECT().GetGenesisBlock(ctx).Return(dummyGenesis, nil) + backend, err := NewBackend( + service.ModeOnline, + clientMock, + parserMock, + avaxAssetID, + pChainNetworkIdentifier, + avalancheNetworkID, + ) + require.NoError(t, err) + + t.Run("preprocess endpoint", func(t *testing.T) { + resp, err := backend.ConstructionPreprocess( + ctx, + &types.ConstructionPreprocessRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Operations: importOperations, + Metadata: preprocessMetadata, + }, + ) + require.Nil(t, err) + require.Equal(t, metadataOptions, resp.Options) + }) + + t.Run("metadata endpoint", func(t *testing.T) { + clientMock.EXPECT().GetTxFee(ctx).Return(&info.GetTxFeeResponse{TxFee: avajson.Uint64(txFee)}, nil) + clientMock.EXPECT().GetBlockchainID(ctx, constants.PChain.String()).Return(pChainID, nil) + clientMock.EXPECT().GetBlockchainID(ctx, constants.CChain.String()).Return(cChainID, nil) + + resp, err := backend.ConstructionMetadata( + ctx, + &types.ConstructionMetadataRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Options: metadataOptions, + }, + ) + require.Nil(t, err) + require.Equal(t, payloadsMetadata, resp.Metadata) + }) + + t.Run("payloads endpoint", func(t *testing.T) { + resp, err := backend.ConstructionPayloads( + ctx, + &types.ConstructionPayloadsRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Operations: importOperations, + Metadata: payloadsMetadata, + }, + ) + require.Nil(t, err) + require.Equal(t, wrappedUnsignedImportTx, resp.UnsignedTransaction) + require.Equal(t, signingPayloads, resp.Payloads, + "signing payloads mismatch: %s %s", + marshalSigningPayloads(signingPayloads), + marshalSigningPayloads(resp.Payloads)) + }) + + t.Run("parse endpoint (unsigned)", func(t *testing.T) { + resp, err := backend.ConstructionParse( + ctx, + &types.ConstructionParseRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Transaction: wrappedUnsignedImportTx, + Signed: false, + }, + ) + require.Nil(t, err) + require.Nil(t, resp.AccountIdentifierSigners) + require.Equal(t, importOperations, resp.Operations) + }) + + t.Run("combine endpoint", func(t *testing.T) { + resp, err := backend.ConstructionCombine( + ctx, + &types.ConstructionCombineRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + UnsignedTransaction: wrappedUnsignedImportTx, + Signatures: signatures, + }, + ) + + require.Nil(t, err) + require.Equal(t, wrappedSignedImportTx, resp.SignedTransaction) + }) + + t.Run("parse endpoint (signed)", func(t *testing.T) { + resp, err := backend.ConstructionParse( + ctx, + &types.ConstructionParseRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Transaction: wrappedSignedImportTx, + Signed: true, + }, + ) + require.Nil(t, err) + require.Equal(t, signers, resp.AccountIdentifierSigners) + require.Equal(t, importOperations, resp.Operations) + }) + + t.Run("hash endpoint", func(t *testing.T) { + resp, err := backend.ConstructionHash(ctx, &types.ConstructionHashRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + SignedTransaction: wrappedSignedImportTx, + }) + require.Nil(t, err) + require.Equal(t, signedImportTxHash, resp.TransactionIdentifier.Hash) + }) + + t.Run("submit endpoint", func(t *testing.T) { + require := require.New(t) + + signedTxBytes, err := mapper.DecodeToBytes(signedImportTx) + require.NoError(err) + txID, err := ids.FromString(signedImportTxHash) + require.NoError(err) + + clientMock.EXPECT().IssueTx(ctx, signedTxBytes).Return(txID, nil) + + resp, terr := backend.ConstructionSubmit(ctx, &types.ConstructionSubmitRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + SignedTransaction: wrappedSignedImportTx, + }) + + require.Nil(terr) + require.Equal(signedImportTxHash, resp.TransactionIdentifier.Hash) + }) +} + +func TestAddValidatorTxConstruction(t *testing.T) { + startTime := uint64(1659592163) + endTime := startTime + 14*86400 + shares := uint32(200000) + + operations := []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{Index: 0}, + RelatedOperations: nil, + Type: pmapper.OpAddValidator, + Account: pAccountIdentifier, + Amount: mapper.AtomicAvaxAmount(big.NewInt(-2_000_000_000_000)), + CoinChange: &types.CoinChange{ + CoinIdentifier: &types.CoinIdentifier{Identifier: coinID1}, + CoinAction: "coin_spent", + }, + Metadata: map[string]interface{}{ + "type": pmapper.OpTypeInput, + "sig_indices": []interface{}{0.0}, + "locktime": 0.0, + }, + }, + { + OperationIdentifier: &types.OperationIdentifier{Index: 1}, + Type: pmapper.OpAddValidator, + Account: pAccountIdentifier, + Amount: mapper.AtomicAvaxAmount(big.NewInt(2_000_000_000_000)), + Metadata: map[string]interface{}{ + "type": pmapper.OpTypeStakeOutput, + "locktime": 0.0, + "threshold": 1.0, + // the following are ignored by payloads endpoint but generated by parse + // added here so that we can simply compare with parse outputs + "staking_start_time": startTime, + "staking_end_time": endTime, + "validator_node_id": nodeID, + "subnet_id": pChainID.String(), + "delegation_rewards_owner": []string{stakeRewardAccount.Address}, + "validator_rewards_owner": []string{stakeRewardAccount.Address}, + }, + }, + } + + preprocessMetadata := map[string]interface{}{ + "node_id": nodeID, + "start": startTime, + "end": endTime, + "shares": shares, + "reward_addresses": []string{stakeRewardAccount.Address}, + } + + metadataOptions := map[string]interface{}{ + "type": pmapper.OpAddValidator, + "node_id": nodeID, + "start": startTime, + "end": endTime, + "shares": shares, + "reward_addresses": []string{stakeRewardAccount.Address}, + } + + payloadsMetadata := map[string]interface{}{ + "network_id": float64(avalancheNetworkID), + "blockchain_id": pChainID.String(), + "node_id": nodeID, + "start": float64(startTime), + "end": float64(endTime), + "shares": float64(shares), + "locktime": 0.0, + "subnet": "", + "threshold": 1.0, + "reward_addresses": []interface{}{stakeRewardAccount.Address}, + "delegator_reward_addresses": nil, + "bls_proof_of_possession": "", + "bls_public_key": "", + } + + signers := []*types.AccountIdentifier{pAccountIdentifier} + stakeSigners := buildRosettaSignerJSON([]string{coinID1}, signers) + + unsignedTx := "0x00000000000c0000000500000000000000000000000000000000000000000000000000000000000000000000000000000001f52a5a6dd8f1b3fe05204bdab4f6bcb5a7059f88d0443c636f6c158f838dd1a8000000003d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa00000005000001d1a94a200000000001000000000000000077e1d5c6c289c49976f744749d54369d2129d7500000000062eb5de30000000062fdd2e3000001d1a94a2000000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa00000007000001d1a94a2000000000000000000000000001000000015445cd01d75b4a06b6b41939193c0b1c5544490d0000000b00000000000000000000000100000001cf7cd358e2e882449d68c1c8889889eaf247b72000030d4000000000482f5298" + unsignedTxHash, err := hex.DecodeString("00c9e13de9f32b5808e54c024b15bdeee5925cbb918d90a272def046cedae800") + require.NoError(t, err) + wrappedUnsignedTx := `{"tx":"` + unsignedTx + `","signers":` + stakeSigners + `}` + + signingPayloads := []*types.SigningPayload{ + { + AccountIdentifier: pAccountIdentifier, + Bytes: unsignedTxHash, + SignatureType: types.EcdsaRecovery, + }, + } + + signedTx := "0x00000000000c0000000500000000000000000000000000000000000000000000000000000000000000000000000000000001f52a5a6dd8f1b3fe05204bdab4f6bcb5a7059f88d0443c636f6c158f838dd1a8000000003d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa00000005000001d1a94a200000000001000000000000000077e1d5c6c289c49976f744749d54369d2129d7500000000062eb5de30000000062fdd2e3000001d1a94a2000000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa00000007000001d1a94a2000000000000000000000000001000000015445cd01d75b4a06b6b41939193c0b1c5544490d0000000b00000000000000000000000100000001cf7cd358e2e882449d68c1c8889889eaf247b72000030d400000000100000009000000017403e32bb967e71902a988b7da635b4bca2475eedbfd23176610a88162f3a92f20b61f2185825b04b7f8ee8c76427c8dc80eb6091f9e594ef259a59856e5401b01e228f820" + signedTxSignature, err := hex.DecodeString("7403e32bb967e71902a988b7da635b4bca2475eedbfd23176610a88162f3a92f20b61f2185825b04b7f8ee8c76427c8dc80eb6091f9e594ef259a59856e5401b01") + require.NoError(t, err) + signedTxHash := "2Exfhp6qjdNz8HvECFH2sQxvUxJsaygZjWriY8xh3BvBXWh7Nb" + + wrappedSignedTx := `{"tx":"` + signedTx + `","signers":` + stakeSigners + `}` + + signatures := []*types.Signature{{ + SigningPayload: &types.SigningPayload{ + AccountIdentifier: cAccountIdentifier, + Bytes: unsignedTxHash, + SignatureType: types.EcdsaRecovery, + }, + SignatureType: types.EcdsaRecovery, + Bytes: signedTxSignature, + }} + + ctx := context.Background() + ctrl := gomock.NewController(t) + clientMock := client.NewMockPChainClient(ctrl) + parserMock := indexer.NewMockParser(ctrl) + parserMock.EXPECT().GetGenesisBlock(ctx).Return(dummyGenesis, nil) + backend, err := NewBackend( + service.ModeOnline, + clientMock, + parserMock, + avaxAssetID, + pChainNetworkIdentifier, + avalancheNetworkID, + ) + require.NoError(t, err) + + t.Run("preprocess endpoint", func(t *testing.T) { + resp, err := backend.ConstructionPreprocess( + ctx, + &types.ConstructionPreprocessRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Operations: operations, + Metadata: preprocessMetadata, + }, + ) + require.Nil(t, err) + require.Equal(t, metadataOptions, resp.Options) + }) + + t.Run("metadata endpoint", func(t *testing.T) { + clientMock.EXPECT().GetBlockchainID(ctx, constants.PChain.String()).Return(pChainID, nil) + + resp, err := backend.ConstructionMetadata( + ctx, + &types.ConstructionMetadataRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Options: metadataOptions, + }, + ) + require.Nil(t, err) + require.Equal(t, payloadsMetadata, resp.Metadata) + }) + + t.Run("payloads endpoint", func(t *testing.T) { + resp, err := backend.ConstructionPayloads( + ctx, + &types.ConstructionPayloadsRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Operations: operations, + Metadata: payloadsMetadata, + }, + ) + require.Nil(t, err) + require.Equal(t, wrappedUnsignedTx, resp.UnsignedTransaction) + require.Equal(t, signingPayloads, resp.Payloads, + "signing payloads mismatch: %s %s", + marshalSigningPayloads(signingPayloads), + marshalSigningPayloads(resp.Payloads)) + }) + + t.Run("parse endpoint (unsigned)", func(t *testing.T) { + resp, err := backend.ConstructionParse( + ctx, + &types.ConstructionParseRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Transaction: wrappedUnsignedTx, + Signed: false, + }, + ) + require.Nil(t, err) + require.Nil(t, resp.AccountIdentifierSigners) + require.Equal(t, operations, resp.Operations) + }) + + t.Run("combine endpoint", func(t *testing.T) { + resp, err := backend.ConstructionCombine( + ctx, + &types.ConstructionCombineRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + UnsignedTransaction: wrappedUnsignedTx, + Signatures: signatures, + }, + ) + + require.Nil(t, err) + require.Equal(t, wrappedSignedTx, resp.SignedTransaction) + }) + + t.Run("parse endpoint (signed)", func(t *testing.T) { + resp, err := backend.ConstructionParse( + ctx, + &types.ConstructionParseRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Transaction: wrappedSignedTx, + Signed: true, + }, + ) + require.Nil(t, err) + require.Equal(t, signers, resp.AccountIdentifierSigners) + require.Equal(t, operations, resp.Operations) + }) + + t.Run("hash endpoint", func(t *testing.T) { + resp, err := backend.ConstructionHash(ctx, &types.ConstructionHashRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + SignedTransaction: wrappedSignedTx, + }) + require.Nil(t, err) + require.Equal(t, signedTxHash, resp.TransactionIdentifier.Hash) + }) + + t.Run("submit endpoint", func(t *testing.T) { + require := require.New(t) + + signedTxBytes, err := mapper.DecodeToBytes(signedTx) + require.NoError(err) + txID, err := ids.FromString(signedTxHash) + require.NoError(err) + + clientMock.EXPECT().IssueTx(ctx, signedTxBytes).Return(txID, nil) + + resp, terr := backend.ConstructionSubmit(ctx, &types.ConstructionSubmitRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + SignedTransaction: wrappedSignedTx, + }) + + require.Nil(terr) + require.Equal(signedTxHash, resp.TransactionIdentifier.Hash) + }) +} + +func TestAddDelegatorTxConstruction(t *testing.T) { + startTime := uint64(1659592163) + endTime := startTime + 14*86400 + + operations := []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{Index: 0}, + RelatedOperations: nil, + Type: pmapper.OpAddDelegator, + Account: pAccountIdentifier, + Amount: mapper.AtomicAvaxAmount(big.NewInt(-25_000_000_000)), + CoinChange: &types.CoinChange{ + CoinIdentifier: &types.CoinIdentifier{Identifier: coinID1}, + CoinAction: "coin_spent", + }, + Metadata: map[string]interface{}{ + "type": pmapper.OpTypeInput, + "sig_indices": []interface{}{0.0}, + "locktime": 0.0, + }, + }, + { + OperationIdentifier: &types.OperationIdentifier{Index: 1}, + Type: pmapper.OpAddDelegator, + Account: pAccountIdentifier, + Amount: mapper.AtomicAvaxAmount(big.NewInt(25_000_000_000)), + Metadata: map[string]interface{}{ + "type": pmapper.OpTypeStakeOutput, + "locktime": 0.0, + "threshold": 1.0, + // the following are ignored by payloads endpoint but generated by parse + // added here so that we can simply compare with parse outputs + "staking_start_time": startTime, + "staking_end_time": endTime, + "validator_node_id": nodeID, + "subnet_id": pChainID.String(), + "delegator_rewards_owner": []string{stakeRewardAccount.Address}, + }, + }, + } + + preprocessMetadata := map[string]interface{}{ + "node_id": nodeID, + "start": startTime, + "end": endTime, + "reward_addresses": []string{stakeRewardAccount.Address}, + } + + metadataOptions := map[string]interface{}{ + "type": pmapper.OpAddDelegator, + "node_id": nodeID, + "start": startTime, + "end": endTime, + "reward_addresses": []string{stakeRewardAccount.Address}, + } + + payloadsMetadata := map[string]interface{}{ + "network_id": float64(avalancheNetworkID), + "blockchain_id": pChainID.String(), + "node_id": nodeID, + "start": float64(startTime), + "end": float64(endTime), + "shares": 0.0, + "locktime": 0.0, + "threshold": 1.0, + "reward_addresses": []interface{}{stakeRewardAccount.Address}, + "bls_proof_of_possession": "", + "bls_public_key": "", + "delegator_reward_addresses": nil, + "subnet": "", + } + + signers := []*types.AccountIdentifier{pAccountIdentifier} + stakeSigners := buildRosettaSignerJSON([]string{coinID1}, signers) + + unsignedTx := "0x00000000000e0000000500000000000000000000000000000000000000000000000000000000000000000000000000000001f52a5a6dd8f1b3fe05204bdab4f6bcb5a7059f88d0443c636f6c158f838dd1a8000000003d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000500000005d21dba0000000001000000000000000077e1d5c6c289c49976f744749d54369d2129d7500000000062eb5de30000000062fdd2e300000005d21dba00000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000700000005d21dba00000000000000000000000001000000015445cd01d75b4a06b6b41939193c0b1c5544490d0000000b00000000000000000000000100000001cf7cd358e2e882449d68c1c8889889eaf247b72000000000eece5b91" + unsignedTxHash, err := hex.DecodeString("832a55223ef63d8e39d85025df08c9ae82d0f185d4a0f60b14dec360d721b9f4") + require.NoError(t, err) + wrappedUnsignedTx := `{"tx":"` + unsignedTx + `","signers":` + stakeSigners + `}` + + signingPayloads := []*types.SigningPayload{ + { + AccountIdentifier: pAccountIdentifier, + Bytes: unsignedTxHash, + SignatureType: types.EcdsaRecovery, + }, + } + + signedTx := "0x00000000000e0000000500000000000000000000000000000000000000000000000000000000000000000000000000000001f52a5a6dd8f1b3fe05204bdab4f6bcb5a7059f88d0443c636f6c158f838dd1a8000000003d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000500000005d21dba0000000001000000000000000077e1d5c6c289c49976f744749d54369d2129d7500000000062eb5de30000000062fdd2e300000005d21dba00000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000700000005d21dba00000000000000000000000001000000015445cd01d75b4a06b6b41939193c0b1c5544490d0000000b00000000000000000000000100000001cf7cd358e2e882449d68c1c8889889eaf247b7200000000100000009000000017403e32bb967e71902a988b7da635b4bca2475eedbfd23176610a88162f3a92f20b61f2185825b04b7f8ee8c76427c8dc80eb6091f9e594ef259a59856e5401b0143d545c4" + signedTxSignature, err := hex.DecodeString("7403e32bb967e71902a988b7da635b4bca2475eedbfd23176610a88162f3a92f20b61f2185825b04b7f8ee8c76427c8dc80eb6091f9e594ef259a59856e5401b01") + require.NoError(t, err) + signedTxHash := "2eppnnog3TwkBTyQKMh44wz5bUy4geDETEeZVCz7m7uMnjGeCP" + + wrappedSignedTx := `{"tx":"` + signedTx + `","signers":` + stakeSigners + `}` + + signatures := []*types.Signature{{ + SigningPayload: &types.SigningPayload{ + AccountIdentifier: cAccountIdentifier, + Bytes: unsignedTxHash, + SignatureType: types.EcdsaRecovery, + }, + SignatureType: types.EcdsaRecovery, + Bytes: signedTxSignature, + }} + + ctx := context.Background() + ctrl := gomock.NewController(t) + clientMock := client.NewMockPChainClient(ctrl) + parserMock := indexer.NewMockParser(ctrl) + parserMock.EXPECT().GetGenesisBlock(ctx).Return(dummyGenesis, nil) + backend, err := NewBackend( + service.ModeOnline, + clientMock, + parserMock, + avaxAssetID, + pChainNetworkIdentifier, + avalancheNetworkID, + ) + require.NoError(t, err) + + t.Run("preprocess endpoint", func(t *testing.T) { + resp, err := backend.ConstructionPreprocess( + ctx, + &types.ConstructionPreprocessRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Operations: operations, + Metadata: preprocessMetadata, + }, + ) + require.Nil(t, err) + require.Equal(t, metadataOptions, resp.Options) + }) + + t.Run("metadata endpoint", func(t *testing.T) { + clientMock.EXPECT().GetBlockchainID(ctx, constants.PChain.String()).Return(pChainID, nil) + + resp, err := backend.ConstructionMetadata( + ctx, + &types.ConstructionMetadataRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Options: metadataOptions, + }, + ) + require.Nil(t, err) + require.Equal(t, payloadsMetadata, resp.Metadata) + }) + + t.Run("payloads endpoint", func(t *testing.T) { + resp, err := backend.ConstructionPayloads( + ctx, + &types.ConstructionPayloadsRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Operations: operations, + Metadata: payloadsMetadata, + }, + ) + require.Nil(t, err) + require.Equal(t, wrappedUnsignedTx, resp.UnsignedTransaction) + require.Equal(t, signingPayloads, resp.Payloads, + "signing payloads mismatch: %s %s", + marshalSigningPayloads(signingPayloads), + marshalSigningPayloads(resp.Payloads)) + }) + + t.Run("parse endpoint (unsigned)", func(t *testing.T) { + resp, err := backend.ConstructionParse( + ctx, + &types.ConstructionParseRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Transaction: wrappedUnsignedTx, + Signed: false, + }, + ) + require.Nil(t, err) + require.Nil(t, resp.AccountIdentifierSigners) + require.Equal(t, operations, resp.Operations) + }) + + t.Run("combine endpoint", func(t *testing.T) { + resp, err := backend.ConstructionCombine( + ctx, + &types.ConstructionCombineRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + UnsignedTransaction: wrappedUnsignedTx, + Signatures: signatures, + }, + ) + + require.Nil(t, err) + require.Equal(t, wrappedSignedTx, resp.SignedTransaction) + }) + + t.Run("parse endpoint (signed)", func(t *testing.T) { + resp, err := backend.ConstructionParse( + ctx, + &types.ConstructionParseRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + Transaction: wrappedSignedTx, + Signed: true, + }, + ) + require.Nil(t, err) + require.Equal(t, signers, resp.AccountIdentifierSigners) + require.Equal(t, operations, resp.Operations) + }) + + t.Run("hash endpoint", func(t *testing.T) { + resp, err := backend.ConstructionHash(ctx, &types.ConstructionHashRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + SignedTransaction: wrappedSignedTx, + }) + require.Nil(t, err) + require.Equal(t, signedTxHash, resp.TransactionIdentifier.Hash) + }) + + t.Run("submit endpoint", func(t *testing.T) { + require := require.New(t) + + signedTxBytes, err := mapper.DecodeToBytes(signedTx) + require.NoError(err) + txID, err := ids.FromString(signedTxHash) + require.NoError(err) + + clientMock.EXPECT().IssueTx(ctx, signedTxBytes).Return(txID, nil) + + resp, terr := backend.ConstructionSubmit(ctx, &types.ConstructionSubmitRequest{ + NetworkIdentifier: pChainNetworkIdentifier, + SignedTransaction: wrappedSignedTx, + }) + + require.Nil(terr) + require.Equal(signedTxHash, resp.TransactionIdentifier.Hash) + }) +} + +func marshalSigningPayloads(payloads []*types.SigningPayload) string { + bytes, err := json.Marshal(payloads) + if err != nil { + return "FAILED_TO_MARSHAL" + } + + return string(bytes) +} diff --git a/server/service/backend/pchain/genesis_handler.go b/server/service/backend/pchain/genesis_handler.go new file mode 100644 index 0000000..fca5de5 --- /dev/null +++ b/server/service/backend/pchain/genesis_handler.go @@ -0,0 +1,141 @@ +package pchain + +import ( + "context" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/block" + "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/ava-labs/avalanche-rosetta/service/backend/pchain/indexer" +) + +var _ genesisHandler = &gHandler{} + +type genesisHandler interface { + isGenesisBlockRequest(index int64, hash string) (bool, error) + getGenesisBlock() *indexer.ParsedGenesisBlock + getGenesisIdentifier() *types.BlockIdentifier + + // [getFullGenesisTxs] returns proper genesis txs + genesis allocation tx + getFullGenesisTxs() ([]*txs.Tx, error) + buildGenesisAllocationTx() (*txs.Tx, error) +} + +func newGenesisHandler(indexerParser indexer.Parser) (genesisHandler, error) { + gh := &gHandler{ + indexerParser: indexerParser, + + // Note: since genesis block and transactions can be considerably larger + // than any other block generated during the blockchain lifetime + // a special codec is used to parse genesis-related objects + genesisCodec: block.GenesisCodec, + } + + // initializing genesis block. No network calls are involved. + return gh, gh.loadGenesisBlk() +} + +type gHandler struct { + indexerParser indexer.Parser + genesisCodec codec.Manager + + genesisBlk *indexer.ParsedGenesisBlock + genesisIdentifier *types.BlockIdentifier + + allocationTx *txs.Tx +} + +func (gh *gHandler) loadGenesisBlk() error { + genesisBlk, err := gh.indexerParser.GetGenesisBlock(context.Background()) + if err != nil { + return err + } + gh.genesisBlk = genesisBlk + gh.genesisIdentifier = &types.BlockIdentifier{ + Index: int64(genesisBlk.Height), + Hash: genesisBlk.BlockID.String(), + } + return nil +} + +func (gh *gHandler) isGenesisBlockRequest(index int64, hash string) (bool, error) { + // if hash is provided, make sure it matches genesis block hash + if hash != "" { + return hash == gh.genesisBlk.BlockID.String(), nil + } + + // if hash is omitted, check if the height matches the genesis block height + return index == int64(gh.genesisBlk.Height), nil +} + +func (gh *gHandler) getGenesisBlock() *indexer.ParsedGenesisBlock { + return gh.genesisBlk +} + +func (gh *gHandler) getGenesisIdentifier() *types.BlockIdentifier { + return gh.genesisIdentifier +} + +func (gh *gHandler) getFullGenesisTxs() ([]*txs.Tx, error) { + res := gh.genesisBlk.Txs + allocationTx, err := gh.buildGenesisAllocationTx() + if err != nil { + return nil, err + } + res = append(res, allocationTx) + return res, nil +} + +// Genesis allocation UTXOs are not part of a real transaction. +// For convenience and compatibility with the rest of the parsing functionality +// they are treated as outputs of an import tx with no inputs and id ids.Empty +func (gh *gHandler) buildGenesisAllocationTx() (*txs.Tx, error) { + if gh.allocationTx != nil { + return gh.allocationTx, nil + } + + outs := []*avax.TransferableOutput{} + for _, utxo := range gh.genesisBlk.UTXOs { + outIntf := utxo.Out + if lockedOut, ok := outIntf.(*stakeable.LockOut); ok { + outIntf = lockedOut.TransferableOut + } + + out, ok := outIntf.(*secp256k1fx.TransferOutput) + + if !ok { + return nil, errUnableToParseUTXO + } + + outs = append(outs, &avax.TransferableOutput{ + Asset: avax.Asset{ + ID: utxo.AssetID(), + }, + Out: &secp256k1fx.TransferOutput{ + Amt: out.Amount(), + OutputOwners: secp256k1fx.OutputOwners{ + Addrs: out.Addrs, + Threshold: out.Threshold, + Locktime: out.Locktime, + }, + }, + }) + } + + // TODO: this is probably not the right way to build this tx + // Some fields are missing that we populate in tx Builder + // We also do not sign/initialize the transaction since genesis outputs are referred to empty id as tx id. + allocationTx := &txs.ImportTx{} + allocationTx.Outs = outs + tx := &txs.Tx{ + Unsigned: allocationTx, + } + + gh.allocationTx = tx + return gh.allocationTx, nil +} diff --git a/server/service/backend/pchain/indexer/mock_parser.go b/server/service/backend/pchain/indexer/mock_parser.go new file mode 100644 index 0000000..7bbf5bb --- /dev/null +++ b/server/service/backend/pchain/indexer/mock_parser.go @@ -0,0 +1,100 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ava-labs/avalanche-rosetta/service/backend/pchain/indexer (interfaces: Parser) +// +// Generated by this command: +// +// mockgen -package=indexer -destination=service/backend/pchain/indexer/mock_parser.go github.com/ava-labs/avalanche-rosetta/service/backend/pchain/indexer Parser +// + +// Package indexer is a generated GoMock package. +package indexer + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockParser is a mock of Parser interface. +type MockParser struct { + ctrl *gomock.Controller + recorder *MockParserMockRecorder +} + +// MockParserMockRecorder is the mock recorder for MockParser. +type MockParserMockRecorder struct { + mock *MockParser +} + +// NewMockParser creates a new mock instance. +func NewMockParser(ctrl *gomock.Controller) *MockParser { + mock := &MockParser{ctrl: ctrl} + mock.recorder = &MockParserMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockParser) EXPECT() *MockParserMockRecorder { + return m.recorder +} + +// GetGenesisBlock mocks base method. +func (m *MockParser) GetGenesisBlock(arg0 context.Context) (*ParsedGenesisBlock, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGenesisBlock", arg0) + ret0, _ := ret[0].(*ParsedGenesisBlock) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetGenesisBlock indicates an expected call of GetGenesisBlock. +func (mr *MockParserMockRecorder) GetGenesisBlock(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGenesisBlock", reflect.TypeOf((*MockParser)(nil).GetGenesisBlock), arg0) +} + +// GetPlatformHeight mocks base method. +func (m *MockParser) GetPlatformHeight(arg0 context.Context) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPlatformHeight", arg0) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPlatformHeight indicates an expected call of GetPlatformHeight. +func (mr *MockParserMockRecorder) GetPlatformHeight(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlatformHeight", reflect.TypeOf((*MockParser)(nil).GetPlatformHeight), arg0) +} + +// ParseCurrentBlock mocks base method. +func (m *MockParser) ParseCurrentBlock(arg0 context.Context) (*ParsedBlock, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ParseCurrentBlock", arg0) + ret0, _ := ret[0].(*ParsedBlock) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ParseCurrentBlock indicates an expected call of ParseCurrentBlock. +func (mr *MockParserMockRecorder) ParseCurrentBlock(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ParseCurrentBlock", reflect.TypeOf((*MockParser)(nil).ParseCurrentBlock), arg0) +} + +// ParseNonGenesisBlock mocks base method. +func (m *MockParser) ParseNonGenesisBlock(arg0 context.Context, arg1 string, arg2 uint64) (*ParsedBlock, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ParseNonGenesisBlock", arg0, arg1, arg2) + ret0, _ := ret[0].(*ParsedBlock) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ParseNonGenesisBlock indicates an expected call of ParseNonGenesisBlock. +func (mr *MockParserMockRecorder) ParseNonGenesisBlock(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ParseNonGenesisBlock", reflect.TypeOf((*MockParser)(nil).ParseNonGenesisBlock), arg0, arg1, arg2) +} diff --git a/server/service/backend/pchain/indexer/parser.go b/server/service/backend/pchain/indexer/parser.go new file mode 100644 index 0000000..88ddee5 --- /dev/null +++ b/server/service/backend/pchain/indexer/parser.go @@ -0,0 +1,290 @@ +package indexer + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils/hashing" + "github.com/ava-labs/avalanchego/vms/platformvm/block" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + + "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/constants" + + avaconstants "github.com/ava-labs/avalanchego/utils/constants" + platformvmgenesis "github.com/ava-labs/avalanchego/vms/platformvm/genesis" + proposervmblock "github.com/ava-labs/avalanchego/vms/proposervm/block" +) + +var ( + _ Parser = &parser{} + + genesisTimestamp = time.Date(2020, time.September, 10, 0, 0, 0, 0, time.UTC) + noProposerTime = time.Time{} + errMissingBlockIndexHash = errors.New("a positive block index, a block hash or both must be specified") +) + +// Parser defines the interface for a P-chain indexer parser +// Note: we use indexer just because platformVM does not currently offer a way to retrieve +// blocks by height. However we do NOT want to use the indexer to retrieve blocks by ID; instead +// we'll use platformvm.GetBlock api for that. The reason is that we want to use +// platformVM-level block ID as P-chain blocks identifier rather than proposerVM-level. +type Parser interface { + // GetGenesisBlock parses and returns the Genesis block + GetGenesisBlock(ctx context.Context) (*ParsedGenesisBlock, error) + // ParseNonGenesisBlock returns the block with provided hash or height + ParseNonGenesisBlock(ctx context.Context, hash string, height uint64) (*ParsedBlock, error) + // GetPlatformHeight returns the current block height of P-chain + GetPlatformHeight(ctx context.Context) (uint64, error) + // ParseCurrentBlock parses and returns the current tip of P-chain + ParseCurrentBlock(ctx context.Context) (*ParsedBlock, error) +} + +type parser struct { + // The full PChainClient is currently needed just to retrieve + // NetworkID. + // TODO: consider introducing a cache for parsed blocks + pChainClient client.PChainClient + + codec codec.Manager + codecVersion uint16 + + networkID uint32 + + aliaser ids.Aliaser +} + +// NewParser creates a new P-chain indexer parser +// Note: NewParser should not contain calls to pChainClient as we +// cannot assume client is ready to serve requests immediately +func NewParser(pChainClient client.PChainClient, avalancheNetworkID uint32) (Parser, error) { + aliaser := ids.NewAliaser() + err := aliaser.Alias(avaconstants.PlatformChainID, constants.PChain.String()) + if err != nil { + return nil, err + } + + return &parser{ + pChainClient: pChainClient, + codec: block.Codec, + codecVersion: block.CodecVersion, + networkID: avalancheNetworkID, + aliaser: aliaser, + }, nil +} + +func (p *parser) GetPlatformHeight(ctx context.Context) (uint64, error) { + container, _, err := p.pChainClient.GetLastAccepted(ctx) + if err != nil { + return 0, err + } + blk, err := p.parseProposerBlock(container.Bytes) + if err != nil { + return 0, err + } + return blk.Height, nil +} + +// GetGenesisBlock is called to initialize P-chain genesis information upon startup. +// GetGenesisBlock should not call the indexer, to ensure backward compatibility with +// previous installations which do no host a block indexer. +func (p *parser) GetGenesisBlock(_ context.Context) (*ParsedGenesisBlock, error) { + bytes, _, err := genesis.FromConfig(genesis.GetConfig(p.networkID)) + if err != nil { + return nil, err + } + + genesisState, err := platformvmgenesis.Parse(bytes) + if err != nil { + return nil, err + } + + genesisTimestamp := time.Unix(int64(genesisState.Timestamp), 0) + + var genesisTxs []*txs.Tx + genesisTxs = append(genesisTxs, genesisState.Validators...) + genesisTxs = append(genesisTxs, genesisState.Chains...) + + // Build genesis ID and ParentID as it's done in platformVM'State, + // without polling indexer (for backward compatibility). + genesisParentID := hashing.ComputeHash256Array(bytes) + genesisBlock, err := block.NewApricotCommitBlock(genesisParentID, 0) + if err != nil { + return nil, err + } + genesisBlockID := genesisBlock.ID() + + // genesis gets its own context to unlock caching + genesisCtx := &snow.Context{ + BCLookup: p.aliaser, + NetworkID: p.networkID, + } + for _, utxo := range genesisState.UTXOs { + utxo.UTXO.Out.InitCtx(genesisCtx) + } + + return &ParsedGenesisBlock{ + ParsedBlock: ParsedBlock{ + ParentID: genesisParentID, + Height: 0, + BlockID: genesisBlockID, + BlockType: "GenesisBlock", + Timestamp: genesisTimestamp.UnixMilli(), + Txs: genesisTxs, + }, + GenesisBlockData: GenesisBlockData{ + Message: genesisState.Message, + InitialSupply: genesisState.InitialSupply, + UTXOs: genesisState.UTXOs, + }, + }, nil +} + +func (p *parser) ParseCurrentBlock(ctx context.Context) (*ParsedBlock, error) { + height, err := p.GetPlatformHeight(ctx) + if err != nil { + return nil, err + } + + return p.parseBlockAtHeight(ctx, height) +} + +func (p *parser) ParseNonGenesisBlock(ctx context.Context, hash string, height uint64) (*ParsedBlock, error) { + if height <= 0 && hash == "" { + return nil, errMissingBlockIndexHash + } + + if hash != "" { + return p.parseBlockWithHash(ctx, hash) + } + + return p.parseBlockAtHeight(ctx, height) +} + +func (p *parser) parseBlockAtHeight(ctx context.Context, height uint64) (*ParsedBlock, error) { + // P-chain indexer does not include genesis and store block at height 1 with index 0. + // Therefore containers are looked up with index = height - 1. + // Note that genesis does not cause a problem here as it is handled in a separate code path + container, err := p.pChainClient.GetContainerByIndex(ctx, height-1) + if err != nil { + return nil, err + } + + return p.parseProposerBlock(container.Bytes) +} + +func (p *parser) parseBlockWithHash(ctx context.Context, hash string) (*ParsedBlock, error) { + hashID, err := ids.FromString(hash) + if err != nil { + return nil, err + } + + // hashID is P-Chain block ID (not proposerVM block ID). Hence we try pulling + // block from P-Chain API (not the indexer, which tracks proposerVM blocks) + blkBytes, err := p.pChainClient.GetBlock(ctx, hashID) + if err != nil { + return nil, err + } + + return p.parsePChainBlock(blkBytes, noProposerTime) +} + +// [parseProposerBlock] parses blocks are retrieved from index api. +// [parseProposerBlock] tries to parse block as ProposerVM block first. +// In case of failure, it tries to parse it as a pre-proposerVM block. +func (p *parser) parseProposerBlock(blkBytes []byte) (*ParsedBlock, error) { + pChainBlkBytes := blkBytes + proposerTime := noProposerTime + + proBlk, err := proposervmblock.Parse(blkBytes, time.Time{}) + if err == nil { + // inner proposerVM bytes, to be parsed as P-chain block + pChainBlkBytes = proBlk.Block() + + // retrieve relevant proposer data + if b, ok := proBlk.(proposervmblock.SignedBlock); ok { + proposerTime = b.Timestamp() + } + } + + return p.parsePChainBlock(pChainBlkBytes, proposerTime) +} + +func (p *parser) parsePChainBlock(pChainBlkBytes []byte, proposerTime time.Time) (*ParsedBlock, error) { + blk, err := block.Parse(p.codec, pChainBlkBytes) + if err != nil { + return nil, fmt.Errorf("unmarshaling block bytes errored with %w", err) + } + txes := blk.Txs() + if txes == nil { + txes = []*txs.Tx{} + } + + // We retrieve timestamps from the block to have a deployment-independent timestamp. + // This blkTime is not guarateed to be monotonic before Banff blocks, whose Mainnet + // activation happened on Tuesday, 2022 October 18 at 12 p.m. EDT. + blkTime, err := retrieveTime(blk, proposerTime) + if err != nil { + return nil, fmt.Errorf("failed retrieving block time: %w", err) + } + + return &ParsedBlock{ + BlockID: blk.ID(), + BlockType: fmt.Sprintf("%T", blk), + ParentID: blk.Parent(), + Timestamp: blkTime.UnixMilli(), + + Height: blk.Height(), + Txs: txes, + }, nil +} + +func retrieveTime(pchainBlk block.Block, proposerTime time.Time) (time.Time, error) { + switch b := pchainBlk.(type) { + // Banff blocks serialize pchain time + case *block.BanffProposalBlock: + return b.Timestamp(), nil + case *block.BanffStandardBlock: + return b.Timestamp(), nil + case *block.BanffAbortBlock: + return b.Timestamp(), nil + case *block.BanffCommitBlock: + return b.Timestamp(), nil + + // Apricot Proposal blocks may contain an advance time tx + // setting pchain time + case *block.ApricotProposalBlock: + if t, ok := b.Tx.Unsigned.(*txs.AdvanceTimeTx); ok { + return t.Timestamp(), nil + } + + case *block.ApricotAtomicBlock, + *block.ApricotStandardBlock, + *block.ApricotAbortBlock, + *block.ApricotCommitBlock: + // no relevant time information in these blocks + + default: + return time.Time{}, fmt.Errorf("unknown block type %T", b) + } + + // No timestamp was found in given pre-Banff block. + // Fallback to proposer timestamp as used by Snowman++ + // if available. While proposer timestamp should be close + // to pchain time at block creation, time monotonicity is + // not guaranteed. + if !proposerTime.IsZero() { + return proposerTime, nil + } + + // Fallback to the genesis timestamp. We cannot simply + // return time.Time{} as Rosetta expects timestamps to be available + // after 1/1/2000. + return genesisTimestamp, nil +} diff --git a/server/service/backend/pchain/indexer/parser_test.go b/server/service/backend/pchain/indexer/parser_test.go new file mode 100644 index 0000000..fe2fbd7 --- /dev/null +++ b/server/service/backend/pchain/indexer/parser_test.go @@ -0,0 +1,174 @@ +package indexer + +import ( + "context" + "encoding/json" + "fmt" + "os" + "testing" + + "github.com/ava-labs/avalanchego/api" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/indexer" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils/formatting" + "github.com/ava-labs/avalanchego/vms/platformvm/genesis" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/constants" + + avaconstants "github.com/ava-labs/avalanchego/utils/constants" +) + +var ( + p Parser + g *ParsedGenesisBlock +) + +// idxs of the containers we test against +var idxs = []uint64{ + 0, + 1, + 2, + 8, + 48, + 173, + 382, + 911, + 1603, + 5981, + 131475, + 211277, + 211333, + 806002, + 810424, + 1000000, + 1000001, + 1000002, + 1000004, +} + +func readFixture(path string, sprintfArgs ...interface{}) []byte { + relpath := fmt.Sprintf(path, sprintfArgs...) + ret, err := os.ReadFile("testdata/" + relpath) + if err != nil { + panic(err) + } + + return ret +} + +func TestMain(m *testing.M) { + ctx := context.Background() + pchainClient := client.NewMockPChainClient(gomock.NewController(nil)) + + for _, idx := range idxs { + ret := readFixture("ins/%v.json", idx) + + var container indexer.Container + err := json.Unmarshal(ret, &container) + if err != nil { + panic(err) + } + + pchainClient.EXPECT().GetContainerByIndex(ctx, idx).Return(container, nil) + } + + txID, err := ids.FromString("jWgE5KiiCejNYbYGDzhu9WAXrAdgwav9EXuycNVdB62rSU4tH") + if err != nil { + panic(err) + } + arg := &api.GetTxArgs{ + TxID: txID, + Encoding: formatting.Hex, + } + bytes := [][]byte{{0, 0, 96, 135, 38, 30, 158, 122, 109, 66, 126, 42, 192, 155, 20, 141, 194, 137, 85, 161, 188, 115, 215, 227, 44, 148, 7, 201, 191, 227, 25, 222, 126, 28, 0, 0, 0, 7, 33, 230, 115, 23, 203, 196, 190, 42, 235, 0, 103, 122, 214, 70, 39, 120, 168, 245, 34, 116, 185, 214, 5, 223, 37, 145, 178, 48, 39, 168, 125, 255, 0, 0, 0, 7, 0, 0, 0, 4, 238, 10, 47, 173, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 237, 104, 212, 116, 123, 119, 22, 41, 162, 163, 85, 62, 170, 126, 105, 250, 197, 149, 192, 120}} //nolint:lll + pchainClient.EXPECT().GetRewardUTXOs(ctx, arg).Return(bytes, nil) + + pchainClient.EXPECT().GetHeight(ctx, gomock.Any()).Return(uint64(1000000), nil) + + p, err = NewParser(pchainClient, avaconstants.MainnetID) + if err != nil { + panic(err) + } + + g, err = p.GetGenesisBlock(ctx) + if err != nil { + panic(err) + } + + os.Exit(m.Run()) +} + +func TestGenesisBlockCreateChainTxs(t *testing.T) { + require := require.New(t) + + g.Txs = g.Txs[(len(g.Txs) - 2):] + for _, tx := range g.Txs { + castTx := tx.Unsigned.(*txs.CreateChainTx) + castTx.GenesisData = []byte{} + } + + g.UTXOs = []*genesis.UTXO{} + + j, err := json.Marshal(g) + require.NoError(err) + + ret := readFixture("outs/genesis.json") + require.JSONEq(string(ret), string(j)) +} + +func TestGenesisBlockParseTxs(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + pchainClient := client.NewMockPChainClient(ctrl) + + p, err := NewParser(pchainClient, avaconstants.FujiID) + require.NoError(err) + + ctx := context.Background() + g, err := p.GetGenesisBlock(ctx) + require.NoError(err) + + initializeTxCtx(g.Txs, avaconstants.FujiID) + j, err := json.MarshalIndent(g, "", " ") + require.NoError(err) + + ret := readFixture("outs/genesis_fuji_txs.json") + require.JSONEq(string(ret), string(j)) +} + +func TestFixtures(t *testing.T) { + require := require.New(t) + ctx := context.Background() + + for _, idx := range idxs { + // +1 because we do -1 inside parseBlockAtIndex + // and ins/outs are based on container ids + // instead of block ids + block, err := p.ParseNonGenesisBlock(ctx, "", idx+1) + require.NoError(err) + + initializeTxCtx(block.Txs, avaconstants.MainnetID) + j, err := json.Marshal(block) + require.NoError(err) + + ret := readFixture("outs/%v.json", idx) + require.JSONEq(string(ret), string(j)) + } +} + +func initializeTxCtx(txs []*txs.Tx, networkID uint32) { + aliaser := ids.NewAliaser() + _ = aliaser.Alias(avaconstants.PlatformChainID, constants.PChain.String()) + ctx := &snow.Context{ + BCLookup: aliaser, + NetworkID: networkID, + } + for _, tx := range txs { + tx.Unsigned.InitCtx(ctx) + } +} diff --git a/server/service/backend/pchain/indexer/testdata/ins/0.json b/server/service/backend/pchain/indexer/testdata/ins/0.json new file mode 100644 index 0000000..0a66e21 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/0.json @@ -0,0 +1,5 @@ +{ + "ID": "4AqeFPxtTW4B5D6oR8gRZTvRKnnqkUWiV6mUNZxjUMbQKYWpi", + "Bytes": "AAAAAAAApI0xSAXUQXW+h54RClUhhwhc6zYRvmpDrNHbp5iuJCcAAAAAAAAAAQAAABMAAAAAX2laoAAAAAA=", + "Timestamp": 1635980695 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/1.json b/server/service/backend/pchain/indexer/testdata/ins/1.json new file mode 100644 index 0000000..b449b4c --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/1.json @@ -0,0 +1,5 @@ +{ + "ID": "2peK2rrira1UUdFuLWZTMHFdGe3JY8sQeKLfPj5jhPdK8mMAcJ", + "Bytes": "AAAAAAACBzJxCZ3bUPT/vW1IU0gfSWCp/MGtvAvK+ADAP6idiLAAAAAAAAAAAg==", + "Timestamp": 1635980695 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/1000000.json b/server/service/backend/pchain/indexer/testdata/ins/1000000.json new file mode 100644 index 0000000..0ec4da7 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/1000000.json @@ -0,0 +1,5 @@ +{ + "ID": "KXR1Ys335NiUE9XKPt7ixBjHQsmrVuH681fZaPmfGnENcuKsG", + "Bytes": "AAAAAAAAtxR/5L6faeWnOj0re1p+EdYEBp6QUItcmgFRhsjqQK0AAAAAYamWiwAAAAAAD0FAAAAEoTCCBJ0wggKFoAMCAQICAQAwDQYJKoZIhvcNAQELBQAwADAgFw05OTEyMzEwMDAwMDBaGA8yMTIxMDMwMjE3MDgxM1owADCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANozOqOjGiOC6MnT+7/FBpBss9tQyv6kyUtRPJHu/0NoSGmVJoADAovceqjC5D0QY7GnDOX15vPivax4OxVYHYuBaOhwZ2MRl1qk/gFKa93ucP3xPWzFA/J1EN1OscQVTMXAzui6+inSNldQV9pKWBZTPmE1JczgJETxio9HkBZCHrzkgigvhjO8BEh081UbCr8Io3e3SkHzjfAx2ITMUhAxW2IE2MbZV0r12WlwCCcn2Mr3fWUzk1cCR1rCYSOYtEgs7arWijnk/RYyDchJCnDrMWVX4gjckxGNYJuVf4zTXoLQJRs+ZpHdpAXlTYroWmAjo7MJWkthe5EZtgZ5gl4mAElsNo/Vh2jE4hp0xl3f29di/Bh990nBEW0Afg4Z3mJfVEPiqXGw3CKXlrI7kQsuA6zmwd/Qg5CbLtiFCmdVoP5wSeW8bo46Vwgg7YHU0alLkH69/izsB1Yw8eTP+hpso9fUREh1TZ0EnJPjyZIiZmJgcgl4UpRwkIuzaVyYnFZt23TQ2GeyPwslSL3FjeK1V/RleiWkOF/G6F7/lIXHubZyiUVmQbFdjaVEn/6sjk5fwATLTopppS815vke8GtLQJeCEK4Zlf6b0DwGMa2FT5XZDzJV8IVT0Hukwbee3sGWzoc86VN5e2l91sIJIZmEG8uuLz45Vtr3eosQWHlfAgMBAAGjIDAeMA4GA1UdDwEB/wQEAwIEsDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQCAr/ZlHdj9TRSuGFA+HOHy7yUIulE7ooit5sirIxxH5SqUmBgG9ZHStWmbCO32EvwlH011aPqgdhcIvVL31sZpt0fRZviKTyVtCr5L7vrSkG9Ozggz38NV47iujnL0R63O8BgfIEZZjVN0GHobpdEiKThnCap7fb4IJ745Ov/lAQei45Mp/G5Lv8/BfDkBBirVsAIekBv07BYPAjx5IJdmGIlMG1ygGjaKnV49xXC8zbmTAuMQ+ym6ySMnGM2nh/m+uZhZDVeJny0nADWdRaIEDwqdAKTY78cQyCNqLIAKfUcqdida5X4xIzCZTHSPXPwxQuzi65Ht2CiD5b9a93gaO9/L4zOs2b/aRcyqoD1uX+THM4DTdrrBDVxbrPzGFimGQ+qBFPIqYwal/m1V1xaPI+msN1oVfcGgEa+QOGNpCVhbp4p9xILADeerCj1dbOfVOnlx/vDcqfB8WronKeLMxJIaNBTlW6uhF+z67ysIHlQX/2ohk7+qR3DdAkIdV+GDvIVwD35XZKQzS1PsSEGprHuk5Bc9iWFbpK7TijFsC+8vpLRfcAIprY4JaZg6tsdHaSX6vlWsw5UjSajvHMLfx5Dm3u9R+GSX8FzyBUUmPYsveZvvGdS/r9wbRmQCxMQ9v6DcYxm7HpzhwMQrmmx8u/v52DFdeHpM7zLvhKuTVwAAAX8AAAAAAAMJRz3JmghRopF02E5SLajMsaVqwj97C6efgKzONM9XaQAAAAAAD0JBAAAAAQAAABIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARxMV+G8s8Vn+fA8qnVWNQLRohOTFzwG2deeokeyDiSAAAAAACHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAABQAAAAM44EZfAAAAAQAAAAAAAAAABCfUsioqeLzd1FZ0LK+RtWutv/mF7hmu8UVz5zQ/1lIAAAABIeZzF8vEvirrAGd61kYneKj1InS51gXfJZGyMCeoff8AAAAHAAAAAzjRBB8AAAAAAAAAAAAAAAEAAAABlaRGfdj5OVVOpOZQHAgpQ4aTjL8AAAABAAAACQAAAAHHlxHEtI3N4gW2NgPv73xhdzoOtH77UD/OvkDSGWK3wl69c0BXQAoSzOnPmazuyEYpI9XZH//hy5CDcige1zhYAQAAAgCDHsW8A43weW+zkx67KGhcUTE9Xb987aIxsbjmKgXWMBxJJ4MJQsyh9E44xgq8qtzn1nj9lbkRrVefNBY5ywChPdMokmbCgweYyWSjRjo/kETdSp9a9KUX961OzHZsFbuzEc6Z5lOkSvLjKUQAsRP484wD3DiQ0M5YngwnVyfz8HjXFhtw0OAYiqVKour0tdjGUHHSLWjUx64VM97QqswGZDfzcnxiBMQBF32z0+j/Q0tnfdUk9Jd1o21/MC6WVNr407NETD6QN7dprPNGmtz11QejKmB/evDQKw6UFzYch6GfWlhxfQM9H7/X/scrulZM3au7ksFDf5lDzKuZH7Yl9FE+Qa1Q744EQ2LNBZT38bhi2sNotpXuzCtDx3nIVuaTHn3SZJxvPhS1+TrmqyykaYoQEh//qHjobrfTxDLsWVjpSC8sXCT1pFB9LSDSUK43cqdgd1NlyUy3NaejncufpyuUWKlakB+L2OA1TTenBOubqAWPiZwxxm20QrWpsojAEURqxWcFRgFRduJR+QxnP8kaOBvUmTKhWmFTWspH9zQXFetN85j8kSNFQJi3b+fmD34fwBQkKfl9zXT7a9jh6JpAQViJyI3MUMLfpI+JAU+zx/PXJKrnBJ4M5bn2RQSTdn21lgW7OMwpuiaM11bccRpDNAN29tDFcJr8NLcnpA==", + "Timestamp": 1638504076 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/1000001.json b/server/service/backend/pchain/indexer/testdata/ins/1000001.json new file mode 100644 index 0000000..f9e162b --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/1000001.json @@ -0,0 +1,5 @@ +{ + "ID": "2jsPqeaxacqgMBjQFuiHGiwTjD4kbXqmgbVxXPjTCUtsY5CnJv", + "Bytes": "AAAAAAAAKg/Wjx19S4e8wJVIpIm90dsB9wzYz1mnIb7LqzMw3lQAAAAAYamWnwAAAAAAD0FBAAAEoTCCBJ0wggKFoAMCAQICAQAwDQYJKoZIhvcNAQELBQAwADAgFw05OTEyMzEwMDAwMDBaGA8yMTIwMDkyODIwMzM0NFowADCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANSzo9QJXXgozzSFQvcyruZ14ybT9oOrE8jDZmAWyntFuD7rSwqinQEpNmPtoU9/e+G0jMl8OwmYXdydwVAn6FpYLFlaL7omT3QZf5PieHngyUcbLvjNpqefECHG+xqr7e6SbOc7uZwPP/DhQ+y9svgYKl9z3KnB7ee71EMD33okcUeN3h2mAK9ekyB9y70W3N5RiGxc5eGAv8xEDSRNv7Kv/T1tcGbUk+bgbBhx6beut7n7+HYpW+CNX7dFnMT88Bk5BjhODXADorXzqwFQFx6xqcc9NR8+uFXjFEbP1ylJXBNb6OR+hELe6kdmXX9jKK3S7LPYbmwwF4Ab+W4jyhNpH2FEEV9VIXLqvueeQDMTj+oUQsHYMeiWSDsHO9E5DA7yD0Qo/8rI6PK6b74tP9dtGMI/dIWFxddcNJbXuqPWhQcx5z3JqLu4SizZSKOnxuPZ4Sme3c9fro2z/17qzghwX/7ZMQLXV4azclz77VTz9j5Bq0svhHM4qXBlAL/h1bR5SvosJNpp6hgR+IuOFxxJo94uA64wIBys8bSq5Y3m8OisATpi22IJabFhb2k8WoiDzYz9FWuQNjBlP48s/IeHTSBOdxXBhPBddeVn+mZ7qAJNbqVecAhiF1CWtrhVtu9Y/Hl/Q6+bOGV2TIkd31DCzXfPSP6j1KpB2ENaCJsnAgMBAAGjIDAeMA4GA1UdDwEB/wQEAwIEsDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQCCoS7rutjA00hwf/WAN5GogOQUrSKBXkLcbBFZ5x0/7S9NcVqYIcEaUAwfbg2nooe/0G1GrBU+OsdmYj4ed2ZXSzuVVmChw3nYZsgOOIMy7tlLt9mGvIjCSLG1f/18crKYZHaRaouIq5r//LOAiu1riSj5jm3s2EvspMGDvVNckquDf0be9o3kjk4my1TJkytg+iOFR4zOPhntORe1jO+9SbVX/2NHHgL4Ylbkk0c2I5GYshlAWzlLXrFTVLThvZsV4RsqdwT4NNDmF+1Aczpulej2NySKPmYoOddCg9F2b5wnhWO4m40bbKNyse9XvGVfZQlsn59/2FlBX7boQKMSjxLJF9jNM6REaOkXIU5Vc7n3hmvTt/MIVa8/qrAEzsyjaGiAaOFwJpFh4SDIjKorJh4kYAyagugJnJjPqFfA33RL2C4YcIbKy2lfsYLdIUCFnpNclHwHRYdRYbD4MSnOZoc5GXW299yV6TA4UZAXdSCUt+oyxAV63EE6yRxqe22F/hnhTPrgP3lGaCV5+8aPrHD/ctHHM7FYc98i7Q5HVHJOHSQB8PmB7ZjpfB+rFniZERLxIL+1RHjbPCVybLXf3DUlvkynnVTuu8d6UKrzbgUN+geDvciSLlP6Wa+7cSW5uBhZAUxBvt3JmuTqqmSN2VcCgmteVs7JhJ+4QwJ5xQAAAD4AAAAAAABR/0sezpbU/kmY389bJ0zGKBwvgJfZf33LVUjeGSht3gAAAAAAD0JCAAAAEwAAAABhqZafAAAAAAAAAgAML4gEP4dAVW2S9jXc4eyfX7ca0pdC2FT5AVd11oAQ8Xpe3q5a5X3EH6BB+cOz4wRiQoTgJbQ7x8Oy9dexGvdR9KMHFGdQt57Mr1/FlktA1QybSmuLl6OImTlSgS3OSpXOCj5e8JZbKNZnWWtjw/i6aDXdo9Dw+vf4uDq5gTvKYySS7izqnT8J60RBVpPjli0ISSfbCdpy1K0NS03ILb4WYK3K7BYmksPFyugd1dFUKbue0tIF7mS7gMV8pzkAnOARxZQRb+L0c8PuaChNz1wa4ioELwFvIl5sAceDNL1JILeYHV2o7cp14pbXdHiKTj1R1YhPzBI/7T26cuROQ2rOSzFQ/6hSj37udaZfKhckOoTJmz7WHgreE7IKXif0RhZq4U/f1yQyuFVo09DMSpWdQS3Jq23L25cbPq6QwKC3dSbSXZv9br1urwMmzN+MzRvSW2097tAGDqMxZIxqlqGQr38RVws5YCFNY/yYJ+JN6meCR5+M8digWxtMI5sFeWJ68tsK4Lp4S/FJpxkh6MzPJav4YM639LCYg4fOH/fbJcTcDNyF+T9X5vrW0lQTCX2aISeNx54+JstqTv/fpnlhgeS8uZuecq1Jsrcu45nzVKNmx3wBw8tFKGSuD7bn5Nrx+IHfipk76dSKLVZfmX0NkUhAoKsdl1iXtUnHgG7XVA==", + "Timestamp": 1638504099 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/1000002.json b/server/service/backend/pchain/indexer/testdata/ins/1000002.json new file mode 100644 index 0000000..8e15d0a --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/1000002.json @@ -0,0 +1,5 @@ +{ + "ID": "oT3nzNB93Lz6vq7Z3BBzgrWYLtNZ6iq8c9V6CikKL5T9nBG4c", + "Bytes": "AAAAAAAB5Qj7IgkMrUS/0zq1+iBI946CuIj8XkH1ZFMD850jlQ0AAAAuAAAAAAACsnc0suqz9VEj8tNgPRUHjDsiDxILds113RctPWa0E0QAAAAAAA9CQw==", + "Timestamp": 1638504099 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/1000004.json b/server/service/backend/pchain/indexer/testdata/ins/1000004.json new file mode 100644 index 0000000..e06211a --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/1000004.json @@ -0,0 +1,5 @@ +{ + "ID": "2Rq1S9c78zZeVg51YB2oGZ6YGtLHkmKezTpAJgZeVS7QSL5pQZ", + "Bytes": "AAAAAAAB2oktQiTDsYakcU4AbyLjQjqshDhP1ocW9yxehKA2QnsAAAAuAAAAAAACG9n23FSK97UdhtE/dZYRD8KTjDbOzj3WtKk+qNAEjFwAAAAAAA9CRQ==", + "Timestamp": 1638504099 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/131475.json b/server/service/backend/pchain/indexer/testdata/ins/131475.json new file mode 100644 index 0000000..1150bd7 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/131475.json @@ -0,0 +1,5 @@ +{ + "ID": "2DxASWodkHwmQURYjddSysMYiWoR2cArEaLEh6s6114gy2zSD4", + "Bytes": "AAAAAAAAWprbLpC5vBTJ9vqtLv4+fPwTw7jJPYjlg6nmmmRlcpMAAAAAAAIBlAAAABRghyYennptQn4qwJsUjcKJVaG8c9fjLJQHyb/jGd5+HAAAAAA=", + "Timestamp": 1635980780 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/1603.json b/server/service/backend/pchain/indexer/testdata/ins/1603.json new file mode 100644 index 0000000..483e59d --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/1603.json @@ -0,0 +1,5 @@ +{ + "ID": "2WSa91XbRZ7h1cHvH7Xp5A3o9etXF4bRCEpj8jdhShXF8Jodt7", + "Bytes": "AAAAAAAA3lJYwOEdGJFePCRFu8PqN+SXC1Cr7QLTd14rwz7ocSAAAAAAAAAGRAAAAAwAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcCVxgE4BrjP6S2fdIGIx++2potcx5AbI7fMVxg8ZQZYAAAAACHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAABQAAEjDYM77AAAAAAQAAAAAAAAAEAAAAAEZfOR3GlhdZQqp61dPCSqhx2xdpAAAAAF9peZkAAAAAX5JYGQAAEjDYM77AAAAAASHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAABwAAEjDYM77AAAAAAAAAAAAAAAABAAAAAW6BWZfolXJebtOq2qV0KP6nUdkJAAAACwAAAAAAAAAAAAAAAQAAAAFugVmX6JVyXm7TqtqldCj+p1HZCQABhqAAAAABAAAACQAAAAEJKVHBW7mVG8tytLcu454BFBK2m/4ge7YSa5NYN6e13RcZSDJTfJ/0BJBr8R1Snsb4G8lET4iJgvKrs1hPWZpaAQ==", + "Timestamp": 1635980695 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/173.json b/server/service/backend/pchain/indexer/testdata/ins/173.json new file mode 100644 index 0000000..479e5fc --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/173.json @@ -0,0 +1,5 @@ +{ + "ID": "yGfD3hSJBAynfRW2BpVfNKFMVmJ7Zn1ZQrMzyraU3G8FeLJ8k", + "Bytes": "AAAAAAAAeX9QABk3qd6VT4kylZpVNJJ17ZajBajXcSxHT3thKc8AAAAAAAAArgAAAAwAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAX3SHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAAFQAAAABhNAiAAAAABQAAAdGpSiAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI8Ah5nMXy8S+KusAZ3rWRid4qPUidLnWBd8lkbIwJ6h9/wAAABUAAAAAYTQIgAAAAAUAAAHRqUogAAAAAAEAAAAAAAAABAAAAAAdX7mcuDFQQpAE9MMpil/OCUEqkQAAAABfaWqGAAAAAGFKngYAAAOjUpRAAAAAAAIh5nMXy8S+KusAZ3rWRid4qPUidLnWBd8lkbIwJ6h9/wAAABYAAAAAYTQIgAAAAAcAAAHRqUogAAAAAAAAAAAAAAAAAQAAAAEwH32O0S0wXeTBeG+PkrztDNrNmSHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAAFgAAAABhNAiAAAAABwAAAdGpSiAAAAAAAAAAAAAAAAABAAAAAaXBiaE6yZKWHg5XYSKA2QPdfB8OAAAACwAAAAAAAAAAAAAAAQAAAAGlwYmhOsmSlh4OV2EigNkD3XwfDgAA6mAAAAACAAAACQAAAAFlnSczylsvFXVgpz0xRqeTMaFnr+QiHDjvzBO5P4RJZFQyzqy9PiGOA3vayoenGUW3mPrvA88UYGNLH0BloBmGAQAAAAkAAAABYP+SAP12xqI1/QEIFbtBvYJESCEg3Xsx+Nb9LG2LiT9vRZ1JT0owO0Be0LqUJ2f75G7trwjCFzGyAoGod/kCpwE=", + "Timestamp": 1635980695 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/2.json b/server/service/backend/pchain/indexer/testdata/ins/2.json new file mode 100644 index 0000000..f73690f --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/2.json @@ -0,0 +1,5 @@ +{ + "ID": "2SqWZicfDEQhebMA7Ab3W9uLHiBv2eMNwQ3w9LSbymCuARiWTZ", + "Bytes": "AAAAAAAA7+BCoIiaLLLp66Zf/w2cHPBmHzZQ0sk3Eh090v47ZJUAAAAAAAAAAwAAAA4AAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAraCHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAAFQAAAABf0BOAAAAABQAAAN+NZlGAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK2kh5nMXy8S+KusAZ3rWRid4qPUidLnWBd8lkbIwJ6h9/wAAABUAAAAAYEa6gAAAAAUAAADfjWZRgAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACtqIeZzF8vEvirrAGd61kYneKj1InS51gXfJZGyMCeoff8AAAAVAAAAAGC9YYAAAAAFAAAA341mUYAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArayHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAAFQAAAABhNAiAAAAABQAAAN+NZlGAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK2wh5nMXy8S+KusAZ3rWRid4qPUidLnWBd8lkbIwJ6h9/wAAABUAAAAAYaqvgAAAAAUAAADfjWZRgAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACttIeZzF8vEvirrAGd61kYneKj1InS51gXfJZGyMCeoff8AAAAVAAAAAGIhVoAAAAAFAAAA341mUYAAAAABAAAAAAAAAAQAAAAAY9JDDcBl2v2fHPPo1OlfZyYaH0UAAAAAX2le+wAAAABfn2x7AAAFPVBl6QAAAAAGIeZzF8vEvirrAGd61kYneKj1InS51gXfJZGyMCeoff8AAAAWAAAAAF/QE4AAAAAHAAAA341mUYAAAAAAAAAAAAAAAAEAAAAB5owpV9cNEP6tJq371txB1Lt6KhMh5nMXy8S+KusAZ3rWRid4qPUidLnWBd8lkbIwJ6h9/wAAABYAAAAAYEa6gAAAAAcAAADfjWZRgAAAAAAAAAAAAAAAAQAAAAHmjClX1w0Q/q0mrfvW3EHUu3oqEyHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAAFgAAAABgvWGAAAAABwAAAN+NZlGAAAAAAAAAAAAAAAABAAAAAeaMKVfXDRD+rSat+9bcQdS7eioTIeZzF8vEvirrAGd61kYneKj1InS51gXfJZGyMCeoff8AAAAWAAAAAGE0CIAAAAAHAAAA341mUYAAAAAAAAAAAAAAAAEAAAAB5owpV9cNEP6tJq371txB1Lt6KhMh5nMXy8S+KusAZ3rWRid4qPUidLnWBd8lkbIwJ6h9/wAAABYAAAAAYaqvgAAAAAcAAADfjWZRgAAAAAAAAAAAAAAAAQAAAAHmjClX1w0Q/q0mrfvW3EHUu3oqEyHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAAFgAAAABiIVaAAAAABwAAAN+NZlGAAAAAAAAAAAAAAAABAAAAAeaMKVfXDRD+rSat+9bcQdS7eioTAAAACwAAAAAAAAAAAAAAAQAAAAHmjClX1w0Q/q0mrfvW3EHUu3oqEwAAAAYAAAAJAAAAAT1qmBkVPt2Q4idPb4Rh8AdZLI3E9i2wtHUQNw3iuRfJaz+aq66z8mteebrSXxNSNkRA0XYV5xdq13tdMCCc2DYAAAAACQAAAAE9apgZFT7dkOInT2+EYfAHWSyNxPYtsLR1EDcN4rkXyWs/mquus/JrXnm60l8TUjZEQNF2FecXatd7XTAgnNg2AAAAAAkAAAABPWqYGRU+3ZDiJ09vhGHwB1ksjcT2LbC0dRA3DeK5F8lrP5qrrrPya155utJfE1I2REDRdhXnF2rXe10wIJzYNgAAAAAJAAAAAT1qmBkVPt2Q4idPb4Rh8AdZLI3E9i2wtHUQNw3iuRfJaz+aq66z8mteebrSXxNSNkRA0XYV5xdq13tdMCCc2DYAAAAACQAAAAE9apgZFT7dkOInT2+EYfAHWSyNxPYtsLR1EDcN4rkXyWs/mquus/JrXnm60l8TUjZEQNF2FecXatd7XTAgnNg2AAAAAAkAAAABPWqYGRU+3ZDiJ09vhGHwB1ksjcT2LbC0dRA3DeK5F8lrP5qrrrPya155utJfE1I2REDRdhXnF2rXe10wIJzYNgA=", + "Timestamp": 1635980695 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/211277.json b/server/service/backend/pchain/indexer/testdata/ins/211277.json new file mode 100644 index 0000000..6b241e1 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/211277.json @@ -0,0 +1,5 @@ +{ + "ID": "arnyzsE9uXJcmFPLwULyYbjUraCvAWbJ3LC6HiJLTA2spD8Eg", + "Bytes": "AAAAAAADS0i+20EQb2SQRLo8RzSfI+z+8aEO7YupsmuKPJ5SHOUAAAAAAAM5TgAAAAEAAAAPAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAABwAAAAAcjPXAAAAAAAAAAAAAAAABAAAAAa6hneoDCuysACGqzE1WIJx8KW4uAAAAAZdWqpwX/IIcZ8GXPS11e112rOScHgGlueUwPcn3NVhLAAAAACHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAABQAAAAAdJYxAAAAAAQAAAAAAAAAAl1aqnBf8ghxnwZc9LXV7XXas5JweAaW55TA9yfc1WEsACEJ0aGVyZXVtZXZtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFl3siY29uZmlnIjp7ImNoYWluSWQiOjQzMTE0LCJob21lc3RlYWRCbG9jayI6MCwiZGFvRm9ya0Jsb2NrIjowLCJkYW9Gb3JrU3VwcG9ydCI6dHJ1ZSwiZWlwMTUwQmxvY2siOjAsImVpcDE1MEhhc2giOiIweDIwODY3OTlhZWViZWFlMTM1YzI0NmM2NTAyMWM4MmI0ZTE1YTJjNDUxMzQwOTkzYWFjZmQyNzUxODg2NTE0ZjAiLCJlaXAxNTVCbG9jayI6MCwiZWlwMTU4QmxvY2siOjAsImJ5emFudGl1bUJsb2NrIjowLCJjb25zdGFudGlub3BsZUJsb2NrIjowLCJwZXRlcnNidXJnQmxvY2siOjAsImlzdGFuYnVsQmxvY2siOjAsIm11aXJHbGFjaWVyQmxvY2siOjB9LCJub25jZSI6IjB4MCIsInRpbWVzdGFtcCI6IjB4MCIsImV4dHJhRGF0YSI6IjB4MDAiLCJnYXNMaW1pdCI6IjB4NWY1ZTEwMCIsImRpZmZpY3VsdHkiOiIweDAiLCJtaXhIYXNoIjoiMHgwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwiY29pbmJhc2UiOiIweDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJhbGxvYyI6eyIwMTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIjp7ImNvZGUiOiIweDczMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDMwMTQ2MDgwNjA0MDUyNjAwNDM2MTA2MDNkNTc2MDAwMzU2MGUwMWM4MDYzMWUwMTA0MzkxNDYwNDI1NzgwNjNiNjUxMGJiMzE0NjA2ZTU3NWI2MDAwODBmZDViNjA1YzYwMDQ4MDM2MDM2MDIwODExMDE1NjA1NjU3NjAwMDgwZmQ1YjUwMzU2MGIxNTY1YjYwNDA4MDUxOTE4MjUyNTE5MDgxOTAwMzYwMjAwMTkwZjM1YjgxODAxNTYwNzk1NzYwMDA4MGZkNWI1MDYwYWY2MDA0ODAzNjAzNjA4MDgxMTAxNTYwOGU1NzYwMDA4MGZkNWI1MDYwMDE2MDAxNjBhMDFiMDM4MTM1MTY5MDYwMjA4MTAxMzU5MDYwNDA4MTAxMzU5MDYwNjAwMTM1NjBiNjU2NWIwMDViMzBjZDkwNTY1YjgzNjAwMTYwMDE2MGEwMWIwMzE2ODE4MzYxMDhmYzg2OTA4MTE1MDI5MDYwNDA1MTYwMDA2MDQwNTE4MDgzMDM4MTg4ODg4NzhjOGFjZjk1NTA1MDUwNTA1MDUwMTU4MDE1NjBmNDU3M2Q2MDAwODAzZTNkNjAwMGZkNWI1MDUwNTA1MDUwNTZmZWEyNjQ2OTcwNjY3MzU4MjIxMjIwMWVlYmNlOTcwZmUzZjVjYjk2YmY4YWM2YmE1ZjVjMTMzZmMyOTA4YWUzZGNkNTEwODJjZmVlOGY1ODM0MjlkMDY0NzM2ZjZjNjM0MzAwMDYwYTAwMzMiLCJiYWxhbmNlIjoiMHgwIn0sIkZGNzYwNUFDNDQwNWM0RTM4QTc4MTM2Q0I5MkI0MjY2OEE1Q0Q5ZjgiOnsiYmFsYW5jZSI6IjB4MjFlMTllMGM5YmFiMjQwMDAwMCJ9fSwibnVtYmVyIjoiMHgwIiwiZ2FzVXNlZCI6IjB4MCIsInBhcmVudEhhc2giOiIweDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAifQAAAAoAAAAAAAAAAgAAAAkAAAABb8vD2KFETJxoI/RapGWd013Do/8TGSlLU7fkHprNYDxd80+a9oMKG8LrxpE1wRsYAFcmtn2jls6/ju+ptaV9ZgEAAAAJAAAAAA==", + "Timestamp": 1635980839 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/211333.json b/server/service/backend/pchain/indexer/testdata/ins/211333.json new file mode 100644 index 0000000..2fb29ad --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/211333.json @@ -0,0 +1,5 @@ +{ + "ID": "VjXMGrfyxyLZ4GGs3VC5H6mzteQtGVLZpkUs1dwFQnKJRawGt", + "Bytes": "AAAAAAAArHeOi2cg48xqUzGcpdOlIi0AKqkTyEobL/fe4yuObtcAAAAAAAM5hgAAAA0AAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIeZzF8vEvirrAGd61kYneKj1InS51gXfJZGyMCeoff8AAAAHAAAAABtMhoAAAAAAAAAAAAAAAAEAAAABrqGd6gMK7KwAIarMTVYgnHwpbi4AAAABlyZAqNQvkdo18nQz9OL521V5zs4bAp53jlONig/0ckQAAAAAIeZzF8vEvirrAGd61kYneKj1InS51gXfJZGyMCeoff8AAAAFAAAAABtbyMAAAAABAAAAAAAAAABpe15sVOHYe0zML075jNG3dLOARAAAAABfla6PAAAAAF+wDFMAAAAAAABOIGGEy6HhWupX3eGiZ59izYdhwPXDdiaxAi9yIL6WfYZDAAAACgAAAAEAAAAAAAAAAgAAAAkAAAAB2bN+dWOkOaerMShozEHfUYqqIrUgiVoM+AWJUw7+qu1qvo4M75mVuEA1jnZdy8IkXFI40NxO/uBOMgMepjr4MQAAAAAJAAAAAdmzfnVjpDmnqzEoaMxB31GKqiK1IIlaDPgFiVMO/qrtar6ODO+ZlbhANY52XcvCJFxSONDcTv7gTjIDHqY6+DEA", + "Timestamp": 1635980839 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/382.json b/server/service/backend/pchain/indexer/testdata/ins/382.json new file mode 100644 index 0000000..9dba46a --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/382.json @@ -0,0 +1,5 @@ +{ + "ID": "2BZD8LSEnArSNwiE74usRGKihA6yDnLwZdGXe1yEfMxwpBFdZB", + "Bytes": "AAAAAAAAGKO7A74qqsSyeldFnlMP+Tz9pXRTgkT5PNApkBMKVcEAAAAAAAABfwAAAA4AAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAatIgRyKEseErvEMr5qMOsxU9FmlHhpFtBI1rRoY0Ji/AAAAACHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAABQAAACw8w2eAAAAAAQAAAAAAAAAEAAAAAGPSQw3AZdr9nxzz6NTpX2cmGh9FAAAAAF9pbdkAAAAAX9AJqQAAACw8w2eAAAAAASHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAABwAAACw8w2eAAAAAAAAAAAAAAAABAAAAAby/cRhQFA8zXE9P5j7E8jLZtMTrAAAACwAAAAAAAAAAAAAAAQAAAAG8v3EYUBQPM1xPT+Y+xPIy2bTE6wAAAAEAAAAJAAAAAZvooY5v/alNK8gLguTor6YiQIg7MGqmWAdUdM2NS9wOK67pWAa9B52tJo2xTEPtV/PT5mgAVPQD9SONMU4nXK4B", + "Timestamp": 1635980695 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/48.json b/server/service/backend/pchain/indexer/testdata/ins/48.json new file mode 100644 index 0000000..8b8181c --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/48.json @@ -0,0 +1,5 @@ +{ + "ID": "2UqBDUJMHRGPxS78watLZqpqFaCtM3f5SuoxPjfSbNGATk3j66", + "Bytes": "AAAAAAAB7A8t8V+WXYJFpPP1KXB+mR/qupONQIwim6rZNtIuEVMAAAAAAAAAMQ==", + "Timestamp": 1635980695 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/5981.json b/server/service/backend/pchain/indexer/testdata/ins/5981.json new file mode 100644 index 0000000..52d87dd --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/5981.json @@ -0,0 +1,5 @@ +{ + "ID": "2jhYxP7xDVi3yb7o4WHxPNKxuCHVBqhsMwf6DqsNG452sDDF1g", + "Bytes": "AAAAAAADh4h5P7mpJP07eoM4/z6PbYy6orQ29Q5xqU3REIjwc9AAAAAAAAAXXgAAAAEAAAAQAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAABwAAAAAAD96AAAAAAAAAAAAAAAABAAAAAYoTsBccdQN1YzryeQS3SFhf135+AAAAAhzz88JNF19c1OOUyHVlAQ0pwzY3c0Dj/ReR9kckxBjPAAAAACHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAABQAAAAAAifCAAAAAAQAAAABQBMb8uUdmk+qZFSNmgitndThpjC1KSwYp3+LB839DRwAAAAAh5nMXy8S+KusAZ3rWRid4qPUidLnWBd8lkbIwJ6h9/wAAAAUAAAAAAB6EgAAAAAEAAAAAAAAAAAAAAAsAAAAAAAAAAAAAAAIAAAACTzi1/JVVneu2f3YpAatgz2TMJ9mKE7AXHHUDdWM68nkEt0hYX9d+fgAAAAIAAAAJAAAAAYW8zS8klm1VKDW4Z8/WEypAWvUqKj4P1lEMTBh/Xei5YrIOwEvVvA6r/4dWU9XXAp3QILJkI2O0wUABu6BhVeABAAAACQAAAAHnjDSPK7EJi1LGZKHywlgx5Y0PnkC79zFck+9E/XsIMyXWON/YeNg1p88ghQUPbFq3Z8BFuf/XjkDrTwX220d6AA==", + "Timestamp": 1635980696 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/8.json b/server/service/backend/pchain/indexer/testdata/ins/8.json new file mode 100644 index 0000000..a7e77d6 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/8.json @@ -0,0 +1,5 @@ +{ + "ID": "2vxS14bQvrcd4JJ1XALpTJE2NGWGAWxsHJexuA5PM4LD4XSi6B", + "Bytes": "AAAAAAAE9KG4Wf3atQQGG8D9L40oOgP/ORCtLnSiGo1//akb7yQAAAAAAAAACQAAABEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIeZzF8vEvirrAGd61kYneKj1InS51gXfJZGyMCeoff8AAAAHAAAEjCcay4AAAAAAAAAAAAAAAAEAAAABMJS5B3rAbpiimmM+IWh/9d91UekAAAAAAAAABAAAAADtXzg0HkNuXUbiuwC0XWKul9GwUMZLxjSuEGJnOeNcSwAAAAGhmt7/ImTY0zTL99MdTK2ruW4MceW+rtIijG013d5j8QAAAAAh5nMXy8S+KusAZ3rWRid4qPUidLnWBd8lkbIwJ6h9/wAAAAUAAASMJyoNwAAAAAEAAAAAAAAAAQAAAAkAAAABCiP9Hl5KkhvI6escGvcCtjpQBxdQDuBHbsQLXFAQcso5zc9h6KYM8H0TpRgLr5li3w9eCvtxr4XvEPrq/5ZZFwE=", + "Timestamp": 1635980695 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/806002.json b/server/service/backend/pchain/indexer/testdata/ins/806002.json new file mode 100644 index 0000000..f9682fe --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/806002.json @@ -0,0 +1,5 @@ +{ + "ID": "Kr5LmM7WtnV9RYSuUQUYBYKPti9LzxBxhsNUNAd2gcysC6Rhu", + "Bytes": "AAAAAAAAPMP/hpZ1jDVYyvJZYZpnq7aSUrQYIRQ4HqEWLU04h28AAAAAYUvKIgAAAAAADExyAAAE/DCCBPgwggLgoAMCAQICAQAwDQYJKoZIhvcNAQELBQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTIxMDUxNzE3NTg1NVoYDzIxMjEwNDIzMTc1ODU1WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDDhCjDzTD4J2SDx3k3o2D/IKZXEvR+BlKENWP/APVroaDBU73sBXuOUjavd966Kfy06V6HThX6zickWorUJK/E51EJlpuAyLAcFiRtllU/4umyAKn+2N7cV7MUra0pnWZk8IQnfioetiBLPBG/l9oRmLe1vjWCC8LUg96PJRCE0bmbx1qDyjqeufh7SmNyUh1okTG6+F1DFHKV2GUBisowgrkmnNsxA9nDykVoWWBu9nyY8yruTipsY/MuP2XrKXo2/UtxLpbi52U8umlju1LGAkMyC2pYmwvFd2g+IqrkCNkstzNsNDc0Tw4nxc29BiXmMLRF8VGVYiBAjLo5TMWCdkw99tsS/iN4+LKx0Kh8ZK7ErZAJi+lIQy80QkPKm2s2QiPFw4XMMs6q2dszZ4F60P9KCAph1/hh/QScfY9quT+U70i0VKc3Kc5Emx5EYx2vqWvjQIMNdGbLQ1LqHEggDuxPQ2/LXsCL+m3SQDRj8KjUFNzl6jA4J00Cn38blBZxc1eMWuRXieFh2Y+kwu01TlLM+gOrnuar98ZEKiVFzqSPURk6sE0DIFFxMK3c/sZHdJJh1jUnX5vvd+gwIi0nPDFHNtpcsuByWtJLjUTEpi8I5aO8ZSPX5VZVCdF96r1viIpmEm65VfXV+jKeCQ5n306Gp0ibrFRUX1kJtrBtJwIDAQABo1MwUTAdBgNVHQ4EFgQUXGty7SrB4wAvkYSeCwY9cpZ/4scwHwYDVR0jBBgwFoAUXGty7SrB4wAvkYSeCwY9cpZ/4scwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAQ5Y9eV0UEKNuq2Im6UuOHh5o2hjDxGp+/R7rbmS25b5f632EPI6eRiqAre/C9GzfYjMzsl7rKcczzufAfRVxAvNYj07YrN+20baQxzthadUijO0jGSUnE03+0FHi0ARolRcbwL166dXeZXCVbKa8DyyQTNxWd8ZG0uyk59zBtRi7eiKC/gxNkbi+Z/Jr/4t8KLlPIFDoXp/R7zFzTPCCF850OAyUgM8xTMrydxoUS8Xa3hNyFfjfORPzCMBGMTH1OHbb1z4QZXS32gAxPmCILwoGtqxaysxfHsK4d3l7fAXXZYUWGirX9y6EmpsgSJasNX8lgT9M2byaiV6DED5Gkh4kq8SS93Z5OcUI7CZbzJZ0LSi4aCw4y0hrhziv2CKlG74jaVr8SlLX0LPSewLZT+IhyWoy3iNjqD7CVrkdfhgu5eEGYEw4PWAgZ7yeZC+BdwHKRnnfleOq6IoXUpWXjdkeBPF3HwnG5JnCVwslsDOe6ZVz8IHHL8lsyYWrQJOT9FZi+qRCnH80VzpAoJ5fBeV8Bm82XVOyAIe6lssagvggBaI9Pau+p2EvkvetTRuXZ+GYgeoizgEblo4+/s2HfLjZH1gDYHBdkYziYVvJlMadqynSEAgTFvIiZ3yKHtaMVC9zaFpNB9YFIOHDQwaKi+9cQvHGFE01CuusP+re6CIAAAJsAAAAAAAEbED0KZO/FBzaPvFHU2Nw/5jTptqhB83e+rd/7zBJUYEAAAAAAAxMcwAAABIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIeZzF8vEvirrAGd61kYneKj1InS51gXfJZGyMCeoff8AAAAHAAAADkRt7+oAAAAAAAAAAAAAAAEAAAABNVZ9JDl+VMb3eXySKGNIvSMw2hkAAAACP27GGfsnTmTdn8b/CEoR6I4LoW9ThQzZ1OIMu64IvosAAAAAIeZzF8vEvirrAGd61kYneKj1InS51gXfJZGyMCeoff8AAAAFAAAAGgP9yE8AAAABAAAAAD9uxhn7J05k3Z/G/whKEeiOC6FvU4UM2dTiDLuuCL6LAAAAASHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAABQAAAAAgZOobAAAAAQAAAAAAAAAA7V84NB5Dbl1G4rsAtF1irpfRsFDGS8Y0rhBiZznjXEsAAAABIeZzF8vEvirrAGd61kYneKj1InS51gXfJZGyMCeoff8AAAAHAAAAC9/lgEAAAAAAAAAAAAAAAAEAAAABNVZ9JDl+VMb3eXySKGNIvSMw2hkAAAACAAAACQAAAAE3L6xEp1I5AuHS5d0vBXFF/xL0xBb/wWJBzqatl/uhJxnWFNTpKiRGqCDAIMdkWPhjbpehVe5S008grC2X2dZkAAAAAAkAAAABNy+sRKdSOQLh0uXdLwVxRf8S9MQW/8FiQc6mrZf7oScZ1hTU6SokRqggwCDHZFj4Y26XoVXuUtNPIKwtl9nWZAAAAAIAgIXozupCTgMNDY0dDShklZ+PushccqwhDL1ockogwVtTqFTZUHZGrc2OHbWZV4e+ptuxMc8lD6irf5QBS9jukGgC6NbsO0+2wuyDZSyw/3PmBsC6zDa9EOUhCpPG3WkjCAT9nW8z+ZvkNkHV2rh8FV4J++lxOXwxH4+nwZuRafduTSYsr0Clf4YvkNjz4H7DM4J1vdQ6CxIsJsGcn1RPk3lcof8wnnNtU6BWR9g/8q+ZmJ7wSenSxtDuAujlowQP/pHQOvPwV0Bev7KZihtnzDEqDfGKSyMZrBJTai19apeV5lyKOcESaIvw84JaYUaY2NxeA16Jug6Jm4BJl5Sr1+6P842HbVVQw+MxQebborlYItMEPHBjAzPKnTg3KcgtrzNfuoGBdgXNsfwaGAFutT4S0CZog9TiIj9Ucm2OJpaV/m729AkmPNbXQ/+m1qC2paH3qudKIlQfbgVcr8xDbp3SISI5ouJKr41sSYqgzhw9x18h2Q2ES1PITJbJhX/rXAX/cqaVn0gxTaLkQY+i0MBMgKMBbV7CuwrRITAQhbaU4ZSOkQklGCPVlT3A05mq8NdnJ2yXnJKV97VM8LQOSqFZU/m0ddib0HUgPVB8+l0Mwz2B8xSbXav3vaQm3ePSZqyP7mWp4NsNa1aZt7YiIseZTTXA5+Lg1m7d4jMfYOA=", + "Timestamp": 1635981783 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/810424.json b/server/service/backend/pchain/indexer/testdata/ins/810424.json new file mode 100644 index 0000000..cbcc687 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/810424.json @@ -0,0 +1,5 @@ +{ + "ID": "pxgTHS2xtw5NQV7G9YbHZLoZvRdgsrjhMEvewDYdSGFNYJ7TG", + "Bytes": "AAAAAAAAiWLG4eKlhDszp9qtbY4p3WpZoMm2cg2wTdfrzy+zLZgAAAAAYU4Y0gAAAAAADF24AAAEoTCCBJ0wggKFoAMCAQICAQAwDQYJKoZIhvcNAQELBQAwADAgFw05OTEyMzEwMDAwMDBaGA8yMTIwMDkyNTAwMjc1NVowADCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANHb750X/8yWpOlMSDFfgRrzzdMwGYsxoGiuLvF922414p29LPbKFsTspmuQQq2SVCCx23gONFvLeVcXUdKwsf91xYX38TqOOzQnrMD9+KcCrXaEA68T3FIMo6DjAvdt8OoVATgU0P+4in4fePYeOv5XeoLPO9jyFhBirwRhG3M3bY5a+SaFVpD6DkImzYVxfWa0rR/qWSTpM0uUxwi1DjuiJem/E/MyhzX1wrd3ka9VCLn19Bssf2kRZHAz2lVANtunL2d3MtFbBK9YInaWQHpJ452EtjQ7ZRzBnAgwm7lzz4cUFS3eTNmEMxkdsg1jA2/MDb3OXaKV8LomNNTesSR69BqT7HT0f55aV4G1Muc7Xf2xHVqJf3AbAL4TcBJhzRtOuxSlmMjYGIll+1t4RXo/72Z6jLXj/lxvllCJVo0QsNlxjw3Lv4uD6pbZwa9nHArE/kt7FvXWhdrVsdzq0O5UlVEITIJamoOkf4bGgFy1q3LS9O3ASxU/jEOoj4MX6gY80zK5mpCWLrEEI9Tz1qrhjCQ/rtZAdWmn549ADeeDSmC8+reIrN8tEbyvyLv/s6azt3bcy11EdvDHijBVpXgqFW5R4/roHB8CJ2smJVtGA/FwtkETjsp5Z2FQo18sSkR6RI4pqZbeRW7WvgnTmyXmHcZ4gKVtHWQSJFVPqdsBAgMBAAGjIDAeMA4GA1UdDwEB/wQEAwIEsDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQCOjUNv/G+78wJ+qDdBVCATKOsYEyA/iEWnR5wNTUtNbv9RllpYDe8Tv/dcsBoD0GMWhYnvT7I6uICGouEJQoOqOZou3YY7b8INjH9j2xbzDLyLSJ8sz/p0KXdxDjan6LblRKKb8jsHAvt/riy+jn5xHmgPjIHEOKk5Yh9G1P64iTEAR9OBh6mHfFs8XrQq5vQuMCgef5+o0XtFUVZFSzlg888nFqwWB4VT1f4BTEjSr/SZ6goZ5IPfLKKrj8CyDUW/cjFGrj/o94Yhm7RmhsIU8AP7V8bbaRX8xlRV5eX/FZmUQmu2gn6sNiHoU7sUg17ebQHZ33NA6roLexdrAuZTXpsxFi2xdQiEUnGww7eveYi9PEiU3yuDgJjo+oLyPjNB01yGEga+cl5NB/1OTIGPqtSa4B+BSdgPuwhlsbC8v5ABMHPNttGkjxPybgdExTznUMrmOjK5hDTVtOWzwlk8D8cQUwr8YGxSLQEZLf5RrT6JVUwe1iE6109v9RrFnaRj+wBB/IK8L6aYpnBanGFu2KHboS9aRnKpm3TpP0t4jpqBKpkUbnDgEk9P58Iy42151c1rs+Ii2Zhh6duH95k4oEUUKMSNnTzvDs+K2md0EcAiclCyi/FLptfTSOXnaOy6J7VURTN++27MKwbYNAmx/J4FlOsTdbf1kRczhixaxQAAAXsAAAAAAASdDjUUmnpcwYOOPus4VidQv6R26lp/AvVbsq1X9/xGhwAAAAAADF25AAAAEQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEh5nMXy8S+KusAZ3rWRid4qPUidLnWBd8lkbIwJ6h9/wAAAAcAAAACcnHfgAAAAAAAAAAAAAAAAQAAAAEoY9Yqmh6b2TgfkBXb88u1fkL6VgAAAAAAAAAA7V84NB5Dbl1G4rsAtF1irpfRsFDGS8Y0rhBiZznjXEsAAAABol51eStLAfaqGKx/LQKDNgAl01VaAMyr/hbuVIlDoekAAAABIeZzF8vEvirrAGd61kYneKj1InS51gXfJZGyMCeoff8AAAAFAAAAAnKBIcAAAAABAAAAAAAAAAEAAAAJAAAAAaBbCqbZrNscCI9yIfEiQC2EwA0+cFPaGrJj/+Tc4Cm9FQU520kb4nVD820aMz2lz+WV3GaDtt0rdRKTn5VYlZMAAAACAJug6KS+9XqI7g8pXzL1j+8rAISPTC4He1EOufQKRfUOfJuQTBdHytMBT3oifjwhMJXkzGm1z3hdrMujbAYD5y4w8p05yF621ri7tEc6AhAPBGKg7USJo7wbrAdDyQx6mcH54sIf6XXuaNLtyfzuzo2Q2U6/ViO0RZFL5vHjciDlORJF7nCkEQP0wW50FYPv5gf90O212KgHit0w3+zm/tBFaZQcaQMoQfBcq8WHjTVFJmQfDhhNrtiiA/ry9ek3S6OfCnWSLHXA1AXYWoWVThdBmWkeomEiMdGWur4EpXLAt3f171JKXUHwaYK1tF1ljWchtbsnqW0NBPtUuz9jWGVxu80dWq1hKF/Ocvgtgat61RExmrgggZG/9q/Q5NjfmWO9Rxp7r+rSkdA78lshki4qtnIKmKk7bvjJCVCC283qkb9BDhpVZ+r5MxvwXFuxAJo9DWuVW5FJZtRG6KJwDcmwuUmfXuXRkTsJj1UkCfo0RY4pVsXIuBS0DvjP2WBX6WIa/FnjQjio/LnC/X68OCzAKVJqBUn4eC6dWi4TFEZEqYkntjRGUtqObJtZ9iUhNPLHyINfkOSF7sQmpIjL+7nfTW1AF4p2KxTtaiWk6TfNf2OdW6xostkzQ9arOxhEDPPzkqtQLeZt0SGX3trtr60GC8I/zmD6qeZs5otvHoZb", + "Timestamp": 1635981793 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/ins/911.json b/server/service/backend/pchain/indexer/testdata/ins/911.json new file mode 100644 index 0000000..2fd6daa --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/ins/911.json @@ -0,0 +1,5 @@ +{ + "ID": "2PpHLb6QA8grZaGP9nmQmADjXBetSYyjPRmR9eqoSfo4yinxq", + "Bytes": "AAAAAAAEQzLLJvLFQ/l58H5qJ6dqV+qcedm2n7QyGoDYQqAwFN4AAAAAAAADkAAAABIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWPL4uczih85S+OSRCquEp9Ej8Ke+l2S6idkX+pcTuc6AAAAACHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAABQAAAACysdmAAAAAAQAAAAAAAAAEAAAAAO1fODQeQ25dRuK7ALRdYq6X0bBQxkvGNK4QYmc541xLAAAAASHmcxfLxL4q6wBnetZGJ3io9SJ0udYF3yWRsjAnqH3/AAAABwAAAACyopdAAAAAAAAAAAAAAAABAAAAAW9OAPSXFC4/R+ktRyNPCasz6c60AAAAAQAAAAkAAAABmDTArw3ZLAqi/r3H57FH17S8RoJc+L0abfMGvH55XyUKBVVaDnd2NzJZ/CLsP1FOLO7VCo62pCiwsoS2AvwDAwE=", + "Timestamp": 1635980695 +} \ No newline at end of file diff --git a/server/service/backend/pchain/indexer/testdata/outs/0.json b/server/service/backend/pchain/indexer/testdata/outs/0.json new file mode 100644 index 0000000..25550a7 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/0.json @@ -0,0 +1,16 @@ +{ + "id": "4AqeFPxtTW4B5D6oR8gRZTvRKnnqkUWiV6mUNZxjUMbQKYWpi", + "type": "*block.ApricotProposalBlock", + "parent": "2FUFPVPxbTpKNn39moGSzsmGroYES4NZRdw3mJgNvMkMiMHJ9e", + "timestamp": 1600740000000, + "height": 1, + "transactions": [ + { + "id": "N3MsLv3aEmeQ9Z82m6JjrmMV884RCM88Xc3y97wv6NXPpvoSK", + "unsignedTx": { + "time": 1600740000 + }, + "credentials": [] + } + ] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/1.json b/server/service/backend/pchain/indexer/testdata/outs/1.json new file mode 100644 index 0000000..ef21a32 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/1.json @@ -0,0 +1,8 @@ +{ + "id": "2peK2rrira1UUdFuLWZTMHFdGe3JY8sQeKLfPj5jhPdK8mMAcJ", + "type": "*block.ApricotCommitBlock", + "parent": "4AqeFPxtTW4B5D6oR8gRZTvRKnnqkUWiV6mUNZxjUMbQKYWpi", + "timestamp": 1599696000000, + "height": 2, + "transactions": [] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/1000000.json b/server/service/backend/pchain/indexer/testdata/outs/1000000.json new file mode 100644 index 0000000..8e896b3 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/1000000.json @@ -0,0 +1,54 @@ +{ + "id": "d7WYmb8VeZNHsny3EJCwMm6QA37s1EHwMxw1Y71V3FqPZ5EFG", + "type": "*block.ApricotStandardBlock", + "parent": "5615di9ytxujackzaXNrVuWQy5y8Yrt8chPCscMr5Ku9YxJ1S", + "timestamp": 1638504075000, + "height": 1000001, + "transactions": [ + { + "id": "AkTcR1J5b6qPdLaUDycs1YqPAoNY9aPNyb33VSnvyQ6vTCryQ", + "unsignedTx": { + "networkID": 1, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [ + { + "txID": "DTqiagiMFdqbNQ62V2Gt1GddTVLkKUk2caGr4pyza9hTtsfta", + "outputIndex": 0, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 13839124063, + "signatureIndices": [ + 0 + ] + } + } + ], + "memo": "0x", + "destinationChain": "2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5", + "exportedOutputs": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-avax1jkjyvlwclyu42n4yuegpczpfgwrf8r9lyj0d3c" + ], + "amount": 13838124063, + "locktime": 0, + "threshold": 1 + } + } + ] + }, + "credentials": [ + { + "signatures": [ + "0xc79711c4b48dcde205b63603efef7c61773a0eb47efb503fcebe40d21962b7c25ebd734057400a12cce9cf99aceec8462923d5d91fffe1cb908372281ed7385801" + ] + } + ] + } + ] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/1000001.json b/server/service/backend/pchain/indexer/testdata/outs/1000001.json new file mode 100644 index 0000000..f146cbb --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/1000001.json @@ -0,0 +1,16 @@ +{ + "id": "2Mbfba1sSJw9yrp1ZGpEs4aa7AFkBEofmrXty1CPL2hrnqGeVT", + "type": "*block.ApricotProposalBlock", + "parent": "d7WYmb8VeZNHsny3EJCwMm6QA37s1EHwMxw1Y71V3FqPZ5EFG", + "timestamp": 1638504095000, + "height": 1000002, + "transactions": [ + { + "id": "xBbJXadKPJrLdEsEFNNtVgMCYnn6UkRqZW9Eox8F4LT7ZMZLZ", + "unsignedTx": { + "time": 1638504095 + }, + "credentials": [] + } + ] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/1000002.json b/server/service/backend/pchain/indexer/testdata/outs/1000002.json new file mode 100644 index 0000000..4d4886c --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/1000002.json @@ -0,0 +1,8 @@ +{ + "id": "bMRTouVMNG6m82UPnUg6QN2o9X19g6rkRtsJRSrSNiY7GaAjJ", + "type": "*block.ApricotCommitBlock", + "parent": "2Mbfba1sSJw9yrp1ZGpEs4aa7AFkBEofmrXty1CPL2hrnqGeVT", + "timestamp": 1599696000000, + "height": 1000003, + "transactions": [] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/1000004.json b/server/service/backend/pchain/indexer/testdata/outs/1000004.json new file mode 100644 index 0000000..fb8de3b --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/1000004.json @@ -0,0 +1,8 @@ +{ + "id": "2Kbis7MxM1MUqdfNxKdhP7FAhEdNYBHDvPivFWmGktDV9Au5nL", + "type": "*block.ApricotCommitBlock", + "parent": "DGRnAy4HYV52uWCRKZV6UXw9s9qZKCat5Xss8PANDaR5mngUz", + "timestamp": 1599696000000, + "height": 1000005, + "transactions": [] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/131475.json b/server/service/backend/pchain/indexer/testdata/outs/131475.json new file mode 100644 index 0000000..7af9cbc --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/131475.json @@ -0,0 +1,16 @@ +{ + "id": "2DxASWodkHwmQURYjddSysMYiWoR2cArEaLEh6s6114gy2zSD4", + "type": "*block.ApricotProposalBlock", + "parent": "guP5pc9yADiupb6riw8fPaWwNTpw5kPwhF43nM2csamDqrrNH", + "timestamp": 1599696000000, + "height": 131476, + "transactions": [ + { + "id": "2U9eR7zZPZ5Li69MxmFhVxwntZFTcQn5MqcFAt6vrHB5oFfWVf", + "unsignedTx": { + "txID": "jWgE5KiiCejNYbYGDzhu9WAXrAdgwav9EXuycNVdB62rSU4tH" + }, + "credentials": [] + } + ] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/1603.json b/server/service/backend/pchain/indexer/testdata/outs/1603.json new file mode 100644 index 0000000..62b8e90 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/1603.json @@ -0,0 +1,67 @@ +{ + "id": "2WSa91XbRZ7h1cHvH7Xp5A3o9etXF4bRCEpj8jdhShXF8Jodt7", + "type": "*block.ApricotProposalBlock", + "parent": "2guuhVtf9anfhF1U5ngbVi4Kf9Wn68N5hguJ4dsDGsFouU9Adb", + "timestamp": 1599696000000, + "height": 1604, + "transactions": [ + { + "id": "9uDu6E25SBoD9t4i2NLxFYM4VfgwxeaXaQz6Xh84K3Cvtc8BX", + "unsignedTx": { + "networkID": 1, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [ + { + "txID": "2TpKwzP7vZdj9TxkEG9WVnpzNhHh3QR92MSBdmwMMRWk7Lcisd", + "outputIndex": 0, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 20000995000000, + "signatureIndices": [ + 0 + ] + } + } + ], + "memo": "0x00000000", + "validator": { + "nodeID": "NodeID-7R6NeTcnqaBeCvS8zdJryQnmBBCPJXhcW", + "start": 1600747929, + "end": 1603426329, + "weight": 20000995000000 + }, + "stake": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-avax1d6q4n9lgj4e9umkn4td22apgl6n4rkgfddj9l0" + ], + "amount": 20000995000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-avax1d6q4n9lgj4e9umkn4td22apgl6n4rkgfddj9l0" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 100000 + }, + "credentials": [ + { + "signatures": [ + "0x092951c15bb9951bcb72b4b72ee39e011412b69bfe207bb6126b935837a7b5dd17194832537c9ff404906bf11d529ec6f81bc9444f888982f2abb3584f599a5a01" + ] + } + ] + } + ] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/173.json b/server/service/backend/pchain/indexer/testdata/outs/173.json new file mode 100644 index 0000000..31a6751 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/173.json @@ -0,0 +1,108 @@ +{ + "id": "yGfD3hSJBAynfRW2BpVfNKFMVmJ7Zn1ZQrMzyraU3G8FeLJ8k", + "type": "*block.ApricotProposalBlock", + "parent": "vWVBLVyPubQkodeHPksSJvF6BpS1SLZuEjg7yskifdzUiXu34", + "timestamp": 1599696000000, + "height": 174, + "transactions": [ + { + "id": "2ZJdGepMparFEjdyVYfwnnyXMMD2iEsvfW39FW2khpvyTaw8Er", + "unsignedTx": { + "networkID": 1, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [ + { + "txID": "11111111111111111111111111111111LpoYY", + "outputIndex": 6109, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "locktime": 1630800000, + "input": { + "amount": 2000000000000, + "signatureIndices": [ + 0 + ] + } + } + }, + { + "txID": "11111111111111111111111111111111LpoYY", + "outputIndex": 9152, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "locktime": 1630800000, + "input": { + "amount": 2000000000000, + "signatureIndices": [ + 0 + ] + } + } + } + ], + "memo": "0x00000000", + "validator": { + "nodeID": "NodeID-3gKGf3khMvQDt84cTwJMi7c14aCbtevts", + "start": 1600744070, + "end": 1632280070, + "weight": 4000000000000 + }, + "stake": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 1630800000, + "output": { + "addresses": [ + "P-avax1xq0hmrk395c9mexp0phcly4ua5xd4nveuw6vhs" + ], + "amount": 2000000000000, + "locktime": 0, + "threshold": 1 + } + } + }, + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 1630800000, + "output": { + "addresses": [ + "P-avax15hqcngf6exffv8sw2asj9qxeq0whc8cwf8ty29" + ], + "amount": 2000000000000, + "locktime": 0, + "threshold": 1 + } + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-avax15hqcngf6exffv8sw2asj9qxeq0whc8cwf8ty29" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 60000 + }, + "credentials": [ + { + "signatures": [ + "0x659d2733ca5b2f157560a73d3146a79331a167afe4221c38efcc13b93f8449645432ceacbd3e218e037bdaca87a71945b798faef03cf1460634b1f4065a0198601" + ] + }, + { + "signatures": [ + "0x60ff9200fd76c6a235fd010815bb41bd8244482120dd7b31f8d6fd2c6d8b893f6f459d494f4a303b405ed0ba942767fbe46eedaf08c21731b20281a877f902a701" + ] + } + ] + } + ] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/2.json b/server/service/backend/pchain/indexer/testdata/outs/2.json new file mode 100644 index 0000000..8caecdf --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/2.json @@ -0,0 +1,247 @@ +{ + "id": "2SqWZicfDEQhebMA7Ab3W9uLHiBv2eMNwQ3w9LSbymCuARiWTZ", + "type": "*block.ApricotProposalBlock", + "parent": "2peK2rrira1UUdFuLWZTMHFdGe3JY8sQeKLfPj5jhPdK8mMAcJ", + "timestamp": 1599696000000, + "height": 3, + "transactions": [ + { + "id": "2gZCB2pSLarX8YRsYkxScLZr18PNNVnoJCj1jDh2MUTBCYUP4x", + "unsignedTx": { + "networkID": 1, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [ + { + "txID": "11111111111111111111111111111111LpoYY", + "outputIndex": 11112, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "locktime": 1607472000, + "input": { + "amount": 960150000000, + "signatureIndices": [ + 0 + ] + } + } + }, + { + "txID": "11111111111111111111111111111111LpoYY", + "outputIndex": 11113, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "locktime": 1615248000, + "input": { + "amount": 960150000000, + "signatureIndices": [ + 0 + ] + } + } + }, + { + "txID": "11111111111111111111111111111111LpoYY", + "outputIndex": 11114, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "locktime": 1623024000, + "input": { + "amount": 960150000000, + "signatureIndices": [ + 0 + ] + } + } + }, + { + "txID": "11111111111111111111111111111111LpoYY", + "outputIndex": 11115, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "locktime": 1630800000, + "input": { + "amount": 960150000000, + "signatureIndices": [ + 0 + ] + } + } + }, + { + "txID": "11111111111111111111111111111111LpoYY", + "outputIndex": 11116, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "locktime": 1638576000, + "input": { + "amount": 960150000000, + "signatureIndices": [ + 0 + ] + } + } + }, + { + "txID": "11111111111111111111111111111111LpoYY", + "outputIndex": 11117, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "locktime": 1646352000, + "input": { + "amount": 960150000000, + "signatureIndices": [ + 0 + ] + } + } + } + ], + "memo": "0x00000000", + "validator": { + "nodeID": "NodeID-A6onFGyJjA37EZ7kYHANMR1PFRT8NmXrF", + "start": 1600741115, + "end": 1604283515, + "weight": 5760900000000 + }, + "stake": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 1607472000, + "output": { + "addresses": [ + "P-avax1u6xzj47hp5g0atfx4haadhzp6jah52sngfgdvg" + ], + "amount": 960150000000, + "locktime": 0, + "threshold": 1 + } + } + }, + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 1615248000, + "output": { + "addresses": [ + "P-avax1u6xzj47hp5g0atfx4haadhzp6jah52sngfgdvg" + ], + "amount": 960150000000, + "locktime": 0, + "threshold": 1 + } + } + }, + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 1623024000, + "output": { + "addresses": [ + "P-avax1u6xzj47hp5g0atfx4haadhzp6jah52sngfgdvg" + ], + "amount": 960150000000, + "locktime": 0, + "threshold": 1 + } + } + }, + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 1630800000, + "output": { + "addresses": [ + "P-avax1u6xzj47hp5g0atfx4haadhzp6jah52sngfgdvg" + ], + "amount": 960150000000, + "locktime": 0, + "threshold": 1 + } + } + }, + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 1638576000, + "output": { + "addresses": [ + "P-avax1u6xzj47hp5g0atfx4haadhzp6jah52sngfgdvg" + ], + "amount": 960150000000, + "locktime": 0, + "threshold": 1 + } + } + }, + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 1646352000, + "output": { + "addresses": [ + "P-avax1u6xzj47hp5g0atfx4haadhzp6jah52sngfgdvg" + ], + "amount": 960150000000, + "locktime": 0, + "threshold": 1 + } + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-avax1u6xzj47hp5g0atfx4haadhzp6jah52sngfgdvg" + ], + "locktime": 0, + "threshold": 1 + } + }, + "credentials": [ + { + "signatures": [ + "0x3d6a9819153edd90e2274f6f8461f007592c8dc4f62db0b47510370de2b917c96b3f9aabaeb3f26b5e79bad25f1352364440d17615e7176ad77b5d30209cd83600" + ] + }, + { + "signatures": [ + "0x3d6a9819153edd90e2274f6f8461f007592c8dc4f62db0b47510370de2b917c96b3f9aabaeb3f26b5e79bad25f1352364440d17615e7176ad77b5d30209cd83600" + ] + }, + { + "signatures": [ + "0x3d6a9819153edd90e2274f6f8461f007592c8dc4f62db0b47510370de2b917c96b3f9aabaeb3f26b5e79bad25f1352364440d17615e7176ad77b5d30209cd83600" + ] + }, + { + "signatures": [ + "0x3d6a9819153edd90e2274f6f8461f007592c8dc4f62db0b47510370de2b917c96b3f9aabaeb3f26b5e79bad25f1352364440d17615e7176ad77b5d30209cd83600" + ] + }, + { + "signatures": [ + "0x3d6a9819153edd90e2274f6f8461f007592c8dc4f62db0b47510370de2b917c96b3f9aabaeb3f26b5e79bad25f1352364440d17615e7176ad77b5d30209cd83600" + ] + }, + { + "signatures": [ + "0x3d6a9819153edd90e2274f6f8461f007592c8dc4f62db0b47510370de2b917c96b3f9aabaeb3f26b5e79bad25f1352364440d17615e7176ad77b5d30209cd83600" + ] + } + ] + } + ] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/211277.json b/server/service/backend/pchain/indexer/testdata/outs/211277.json new file mode 100644 index 0000000..9a8d819 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/211277.json @@ -0,0 +1,63 @@ +{ + "id": "arnyzsE9uXJcmFPLwULyYbjUraCvAWbJ3LC6HiJLTA2spD8Eg", + "type": "*block.ApricotStandardBlock", + "parent": "aA2ucjyrYR6QscaqANKUaUMuMmH4egHCqk987F96kxzSZsnv2", + "timestamp": 1599696000000, + "height": 211278, + "transactions": [ + { + "id": "GF5KEgiGs2yhDFTL1KgfmszU61U3ipSMpQJZsra5nFysots68", + "unsignedTx": { + "networkID": 1, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-avax146sem6srptk2cqpp4txy643qn37zjm3wpdvgd7" + ], + "amount": 479000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "inputs": [ + { + "txID": "29ejv3iGb7xiAiCiTyxqm5cSR81CkitmWttxddRcr5wnrC3uhW", + "outputIndex": 0, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 489000000, + "signatureIndices": [ + 0 + ] + } + } + ], + "memo": "0x", + "subnetID": "29ejv3iGb7xiAiCiTyxqm5cSR81CkitmWttxddRcr5wnrC3uhW", + "chainName": "Bthereum", + "vmID": "mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6", + "fxIDs": [], + "genesisData": "eyJjb25maWciOnsiY2hhaW5JZCI6NDMxMTQsImhvbWVzdGVhZEJsb2NrIjowLCJkYW9Gb3JrQmxvY2siOjAsImRhb0ZvcmtTdXBwb3J0Ijp0cnVlLCJlaXAxNTBCbG9jayI6MCwiZWlwMTUwSGFzaCI6IjB4MjA4Njc5OWFlZWJlYWUxMzVjMjQ2YzY1MDIxYzgyYjRlMTVhMmM0NTEzNDA5OTNhYWNmZDI3NTE4ODY1MTRmMCIsImVpcDE1NUJsb2NrIjowLCJlaXAxNThCbG9jayI6MCwiYnl6YW50aXVtQmxvY2siOjAsImNvbnN0YW50aW5vcGxlQmxvY2siOjAsInBldGVyc2J1cmdCbG9jayI6MCwiaXN0YW5idWxCbG9jayI6MCwibXVpckdsYWNpZXJCbG9jayI6MH0sIm5vbmNlIjoiMHgwIiwidGltZXN0YW1wIjoiMHgwIiwiZXh0cmFEYXRhIjoiMHgwMCIsImdhc0xpbWl0IjoiMHg1ZjVlMTAwIiwiZGlmZmljdWx0eSI6IjB4MCIsIm1peEhhc2giOiIweDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJjb2luYmFzZSI6IjB4MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsImFsbG9jIjp7IjAxMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiOnsiY29kZSI6IjB4NzMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMzAxNDYwODA2MDQwNTI2MDA0MzYxMDYwM2Q1NzYwMDAzNTYwZTAxYzgwNjMxZTAxMDQzOTE0NjA0MjU3ODA2M2I2NTEwYmIzMTQ2MDZlNTc1YjYwMDA4MGZkNWI2MDVjNjAwNDgwMzYwMzYwMjA4MTEwMTU2MDU2NTc2MDAwODBmZDViNTAzNTYwYjE1NjViNjA0MDgwNTE5MTgyNTI1MTkwODE5MDAzNjAyMDAxOTBmMzViODE4MDE1NjA3OTU3NjAwMDgwZmQ1YjUwNjBhZjYwMDQ4MDM2MDM2MDgwODExMDE1NjA4ZTU3NjAwMDgwZmQ1YjUwNjAwMTYwMDE2MGEwMWIwMzgxMzUxNjkwNjAyMDgxMDEzNTkwNjA0MDgxMDEzNTkwNjA2MDAxMzU2MGI2NTY1YjAwNWIzMGNkOTA1NjViODM2MDAxNjAwMTYwYTAxYjAzMTY4MTgzNjEwOGZjODY5MDgxMTUwMjkwNjA0MDUxNjAwMDYwNDA1MTgwODMwMzgxODg4ODg3OGM4YWNmOTU1MDUwNTA1MDUwNTAxNTgwMTU2MGY0NTczZDYwMDA4MDNlM2Q2MDAwZmQ1YjUwNTA1MDUwNTA1NmZlYTI2NDY5NzA2NjczNTgyMjEyMjAxZWViY2U5NzBmZTNmNWNiOTZiZjhhYzZiYTVmNWMxMzNmYzI5MDhhZTNkY2Q1MTA4MmNmZWU4ZjU4MzQyOWQwNjQ3MzZmNmM2MzQzMDAwNjBhMDAzMyIsImJhbGFuY2UiOiIweDAifSwiRkY3NjA1QUM0NDA1YzRFMzhBNzgxMzZDQjkyQjQyNjY4QTVDRDlmOCI6eyJiYWxhbmNlIjoiMHgyMWUxOWUwYzliYWIyNDAwMDAwIn19LCJudW1iZXIiOiIweDAiLCJnYXNVc2VkIjoiMHgwIiwicGFyZW50SGFzaCI6IjB4MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCJ9", + "subnetAuthorization": { + "signatureIndices": [] + } + }, + "credentials": [ + { + "signatures": [ + "0x6fcbc3d8a1444c9c6823f45aa4659dd35dc3a3ff1319294b53b7e41e9acd603c5df34f9af6830a1bc2ebc69135c11b18005726b67da396cebf8eefa9b5a57d6601" + ] + }, + { + "signatures": [] + } + ] + } + ] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/211333.json b/server/service/backend/pchain/indexer/testdata/outs/211333.json new file mode 100644 index 0000000..1711296 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/211333.json @@ -0,0 +1,69 @@ +{ + "id": "VjXMGrfyxyLZ4GGs3VC5H6mzteQtGVLZpkUs1dwFQnKJRawGt", + "type": "*block.ApricotProposalBlock", + "parent": "2JxSSBZAQLj8M6oBdUp129J6KSRDFcsyT6GP5k741qNJx6GDNd", + "timestamp": 1599696000000, + "height": 211334, + "transactions": [ + { + "id": "2NSNvNtwNLD8hQoeDwkJf126dBj9RdUStodpLuZoWsM8YnV6F", + "unsignedTx": { + "networkID": 1, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-avax146sem6srptk2cqpp4txy643qn37zjm3wpdvgd7" + ], + "amount": 458000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "inputs": [ + { + "txID": "29ZujU1g8h2qc2JgQjFo7Wby3F2hJhVGohkbeQ9iZ8LjMy2fmZ", + "outputIndex": 0, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 459000000, + "signatureIndices": [ + 0 + ] + } + } + ], + "memo": "0x", + "validator": { + "nodeID": "NodeID-AcjjxenGBACbhPfxzZYbbudKPq2K5XqGh", + "start": 1603645071, + "end": 1605373011, + "weight": 20000, + "subnetID": "jwz8ycZ8V2yHH5b1ysqtZvxtB5UCq34VoHx6C6uy9Gs4GeQ5t" + }, + "subnetAuthorization": { + "signatureIndices": [ + 0 + ] + } + }, + "credentials": [ + { + "signatures": [ + "0xd9b37e7563a439a7ab312868cc41df518aaa22b520895a0cf80589530efeaaed6abe8e0cef9995b840358e765dcbc2245c5238d0dc4efee04e32031ea63af83100" + ] + }, + { + "signatures": [ + "0xd9b37e7563a439a7ab312868cc41df518aaa22b520895a0cf80589530efeaaed6abe8e0cef9995b840358e765dcbc2245c5238d0dc4efee04e32031ea63af83100" + ] + } + ] + } + ] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/382.json b/server/service/backend/pchain/indexer/testdata/outs/382.json new file mode 100644 index 0000000..58cfce3 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/382.json @@ -0,0 +1,66 @@ +{ + "id": "2BZD8LSEnArSNwiE74usRGKihA6yDnLwZdGXe1yEfMxwpBFdZB", + "type": "*block.ApricotProposalBlock", + "parent": "BrPKVKyqobPfk7hZDFeMy4dpi7KFCFonTjgYN1LT5DA7AFaTN", + "timestamp": 1599696000000, + "height": 383, + "transactions": [ + { + "id": "xFJas2LiFdh3mazdL9Nuv8WcMn4TRsnSD6BauWhNBNduvURVg", + "unsignedTx": { + "networkID": 1, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [ + { + "txID": "2JSCbhgKWP7wa6imjfiH2r22djBYbJPy3xvr3qdfDMsRD1CQyy", + "outputIndex": 0, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 189998000000, + "signatureIndices": [ + 0 + ] + } + } + ], + "memo": "0x00000000", + "validator": { + "nodeID": "NodeID-A6onFGyJjA37EZ7kYHANMR1PFRT8NmXrF", + "start": 1600744921, + "end": 1607469481, + "weight": 189998000000 + }, + "stake": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-avax1hjlhzxzszs8nxhz0flnra38jxtvmf38td3qxa6" + ], + "amount": 189998000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-avax1hjlhzxzszs8nxhz0flnra38jxtvmf38td3qxa6" + ], + "locktime": 0, + "threshold": 1 + } + }, + "credentials": [ + { + "signatures": [ + "0x9be8a18e6ffda94d2bc80b82e4e8afa62240883b306aa658075474cd8d4bdc0e2baee95806bd079dad268db14c43ed57f3d3e6680054f403f5238d314e275cae01" + ] + } + ] + } + ] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/48.json b/server/service/backend/pchain/indexer/testdata/outs/48.json new file mode 100644 index 0000000..e86a117 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/48.json @@ -0,0 +1,8 @@ +{ + "id": "2UqBDUJMHRGPxS78watLZqpqFaCtM3f5SuoxPjfSbNGATk3j66", + "type": "*block.ApricotAbortBlock", + "parent": "2nxpSHKxf7PoKRvWgXD9k6uqDfHeEaK5uS6x7eDBFJy6Kt6ic6", + "timestamp": 1599696000000, + "height": 49, + "transactions": [] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/5981.json b/server/service/backend/pchain/indexer/testdata/outs/5981.json new file mode 100644 index 0000000..e2bf4e3 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/5981.json @@ -0,0 +1,77 @@ +{ + "id": "2jhYxP7xDVi3yb7o4WHxPNKxuCHVBqhsMwf6DqsNG452sDDF1g", + "type": "*block.ApricotStandardBlock", + "parent": "22h1eU6hUa2hubPAG26ix5zkE6SB7nyFjrv9y6i7tMgicD6jjr", + "timestamp": 1599696000000, + "height": 5982, + "transactions": [ + { + "id": "UC4CosZv99LqYRYSCCu9yX5NCGoLjVE2aP6pSwNVsiX48Lxhc", + "unsignedTx": { + "networkID": 1, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-avax13gfmq9cuw5ph2ce67fusfd6gtp0awln7px5mh7" + ], + "amount": 1040000, + "locktime": 0, + "threshold": 1 + } + } + ], + "inputs": [ + { + "txID": "DkZi7z2Psit7eMu3ENoBkfUAeWsUinjqHbqGYBYrbfDkh1zaX", + "outputIndex": 0, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 9040000, + "signatureIndices": [ + 0 + ] + } + }, + { + "txID": "cEyDkMHU1CsKqLkgEWZ2anmFGQi4iuBPx1jVq4WDZTikaSxEn", + "outputIndex": 0, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 2000000, + "signatureIndices": [ + 0 + ] + } + } + ], + "memo": "0x", + "owner": { + "addresses": [ + "P-avax1fuuttly42kw7hdnlwc5sr2mqeajvcf7ejmscyr", + "P-avax13gfmq9cuw5ph2ce67fusfd6gtp0awln7px5mh7" + ], + "locktime": 0, + "threshold": 2 + } + }, + "credentials": [ + { + "signatures": [ + "0x85bccd2f24966d552835b867cfd6132a405af52a2a3e0fd6510c4c187f5de8b962b20ec04bd5bc0eabff875653d5d7029dd020b2642363b4c14001bba06155e001" + ] + }, + { + "signatures": [ + "0xe78c348f2bb1098b52c664a1f2c25831e58d0f9e40bbf7315c93ef44fd7b083325d638dfd878d835a7cf2085050f6c5ab767c045b9ffd78e40eb4f05f6db477a00" + ] + } + ] + } + ] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/8.json b/server/service/backend/pchain/indexer/testdata/outs/8.json new file mode 100644 index 0000000..5adf08b --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/8.json @@ -0,0 +1,54 @@ +{ + "id": "2vxS14bQvrcd4JJ1XALpTJE2NGWGAWxsHJexuA5PM4LD4XSi6B", + "type": "*block.ApricotAtomicBlock", + "parent": "2rjnkz8QvbNM83z7tnCSRZ5NuosUFFhz628ex4MKHrxvyyk5Wc", + "timestamp": 1599696000000, + "height": 9, + "transactions": [ + { + "id": "2B2MQhogRmFqVWnK7y1qHk5shobzHgjcZMvsbntwsyvtyrFshr", + "unsignedTx": { + "networkID": 1, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-avax1xz2tjpm6cphf3g56vvlzz6rl7h0h250f7qmvxq" + ], + "amount": 4999998000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "inputs": [], + "memo": "0x00000000", + "sourceChain": "2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM", + "importedInputs": [ + { + "txID": "2EAyxEvLqWDLcj2q2Vs5y7HDXNsrCGbxGwwUb5CePzbEVd9JoV", + "outputIndex": 0, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 4999999000000, + "signatureIndices": [ + 0 + ] + } + } + ] + }, + "credentials": [ + { + "signatures": [ + "0x0a23fd1e5e4a921bc8e9eb1c1af702b63a500717500ee0476ec40b5c501072ca39cdcf61e8a60cf07d13a5180baf9962df0f5e0afb71af85ef10faeaff96591701" + ] + } + ] + } + ] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/806002.json b/server/service/backend/pchain/indexer/testdata/outs/806002.json new file mode 100644 index 0000000..732bf2c --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/806002.json @@ -0,0 +1,84 @@ +{ + "id": "2uJMNgW7j7T2Gwp98aHQTgMY47aYC8GZ2hmEGR92k6wM4UCxbC", + "type": "*block.ApricotAtomicBlock", + "parent": "pgCNn9vUpsy4mkY4i3tnAsgmBsYALw92YsXyXxtPph4QPyUtV", + "timestamp": 1632356898000, + "height": 806003, + "transactions": [ + { + "id": "9Rg7jCYZCAyFHaYmvYNZBNbTZyoTS9dikV9sL625vrsoM7dSU", + "unsignedTx": { + "networkID": 1, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-avax1x4t86fpe0e2vdame0jfzsc6gh53npksev5kdl6" + ], + "amount": 61277597674, + "locktime": 0, + "threshold": 1 + } + } + ], + "inputs": [ + { + "txID": "UwJboUAnW3fUctUpgZL9QJPqAS5anpTdyXGagkRYFBYtQmv1s", + "outputIndex": 0, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 111736113231, + "signatureIndices": [ + 0 + ] + } + }, + { + "txID": "UwJboUAnW3fUctUpgZL9QJPqAS5anpTdyXGagkRYFBYtQmv1s", + "outputIndex": 1, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 543484443, + "signatureIndices": [ + 0 + ] + } + } + ], + "memo": "0x", + "destinationChain": "2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM", + "exportedOutputs": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-avax1x4t86fpe0e2vdame0jfzsc6gh53npksev5kdl6" + ], + "amount": 51001000000, + "locktime": 0, + "threshold": 1 + } + } + ] + }, + "credentials": [ + { + "signatures": [ + "0x372fac44a7523902e1d2e5dd2f057145ff12f4c416ffc16241cea6ad97fba12719d614d4e92a2446a820c020c76458f8636e97a155ee52d34f20ac2d97d9d66400" + ] + }, + { + "signatures": [ + "0x372fac44a7523902e1d2e5dd2f057145ff12f4c416ffc16241cea6ad97fba12719d614d4e92a2446a820c020c76458f8636e97a155ee52d34f20ac2d97d9d66400" + ] + } + ] + } + ] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/810424.json b/server/service/backend/pchain/indexer/testdata/outs/810424.json new file mode 100644 index 0000000..e91d411 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/810424.json @@ -0,0 +1,54 @@ +{ + "id": "28j2iZ1ASML6PsG53VSjLYC4CCA7xfo4XHAqVbTFrPt9AFb8Me", + "type": "*block.ApricotAtomicBlock", + "parent": "2CAmmeHJiFxH24BE98SxJKXsg3RMZ2Po6L55goLQU8bxG7Fepi", + "timestamp": 1632508114000, + "height": 810425, + "transactions": [ + { + "id": "jEjMoaKpfPBAU44sXMcbvPaSF5Ecihhkye8RSBwzzs1CB6LLg", + "unsignedTx": { + "networkID": 1, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-avax19p3av256r6dajwqljq2ahu7tk4ly97jky09msa" + ], + "amount": 10510000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "inputs": [], + "memo": "0x", + "sourceChain": "2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM", + "importedInputs": [ + { + "txID": "2EWVsAm1REH44ejCHBmVLHfN78oJuSw3wWyXLVwxf7jHxuJaJh", + "outputIndex": 1, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 10511000000, + "signatureIndices": [ + 0 + ] + } + } + ] + }, + "credentials": [ + { + "signatures": [ + "0xa05b0aa6d9acdb1c088f7221f122402d84c00d3e7053da1ab263ffe4dce029bd150539db491be27543f36d1a333da5cfe595dc6683b6dd2b7512939f9558959300" + ] + } + ] + } + ] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/911.json b/server/service/backend/pchain/indexer/testdata/outs/911.json new file mode 100644 index 0000000..52edc5a --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/911.json @@ -0,0 +1,54 @@ +{ + "id": "2PpHLb6QA8grZaGP9nmQmADjXBetSYyjPRmR9eqoSfo4yinxq", + "type": "*block.ApricotAtomicBlock", + "parent": "WbVcJjrYR7SMHeymedravjJMi7SgWMtP4HZiVKcwTS8DuvZAX", + "timestamp": 1599696000000, + "height": 912, + "transactions": [ + { + "id": "ePJ2GEN3gBUquRUhBkehfUS1cWBz1SZNKuYPjG5Ktpjfmpef", + "unsignedTx": { + "networkID": 1, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [ + { + "txID": "kxAd41CPYQJ4nC5yYd99iHsGuce2Q5A2ZebpZwyHBkcEiKYRz", + "outputIndex": 0, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 2998000000, + "signatureIndices": [ + 0 + ] + } + } + ], + "memo": "0x00000000", + "destinationChain": "2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM", + "exportedOutputs": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-avax1da8qpayhzshr73lf94rjxncf4ve7nn4570fakq" + ], + "amount": 2997000000, + "locktime": 0, + "threshold": 1 + } + } + ] + }, + "credentials": [ + { + "signatures": [ + "0x9834c0af0dd92c0aa2febdc7e7b147d7b4bc46825cf8bd1a6df306bc7e795f250a05555a0e7776373259fc22ec3f514e2ceed50a8eb6a428b0b284b602fc030301" + ] + } + ] + } + ] +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/genesis.json b/server/service/backend/pchain/indexer/testdata/outs/genesis.json new file mode 100644 index 0000000..a5b6000 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/genesis.json @@ -0,0 +1,56 @@ +{ + "id": "2FUFPVPxbTpKNn39moGSzsmGroYES4NZRdw3mJgNvMkMiMHJ9e", + "type": "GenesisBlock", + "parent": "UUvXi6j7QhVvgpbKM89MP5HdrxKm9CaJeHc187TsDNf8nZdLk", + "timestamp": 1599696000000, + "height": 0, + "transactions": [ + { + "id":"2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM", + "unsignedTx": { + "networkID": 1, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "subnetID": "11111111111111111111111111111111LpoYY", + "chainName": "X-Chain", + "vmID": "jvYyfQTxGMJLuGWa55kdP2p2zSUYsQ5Raupu4TW34ZAUBAbtq", + "fxIDs": [ + "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "qd2U4HDWUvMrVUeTcCHp6xH3Qpnn1XbU5MDdnBoiifFqvgXwT", + "rXJsCSEYXg2TehWxCEEGj6JU2PWKTkd6cBdNLjoe2SpsKD9cy" + ], + "genesisData": "", + "subnetAuthorization": { + "signatureIndices": [] + } + }, + "credentials": [] + }, + { + "id": "2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5", + "unsignedTx": { + "networkID": 1, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "subnetID": "11111111111111111111111111111111LpoYY", + "chainName": "C-Chain", + "vmID": "mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6", + "fxIDs": [], + "genesisData": "", + "subnetAuthorization": { + "signatureIndices": [] + } + }, + "credentials": [] + } + ], + "data": { + "message": "From Snowflake to Avalanche. Per consensum ad astra.", + "initialSupply": 359999999999990210, + "utxos": [] + } +} diff --git a/server/service/backend/pchain/indexer/testdata/outs/genesis_fuji_txs.json b/server/service/backend/pchain/indexer/testdata/outs/genesis_fuji_txs.json new file mode 100644 index 0000000..cb40b84 --- /dev/null +++ b/server/service/backend/pchain/indexer/testdata/outs/genesis_fuji_txs.json @@ -0,0 +1,836 @@ +{ + "id": "99BWrAqUMvTp9nXKXyjPsCqjGwDqVFqssTRQbu58af57Cf9VG", + "type": "GenesisBlock", + "parent": "MSj6o9TpezwsQx4Tv7SHqpVvCbJ8of1ikjsqPZ1bKRjc9zBy3", + "timestamp": 1599696000000, + "height": 0, + "transactions": [ + { + "id": "TdoZgvfDCs42jxihsb1coBY7rrhibDj56RH3UDFzkzFFQyGQo", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-4CWTbdvgXHY1CLXqQNAp22nJDo5nAmts6", + "start": 1599696000, + "end": 1630206000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 20000 + }, + "credentials": [] + }, + { + "id": "2L1UKtRr61kpYUvGymnvjRhHdiASityfc3S1jtoG5n7Uc4sgm4", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-JjvzhxnLHLUQ5HjVRkvG827ivbLXPwA9u", + "start": 1599696000, + "end": 1630260000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 20000 + }, + "credentials": [] + }, + { + "id": "2RcmJyJFYNPSKPEoGMLzKJkeJrWxbQwgseU57fWWz2deRGA5nc", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-4B4rc5vdD1758JSBYL1xyvE5NHGzz6xzH", + "start": 1599696000, + "end": 1630530000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 20000 + }, + "credentials": [] + }, + { + "id": "2eGGdfQNMa74nAfL2oP595suTuotfnbn76q9gCiysZMu2MvMfA", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-CTtkcXvVdhpNp6f97LEUXPwsRD3A2ZHqP", + "start": 1599696000, + "end": 1630368000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 20000 + }, + "credentials": [] + }, + { + "id": "xTNV2MGT6UwRdE9e55jEjk1QNVCaujNA3ftyptvcGgDrs3bQa", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-84KbQHSDnojroCVY7vQ7u9Tx7pUonPaS", + "start": 1599696000, + "end": 1630314000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 20000 + }, + "credentials": [] + }, + { + "id": "2efFvnZQPNJcvFaor6JSawVX7679xgXFHRFTsruGY3rDPXazW7", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-LegbVf6qaMKcsXPnLStkdc1JVktmmiDxy", + "start": 1599696000, + "end": 1630692000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 20000 + }, + "credentials": [] + }, + { + "id": "NR8h3zYezKrfCW6ykYkYo9bHk6X6NtcUtaE8ZKVayDfUCCXFZ", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-BFa1padLXBj7VHa2JYvYGzcTBPQGjPhUy", + "start": 1599696000, + "end": 1630584000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 20000 + }, + "credentials": [] + }, + { + "id": "gMf2xGJqueY5seAGHNs8sWFCDDe2oS4GGDEczEBPY27HYDMYF", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-CYKruAjwH1BmV3m37sXNuprbr7dGQuJwG", + "start": 1599696000, + "end": 1630746000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 20000 + }, + "credentials": [] + }, + { + "id": "xv3yCa3nkRcSk253mtEonhDkgxQqQ5CeevCJXGt9cNY9bL5yV", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-CZmZ9xpCzkWqjAyS7L4htzh5Lg6kf1k18", + "start": 1599696000, + "end": 1630422000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 20000 + }, + "credentials": [] + }, + { + "id": "2Hsyxia2a1QrMjpVhFort3hWNzCjZwXkRu14PbmkfCzJjj1NWP", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-EzGaipqomyK9UKx9DBHV6Ky3y68hoknrF", + "start": 1599696000, + "end": 1630800000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 20000 + }, + "credentials": [] + }, + { + "id": "KfBfLm8DaErS7rJgdnobgSeeQM9CQnRj5YwbtP6rpMs4pvUJp", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-JyE4P8f4cTryNV8DCz2M81bMtGhFFHexG", + "start": 1599696000, + "end": 1630854000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 20000 + }, + "credentials": [] + }, + { + "id": "Uumyppd474jxw2KBt8RyXdQUGw1MKESJa19C2Aiir3ihYge5S", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-2m38qc95mhHXtrhjyGbe7r2NhniqHHJRB", + "start": 1599696000, + "end": 1631178000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 500000 + }, + "credentials": [] + }, + { + "id": "uSZ55F395FDepT7aY7pwUx9ePD8hjLdhUnC6frmsdPAmMqK1N", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-HGZ8ae74J3odT8ESreAdCtdnvWG1J4X5n", + "start": 1599696000, + "end": 1630962000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 31250 + }, + "credentials": [] + }, + { + "id": "w6HpCJk38zwZvq5khyU2QmF83qL9gBTB3ggumVqfBJsWUa4jm", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-4QBwET5o8kUhvt9xArhir4d3R25CtmZho", + "start": 1599696000, + "end": 1631016000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 62500 + }, + "credentials": [] + }, + { + "id": "4qzP7ByUFWsjVAeVFzbBioLLuFyAAWCToGRfj5uomAbqpYGF7", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-FesGqwKq7z5nPFHa5iwZctHE5EZV9Lpdq", + "start": 1599696000, + "end": 1630638000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 20000 + }, + "credentials": [] + }, + { + "id": "QPaQMmD4xxCDDWBs9CjNMc1XvtfPwTMiKfKCNv2AP5xddbA3z", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-NpagUxt6KQiwPch9Sd4osv8kD1TZnkjdk", + "start": 1599696000, + "end": 1631232000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 1000000 + }, + "credentials": [] + }, + { + "id": "2hr6QaBeVqQfPYKYZvgb7Y2fzsUKUnkJNZ4UKYsa753jeuLVbG", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-4KXitMCoE9p2BHA6VzXtaTxLoEjNDo2Pt", + "start": 1599696000, + "end": 1630908000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 20000 + }, + "credentials": [] + }, + { + "id": "2qzRvGxRSMpxWozdhgtpSQu9EBUG5jWqCULx9oh6EiQ6WaoPyd", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-hArafGhY2HFTbwaaVh1CSCUCUCiJ2Vfb", + "start": 1599696000, + "end": 1631070000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 125000 + }, + "credentials": [] + }, + { + "id": "27G2D9fvAMMvRAytKyWNgfXh77r4oySK9Hp3R9NFrintntdQF9", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-EDESh4DfZFC15i613pMtWniQ9arbBZRnL", + "start": 1599696000, + "end": 1630476000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 20000 + }, + "credentials": [] + }, + { + "id": "25yDypVdxt6qHs95TWpSLQLC7B4XKLgt8JCpx9d4FeVSfMW5fp", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "validator": { + "nodeID": "NodeID-LQwRLm4cbJ7T2kxcxp4uXCU5XD8DFrE1C", + "start": 1599696000, + "end": 1631124000, + "weight": 2000000000000000 + }, + "stake": [ + { + "assetID": "U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "amount": 2000000000000000, + "locktime": 0, + "threshold": 1 + } + } + ], + "rewardsOwner": { + "addresses": [ + "P-fuji1wycv8n7d2fg9aq6unp23pnj4q0arv03ysya8jw" + ], + "locktime": 0, + "threshold": 1 + }, + "shares": 250000 + }, + "credentials": [] + }, + { + "id": "2JVSBoinj9C2J33VntvzYtVJNZdN2NKiwwKjcumHUWEb5DbBrm", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "subnetID": "11111111111111111111111111111111LpoYY", + "chainName": "X-Chain", + "vmID": "jvYyfQTxGMJLuGWa55kdP2p2zSUYsQ5Raupu4TW34ZAUBAbtq", + "fxIDs": [ + "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "qd2U4HDWUvMrVUeTcCHp6xH3Qpnn1XbU5MDdnBoiifFqvgXwT", + "rXJsCSEYXg2TehWxCEEGj6JU2PWKTkd6cBdNLjoe2SpsKD9cy" + ], + "genesisData": "AAAAAAABAARBVkFYAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADIs9grE2fTYt6Zq1mmWBZa/1IMvU2z2CsTZ9Ni3pmrWaZYFlr/Ugy9TbPYKxNn02LematZplgWWv9SDL1Ns9grE2fTYt6Zq1mmWBZa/1IMvU2z2CsTZ9Ni3pmrWaZYFlr/Ugy9TbPYKxNn02LematZplgWWv9SDL1Ns9grE2fTYt6Zq1mmWBZa/1IMvU2z2CsTZ9Ni3pmrWaZYFlr/Ugy9TbPYKxNn02LematZplgWWv9SDL1Ns9grE2fTYt6Zq1mmWBZa/1IMvU0ACUF2YWxhbmNoZQAEQVZBWAkAAAABAAAAAAAAAAoAAAAHAHGv1JjQAAAAAAAAAAAAAAAAAAEAAAABAPcvcKEHCEjvtxGyB85/nJXcZy0AAAAHAHGv1JjQAAAAAAAAAAAAAAAAAAEAAAABE7L7Guj0TRzdC7WEqti4F9KBLxUAAAAHAHGv1JjQAAAAAAAAAAAAAAAAAAEAAAABMHZn/kMLAZMNg5K0maEMG/piu3EAAAAHAHGv1JjQAAAAAAAAAAAAAAAAAAEAAAABOhW/VPG2oOzXWAp2AulvLAOohQwAAAAHAHGv1JjQAAAAAAAAAAAAAAAAAAEAAAABP/syaTL4jY8zgEtKatiuAF5L2f4AAAAHAHGv1JjQAAAAAAAAAAAAAAAAAAEAAAABP/syaTL4jY8zgEtKatiuAF5L2f4AAAAHAHGv1JjQAAAAAAAAAAAAAAAAAAEAAAABSDEK7KE6Gh8jFrRfM/NHbSkrPu0AAAAHAHGv1JjQAAAAAAAAAAAAAAAAAAEAAAABcNhVARQ0WwANfLNwcBphByC4gb0AAAAHAHGv1JjQAAAAAAAAAAAAAAAAAAEAAAABe1SQST+KL/9ESsi1TiezM518YNwAAAAHAHGv1JjQAAAAAAAAAAAAAAAAAAEAAAABx0MR8Ui+IyW2fCZJ6Jx8PymbN60=", + "subnetAuthorization": { + "signatureIndices": [] + } + }, + "credentials": [] + }, + { + "id": "yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp", + "unsignedTx": { + "networkID": 5, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [], + "inputs": [], + "memo": "0x", + "subnetID": "11111111111111111111111111111111LpoYY", + "chainName": "C-Chain", + "vmID": "mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6", + "fxIDs": [], + "genesisData": "eyJjb25maWciOnsiY2hhaW5JZCI6NDMxMTMsImhvbWVzdGVhZEJsb2NrIjowLCJkYW9Gb3JrQmxvY2siOjAsImRhb0ZvcmtTdXBwb3J0Ijp0cnVlLCJlaXAxNTBCbG9jayI6MCwiZWlwMTUwSGFzaCI6IjB4MjA4Njc5OWFlZWJlYWUxMzVjMjQ2YzY1MDIxYzgyYjRlMTVhMmM0NTEzNDA5OTNhYWNmZDI3NTE4ODY1MTRmMCIsImVpcDE1NUJsb2NrIjowLCJlaXAxNThCbG9jayI6MCwiYnl6YW50aXVtQmxvY2siOjAsImNvbnN0YW50aW5vcGxlQmxvY2siOjAsInBldGVyc2J1cmdCbG9jayI6MCwiaXN0YW5idWxCbG9jayI6MCwibXVpckdsYWNpZXJCbG9jayI6MH0sIm5vbmNlIjoiMHgwIiwidGltZXN0YW1wIjoiMHgwIiwiZXh0cmFEYXRhIjoiMHgwMCIsImdhc0xpbWl0IjoiMHg1ZjVlMTAwIiwiZGlmZmljdWx0eSI6IjB4MCIsIm1peEhhc2giOiIweDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJjb2luYmFzZSI6IjB4MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsImFsbG9jIjp7IjAxMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiOnsiY29kZSI6IjB4NzMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMzAxNDYwODA2MDQwNTI2MDA0MzYxMDYwM2Q1NzYwMDAzNTYwZTAxYzgwNjMxZTAxMDQzOTE0NjA0MjU3ODA2M2I2NTEwYmIzMTQ2MDZlNTc1YjYwMDA4MGZkNWI2MDVjNjAwNDgwMzYwMzYwMjA4MTEwMTU2MDU2NTc2MDAwODBmZDViNTAzNTYwYjE1NjViNjA0MDgwNTE5MTgyNTI1MTkwODE5MDAzNjAyMDAxOTBmMzViODE4MDE1NjA3OTU3NjAwMDgwZmQ1YjUwNjBhZjYwMDQ4MDM2MDM2MDgwODExMDE1NjA4ZTU3NjAwMDgwZmQ1YjUwNjAwMTYwMDE2MGEwMWIwMzgxMzUxNjkwNjAyMDgxMDEzNTkwNjA0MDgxMDEzNTkwNjA2MDAxMzU2MGI2NTY1YjAwNWIzMGNkOTA1NjViODM2MDAxNjAwMTYwYTAxYjAzMTY4MTgzNjEwOGZjODY5MDgxMTUwMjkwNjA0MDUxNjAwMDYwNDA1MTgwODMwMzgxODg4ODg3OGM4YWNmOTU1MDUwNTA1MDUwNTAxNTgwMTU2MGY0NTczZDYwMDA4MDNlM2Q2MDAwZmQ1YjUwNTA1MDUwNTA1NmZlYTI2NDY5NzA2NjczNTgyMjEyMjAxZWViY2U5NzBmZTNmNWNiOTZiZjhhYzZiYTVmNWMxMzNmYzI5MDhhZTNkY2Q1MTA4MmNmZWU4ZjU4MzQyOWQwNjQ3MzZmNmM2MzQzMDAwNjBhMDAzMyIsImJhbGFuY2UiOiIweDAifX0sIm51bWJlciI6IjB4MCIsImdhc1VzZWQiOiIweDAiLCJwYXJlbnRIYXNoIjoiMHgwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIn0=", + "subnetAuthorization": { + "signatureIndices": [] + } + }, + "credentials": [] + } + ], + "data": { + "message": "hi mom", + "initialSupply": 360000000000000000, + "utxos": [] + } +} diff --git a/server/service/backend/pchain/indexer/types.go b/server/service/backend/pchain/indexer/types.go new file mode 100644 index 0000000..5ecdff8 --- /dev/null +++ b/server/service/backend/pchain/indexer/types.go @@ -0,0 +1,30 @@ +package indexer + +import ( + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/platformvm/genesis" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" +) + +// ParsedBlock contains block details parsed from indexer containers +type ParsedBlock struct { + BlockID ids.ID `json:"id"` + BlockType string `json:"type"` + ParentID ids.ID `json:"parent"` + Timestamp int64 `json:"timestamp"` + Height uint64 `json:"height"` + Txs []*txs.Tx `json:"transactions"` +} + +// GenesisBlockData contains Genesis state details +type GenesisBlockData struct { + Message string `json:"message"` + InitialSupply uint64 `json:"initialSupply"` + UTXOs []*genesis.UTXO `json:"utxos"` +} + +// ParsedGenesisBlock contains Genesis state details +type ParsedGenesisBlock struct { + ParsedBlock + GenesisBlockData `json:"data"` +} diff --git a/server/service/backend/pchain/network.go b/server/service/backend/pchain/network.go new file mode 100644 index 0000000..ba68dbb --- /dev/null +++ b/server/service/backend/pchain/network.go @@ -0,0 +1,81 @@ +package pchain + +import ( + "context" + + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/mapper" + "github.com/ava-labs/avalanche-rosetta/service" + + pmapper "github.com/ava-labs/avalanche-rosetta/mapper/pchain" +) + +// NetworkIdentifier returns P-chain network identifier +// used by /network/list endpoint to list available networks +func (b *Backend) NetworkIdentifier() *types.NetworkIdentifier { + return b.networkID +} + +// NetworkStatus implements /network/status endpoint for P-chain +func (b *Backend) NetworkStatus(ctx context.Context, _ *types.NetworkRequest) (*types.NetworkStatusResponse, *types.Error) { + // Fetch peers + infoPeers, err := b.pClient.Peers(ctx) + if err != nil { + return nil, service.WrapError(service.ErrClientError, err) + } + peers := mapper.Peers(infoPeers) + + // Check if network is bootstrapped + ready, err := b.pClient.IsBootstrapped(ctx, constants.PChain.String()) + if err != nil { + return nil, service.WrapError(service.ErrClientError, err) + } + + if !ready { + genesisBlock := b.getGenesisBlock() + return &types.NetworkStatusResponse{ + CurrentBlockIdentifier: b.getGenesisIdentifier(), + CurrentBlockTimestamp: genesisBlock.Timestamp, + GenesisBlockIdentifier: b.getGenesisIdentifier(), + SyncStatus: mapper.StageBootstrap, + Peers: peers, + }, nil + } + + // Current block height + currentBlock, err := b.indexerParser.ParseCurrentBlock(ctx) + if err != nil { + return nil, service.WrapError(service.ErrClientError, err) + } + + return &types.NetworkStatusResponse{ + CurrentBlockIdentifier: &types.BlockIdentifier{ + Index: int64(currentBlock.Height), + Hash: currentBlock.BlockID.String(), + }, + CurrentBlockTimestamp: currentBlock.Timestamp, + GenesisBlockIdentifier: b.getGenesisIdentifier(), + SyncStatus: mapper.StageSynced, + Peers: peers, + }, nil +} + +// NetworkOptions implements /network/options endpoint for P-chain +func (*Backend) NetworkOptions(_ context.Context, _ *types.NetworkRequest) (*types.NetworkOptionsResponse, *types.Error) { + return &types.NetworkOptionsResponse{ + Version: &types.Version{ + RosettaVersion: types.RosettaAPIVersion, + NodeVersion: service.NodeVersion, + MiddlewareVersion: types.String(service.MiddlewareVersion), + }, + Allow: &types.Allow{ + OperationStatuses: mapper.OperationStatuses, + OperationTypes: pmapper.OperationTypes, + CallMethods: pmapper.CallMethods, + Errors: service.Errors, + HistoricalBalanceLookup: false, + }, + }, nil +} diff --git a/server/service/backend/pchain/types.go b/server/service/backend/pchain/types.go new file mode 100644 index 0000000..951d905 --- /dev/null +++ b/server/service/backend/pchain/types.go @@ -0,0 +1,137 @@ +package pchain + +import ( + "errors" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/hashing" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/ava-labs/avalanche-rosetta/constants" + "github.com/ava-labs/avalanche-rosetta/mapper" + "github.com/ava-labs/avalanche-rosetta/service" + "github.com/ava-labs/avalanche-rosetta/service/backend/common" + + pmapper "github.com/ava-labs/avalanche-rosetta/mapper/pchain" +) + +var ( + _ common.AvaxTx = &pTx{} + _ common.TxBuilder = &pTxBuilder{} + _ common.TxParser = &pTxParser{} + + errInvalidTransaction = errors.New("invalid transaction") +) + +// AccountBalance contains P-chain account balances +type AccountBalance struct { + Total uint64 + Unlocked uint64 + Staked uint64 + LockedStakeable uint64 + LockedNotStakeable uint64 +} + +type pTx struct { + Tx *txs.Tx + Codec codec.Manager + CodecVersion uint16 +} + +func (p *pTx) Initialize() error { + if p.Tx == nil { + return common.ErrNoTxGiven + } + return p.Tx.Sign(p.Codec, nil) +} + +func (p *pTx) Marshal() ([]byte, error) { + return p.Codec.Marshal(p.CodecVersion, p.Tx) +} + +func (p *pTx) Unmarshal(bytes []byte) error { + tx := txs.Tx{} + _, err := p.Codec.Unmarshal(bytes, &tx) + if err != nil { + return err + } + if err := tx.Sign(p.Codec, nil); err != nil { + return err + } + p.Tx = &tx + + return p.Initialize() +} + +func (p *pTx) SigningPayload() []byte { + return hashing.ComputeHash256(p.Tx.Unsigned.Bytes()) +} + +func (p *pTx) Hash() ids.ID { + return p.Tx.ID() +} + +type pTxBuilder struct { + avaxAssetID ids.ID + codec codec.Manager + codecVersion uint16 +} + +func (p pTxBuilder) BuildTx(operations []*types.Operation, metadataMap map[string]interface{}) (common.AvaxTx, []*types.AccountIdentifier, *types.Error) { + var metadata pmapper.Metadata + err := mapper.UnmarshalJSONMap(metadataMap, &metadata) + if err != nil { + return nil, nil, service.WrapError(service.ErrInvalidInput, err) + } + + matches, err := common.MatchOperations(operations) + if err != nil { + return nil, nil, service.WrapError(service.ErrInvalidInput, err) + } + + opType := matches[0].Operations[0].Type + tx, signers, err := pmapper.BuildTx(opType, matches, metadata, p.codec, p.avaxAssetID) + if err != nil { + return nil, nil, service.WrapError(service.ErrInvalidInput, err) + } + + return &pTx{ + Tx: tx, + Codec: p.codec, + CodecVersion: p.codecVersion, + }, signers, nil +} + +type pTxParser struct { + hrp string + chainIDs map[ids.ID]constants.ChainIDAlias + avaxAssetID ids.ID +} + +func (p pTxParser) ParseTx(tx *common.RosettaTx, inputAddresses map[string]*types.AccountIdentifier) ([]*types.Operation, error) { + pTx, ok := tx.Tx.(*pTx) + if !ok { + return nil, errInvalidTransaction + } + + parserCfg := pmapper.TxParserConfig{ + IsConstruction: true, + Hrp: p.hrp, + ChainIDs: p.chainIDs, + AvaxAssetID: p.avaxAssetID, + PChainClient: nil, + } + parser, err := pmapper.NewTxParser(parserCfg, inputAddresses, nil) + if err != nil { + return nil, err + } + + transactions, err := parser.Parse(pTx.Tx) + if err != nil { + return nil, err + } + + return transactions.Operations, nil +} diff --git a/server/service/config.go b/server/service/config.go index 69bd80e..e70b76e 100644 --- a/server/service/config.go +++ b/server/service/config.go @@ -3,8 +3,10 @@ package service import ( "math/big" - ethtypes "github.com/ava-labs/coreth/core/types" + "github.com/ava-labs/coreth/params" "github.com/coinbase/rosetta-sdk-go/types" + + ethtypes "github.com/ava-labs/coreth/core/types" ) // Config holds the service configuration @@ -16,6 +18,7 @@ type Config struct { FlareAssetID string IngestionMode string TokenWhiteList []string + BridgeTokenList []string IndexUnknownTokens bool // Upgrade Times @@ -56,5 +59,16 @@ func (c Config) IsTokenListEmpty() bool { // Signer returns an eth signer object for a given chain func (c Config) Signer() ethtypes.Signer { + if c.ChainID != nil { + if c.ChainID.Cmp(params.AvalancheMainnetChainID) == 0 { + return ethtypes.LatestSigner(params.AvalancheMainnetChainConfig) + } + if c.ChainID.Cmp(params.AvalancheFujiChainID) == 0 { + return ethtypes.LatestSigner(params.AvalancheFujiChainConfig) + } + if c.ChainID.Cmp(params.AvalancheLocalChainID) == 0 { + return ethtypes.LatestSigner(params.AvalancheLocalChainConfig) + } + } return ethtypes.LatestSignerForChainID(c.ChainID) } diff --git a/server/service/config_test.go b/server/service/config_test.go index 386fa42..3d3ddb3 100644 --- a/server/service/config_test.go +++ b/server/service/config_test.go @@ -1,41 +1,42 @@ package service import ( - "math/big" "testing" - ethtypes "github.com/ava-labs/coreth/core/types" + "github.com/ava-labs/coreth/params" "github.com/coinbase/rosetta-sdk-go/types" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ethtypes "github.com/ava-labs/coreth/core/types" ) func TestConfig(t *testing.T) { t.Run("online", func(t *testing.T) { cfg := Config{ Mode: "online", - ChainID: big.NewInt(1), + ChainID: params.AvalancheMainnetChainID, NetworkID: &types.NetworkIdentifier{}, } - assert.Equal(t, false, cfg.IsOfflineMode()) - assert.Equal(t, true, cfg.IsOnlineMode()) + require.False(t, cfg.IsOfflineMode()) + require.True(t, cfg.IsOnlineMode()) }) t.Run("offline", func(t *testing.T) { cfg := Config{ Mode: "offline", - ChainID: big.NewInt(1), + ChainID: params.AvalancheMainnetChainID, NetworkID: &types.NetworkIdentifier{}, } - assert.Equal(t, true, cfg.IsOfflineMode()) - assert.Equal(t, false, cfg.IsOnlineMode()) + require.True(t, cfg.IsOfflineMode()) + require.False(t, cfg.IsOnlineMode()) }) t.Run("signer", func(t *testing.T) { cfg := Config{ - ChainID: big.NewInt(1), + ChainID: params.AvalancheMainnetChainID, } - assert.IsType(t, ethtypes.NewLondonSigner(big.NewInt(1)), cfg.Signer()) + require.IsType(t, ethtypes.NewLondonSigner(params.AvalancheMainnetChainID), cfg.Signer()) }) } diff --git a/server/service/contract_call_data.go b/server/service/contract_call_data.go index 891d4ff..a933b6f 100644 --- a/server/service/contract_call_data.go +++ b/server/service/contract_call_data.go @@ -12,7 +12,16 @@ import ( "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" + "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. @@ -70,13 +79,9 @@ func constructContractCallDataGeneric(methodSig string, methodArgs interface{}) // 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 @@ -86,11 +91,12 @@ func encodeMethodArgsStrings(methodID []byte, methodSig string, methodArgs []str 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{ @@ -101,7 +107,6 @@ func encodeMethodArgsStrings(methodID []byte, methodSig string, methodArgs []str arguments = append(arguments, argument...) var argData interface{} - const base = 10 switch { case v == "address": { @@ -109,7 +114,7 @@ func encodeMethodArgsStrings(methodID []byte, methodSig string, methodArgs []str } case v == "uint32": { - u64, err := strconv.ParseUint(methodArgs[i], 10, 32) + u64, err := strconv.ParseUint(methodArgs[i], base10, 32) if err != nil { log.Fatal(err) } @@ -118,7 +123,7 @@ func encodeMethodArgsStrings(methodID []byte, methodSig string, methodArgs []str case strings.HasPrefix(v, "uint") || strings.HasPrefix(v, "int"): { value := new(big.Int) - value.SetString(methodArgs[i], base) + value.SetString(methodArgs[i], base10) argData = value } case v == "bytes32": @@ -133,15 +138,11 @@ func encodeMethodArgsStrings(methodID []byte, methodSig string, methodArgs []str } 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 + argData = bytes } case strings.HasPrefix(v, "string"): { @@ -156,9 +157,8 @@ func encodeMethodArgsStrings(methodID []byte, methodSig string, methodArgs []str argData = value } default: - return nil, errors.New(fmt.Sprintf("invalid argument type:%s", v)) + return nil, fmt.Errorf("invalid argument type: %s", v) } - argumentsData = append(argumentsData, argData) } @@ -174,11 +174,8 @@ func encodeMethodArgsStrings(methodID []byte, methodSig string, methodArgs []str // 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 + if len(methodSig) < 4 { + return nil, errors.New("method signature is empty or too small") } - - return hash.Sum(nil)[:4], nil + return crypto.Keccak256([]byte(methodSig))[:4], nil } diff --git a/server/service/contract_call_data_test.go b/server/service/contract_call_data_test.go index 06688f6..217d5e8 100644 --- a/server/service/contract_call_data_test.go +++ b/server/service/contract_call_data_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestConstruction_ContractCallData(t *testing.T) { @@ -47,6 +47,11 @@ func TestConstruction_ContractCallData(t *testing.T) { 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 { @@ -54,9 +59,9 @@ func TestConstruction_ContractCallData(t *testing.T) { bytes, err := constructContractCallDataGeneric(test.methodSig, test.methodArgs) if err != nil { fmt.Println(err) - assert.EqualError(t, err, test.expectedError.Error()) + require.EqualError(t, err, test.expectedError.Error()) } else { - assert.Equal(t, test.expectedResponse, hexutil.Encode(bytes)) + require.Equal(t, test.expectedResponse, hexutil.Encode(bytes)) } }) } diff --git a/server/service/errors.go b/server/service/errors.go index 973db1e..34e2960 100644 --- a/server/service/errors.go +++ b/server/service/errors.go @@ -7,31 +7,33 @@ import ( var ( // Errors lists all available error types Errors = []*types.Error{ - errNotReady, - errNotImplemented, - errNotSupported, - errUnavailableOffline, - errInternalError, - errInvalidInput, - errClientError, - errBlockInvalidInput, - errBlockNotFound, - errCallInvalidMethod, - errCallInvalidParams, + ErrNotReady, + ErrNotImplemented, + ErrNotSupported, + ErrUnavailableOffline, + ErrInternalError, + ErrInvalidInput, + ErrClientError, + ErrBlockInvalidInput, + ErrBlockNotFound, + ErrCallInvalidMethod, + ErrCallInvalidParams, + ErrTransactionNotFound, } // General errors - errNotReady = makeError(1, "Node is not ready", true) - errNotImplemented = makeError(2, "Endpoint is not implemented", false) - errNotSupported = makeError(3, "Endpoint is not supported", false) - errUnavailableOffline = makeError(4, "Endpoint is not available offline", false) - errInternalError = makeError(5, "Internal server error", true) - errInvalidInput = makeError(6, "Invalid input", false) - errClientError = makeError(7, "Client error", true) - errBlockInvalidInput = makeError(8, "Block number or hash is required", false) - errBlockNotFound = makeError(9, "Block was not found", true) - errCallInvalidMethod = makeError(10, "Invalid call method", false) - errCallInvalidParams = makeError(11, "invalid call params", false) + ErrNotReady = makeError(1, "Node is not ready", true) + ErrNotImplemented = makeError(2, "Endpoint is not implemented", false) + ErrNotSupported = makeError(3, "Endpoint is not supported", false) + ErrUnavailableOffline = makeError(4, "Endpoint is not available offline", false) + ErrInternalError = makeError(5, "Internal server error", true) + ErrInvalidInput = makeError(6, "Invalid input", false) + ErrClientError = makeError(7, "Client error", true) + ErrBlockInvalidInput = makeError(8, "Block number or hash is required", false) + ErrBlockNotFound = makeError(9, "Block was not found", true) + ErrCallInvalidMethod = makeError(10, "Invalid call method", false) + ErrCallInvalidParams = makeError(11, "invalid call params", false) + ErrTransactionNotFound = makeError(12, "Transaction was not found", true) ) func makeError(code int32, message string, retriable bool) *types.Error { @@ -43,7 +45,7 @@ func makeError(code int32, message string, retriable bool) *types.Error { } } -func wrapError(err *types.Error, message interface{}) *types.Error { +func WrapError(err *types.Error, message interface{}) *types.Error { newErr := makeError(err.Code, err.Message, err.Retriable) if err.Description != nil { @@ -54,11 +56,11 @@ func wrapError(err *types.Error, message interface{}) *types.Error { newErr.Details[k] = v } - switch t := message.(type) { + switch castMsg := message.(type) { case error: - newErr.Details["error"] = t.Error() + newErr.Details["error"] = castMsg.Error() default: - newErr.Details["error"] = t + newErr.Details["error"] = castMsg } return newErr diff --git a/server/service/helper.go b/server/service/helper.go index bb89bdf..d75061e 100644 --- a/server/service/helper.go +++ b/server/service/helper.go @@ -2,20 +2,21 @@ package service import ( "context" - "encoding/json" "math/big" "strings" - "github.com/ava-labs/avalanche-rosetta/client" "github.com/coinbase/rosetta-sdk-go/types" + "github.com/ethereum/go-ethereum/common" + + "github.com/ava-labs/avalanche-rosetta/client" ethtypes "github.com/ava-labs/coreth/core/types" - ethcommon "github.com/ethereum/go-ethereum/common" ) const ( nativeTransferGasLimit = uint64(21000) erc20TransferGasLimit = uint64(250000) + unwrapGasLimit = uint64(750000) genesisTimestamp = 946713601000 // min allowable timestamp ) @@ -47,48 +48,23 @@ func blockHeaderFromInput( header, err = c.HeaderByNumber(ctx, nil) } else { if input.Hash == nil && input.Index == nil { - return nil, errInvalidInput + return nil, ErrInvalidInput } if input.Index != nil { header, err = c.HeaderByNumber(ctx, big.NewInt(*input.Index)) } else { - header, err = c.HeaderByHash(ctx, ethcommon.HexToHash(*input.Hash)) + header, err = c.HeaderByHash(ctx, common.HexToHash(*input.Hash)) } } if err != nil { - return nil, wrapError(errInternalError, err) + return nil, WrapError(ErrInternalError, err) } return header, nil } -// unmarshalJSONMap converts map[string]interface{} into a interface{}. -func unmarshalJSONMap(m map[string]interface{}, i interface{}) error { - b, err := json.Marshal(m) - if err != nil { - return err - } - - return json.Unmarshal(b, i) -} - -// marshalJSONMap converts an interface into a map[string]interface{}. -func marshalJSONMap(i interface{}) (map[string]interface{}, error) { - b, err := json.Marshal(i) - if err != nil { - return nil, err - } - - var m map[string]interface{} - if err := json.Unmarshal(b, &m); err != nil { - return nil, err - } - - return m, nil -} - // ChecksumAddress ensures an Ethereum hex address // is in Checksum Format. If the address cannot be converted, // it returns !ok. @@ -97,7 +73,7 @@ func ChecksumAddress(address string) (string, bool) { return "", false } - addr, err := ethcommon.NewMixedcaseAddressFromString(address) + addr, err := common.NewMixedcaseAddressFromString(address) if err != nil { return "", false } diff --git a/server/service/helper_test.go b/server/service/helper_test.go index 2c1f783..ff35153 100644 --- a/server/service/helper_test.go +++ b/server/service/helper_test.go @@ -3,42 +3,42 @@ package service import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestChecksumAddress(t *testing.T) { t.Run("valid checksum address", func(t *testing.T) { testAddr := "0x05da63494DfbfF6AA215E074D34aC9A25B616eF2" addr, ok := ChecksumAddress(testAddr) - assert.True(t, ok) - assert.Equal(t, testAddr, addr) + require.True(t, ok) + require.Equal(t, testAddr, addr) }) t.Run("modified checksum address", func(t *testing.T) { testAddr := "0x05da63494DfbfF6AA215E074D34aC9A25B616ef2" addr, ok := ChecksumAddress(testAddr) - assert.True(t, ok) - assert.Equal(t, "0x05da63494DfbfF6AA215E074D34aC9A25B616eF2", addr) + require.True(t, ok) + require.Equal(t, "0x05da63494DfbfF6AA215E074D34aC9A25B616eF2", addr) }) t.Run("invalid hex", func(t *testing.T) { testAddr := "0x05da63494DfbfF6AA215E074D34aC9A25B616eK2" addr, ok := ChecksumAddress(testAddr) - assert.False(t, ok) - assert.Equal(t, "", addr) + require.False(t, ok) + require.Equal(t, "", addr) }) t.Run("invalid length", func(t *testing.T) { testAddr := "0x05da63494DfbfF6AA215E074D34aC9A25B" addr, ok := ChecksumAddress(testAddr) - assert.False(t, ok) - assert.Equal(t, "", addr) + require.False(t, ok) + require.Equal(t, "", addr) }) t.Run("missing 0x", func(t *testing.T) { testAddr := "05da63494DfbfF6AA215E074D34aC9A25B616eF2" addr, ok := ChecksumAddress(testAddr) - assert.False(t, ok) - assert.Equal(t, "", addr) + require.False(t, ok) + require.Equal(t, "", addr) }) } diff --git a/server/service/mock_service.go b/server/service/mock_service.go new file mode 100644 index 0000000..5303031 --- /dev/null +++ b/server/service/mock_service.go @@ -0,0 +1,242 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ava-labs/avalanche-rosetta/service (interfaces: AccountBackend,ConstructionBackend) +// +// Generated by this command: +// +// mockgen -package=service -destination=service/mock_service.go github.com/ava-labs/avalanche-rosetta/service AccountBackend,ConstructionBackend +// + +// Package service is a generated GoMock package. +package service + +import ( + context "context" + reflect "reflect" + + types "github.com/coinbase/rosetta-sdk-go/types" + gomock "go.uber.org/mock/gomock" +) + +// MockAccountBackend is a mock of AccountBackend interface. +type MockAccountBackend struct { + ctrl *gomock.Controller + recorder *MockAccountBackendMockRecorder +} + +// MockAccountBackendMockRecorder is the mock recorder for MockAccountBackend. +type MockAccountBackendMockRecorder struct { + mock *MockAccountBackend +} + +// NewMockAccountBackend creates a new mock instance. +func NewMockAccountBackend(ctrl *gomock.Controller) *MockAccountBackend { + mock := &MockAccountBackend{ctrl: ctrl} + mock.recorder = &MockAccountBackendMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAccountBackend) EXPECT() *MockAccountBackendMockRecorder { + return m.recorder +} + +// AccountBalance mocks base method. +func (m *MockAccountBackend) AccountBalance(arg0 context.Context, arg1 *types.AccountBalanceRequest) (*types.AccountBalanceResponse, *types.Error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AccountBalance", arg0, arg1) + ret0, _ := ret[0].(*types.AccountBalanceResponse) + ret1, _ := ret[1].(*types.Error) + return ret0, ret1 +} + +// AccountBalance indicates an expected call of AccountBalance. +func (mr *MockAccountBackendMockRecorder) AccountBalance(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountBalance", reflect.TypeOf((*MockAccountBackend)(nil).AccountBalance), arg0, arg1) +} + +// AccountCoins mocks base method. +func (m *MockAccountBackend) AccountCoins(arg0 context.Context, arg1 *types.AccountCoinsRequest) (*types.AccountCoinsResponse, *types.Error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AccountCoins", arg0, arg1) + ret0, _ := ret[0].(*types.AccountCoinsResponse) + ret1, _ := ret[1].(*types.Error) + return ret0, ret1 +} + +// AccountCoins indicates an expected call of AccountCoins. +func (mr *MockAccountBackendMockRecorder) AccountCoins(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountCoins", reflect.TypeOf((*MockAccountBackend)(nil).AccountCoins), arg0, arg1) +} + +// ShouldHandleRequest mocks base method. +func (m *MockAccountBackend) ShouldHandleRequest(arg0 any) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ShouldHandleRequest", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// ShouldHandleRequest indicates an expected call of ShouldHandleRequest. +func (mr *MockAccountBackendMockRecorder) ShouldHandleRequest(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShouldHandleRequest", reflect.TypeOf((*MockAccountBackend)(nil).ShouldHandleRequest), arg0) +} + +// MockConstructionBackend is a mock of ConstructionBackend interface. +type MockConstructionBackend struct { + ctrl *gomock.Controller + recorder *MockConstructionBackendMockRecorder +} + +// MockConstructionBackendMockRecorder is the mock recorder for MockConstructionBackend. +type MockConstructionBackendMockRecorder struct { + mock *MockConstructionBackend +} + +// NewMockConstructionBackend creates a new mock instance. +func NewMockConstructionBackend(ctrl *gomock.Controller) *MockConstructionBackend { + mock := &MockConstructionBackend{ctrl: ctrl} + mock.recorder = &MockConstructionBackendMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockConstructionBackend) EXPECT() *MockConstructionBackendMockRecorder { + return m.recorder +} + +// ConstructionCombine mocks base method. +func (m *MockConstructionBackend) ConstructionCombine(arg0 context.Context, arg1 *types.ConstructionCombineRequest) (*types.ConstructionCombineResponse, *types.Error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConstructionCombine", arg0, arg1) + ret0, _ := ret[0].(*types.ConstructionCombineResponse) + ret1, _ := ret[1].(*types.Error) + return ret0, ret1 +} + +// ConstructionCombine indicates an expected call of ConstructionCombine. +func (mr *MockConstructionBackendMockRecorder) ConstructionCombine(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConstructionCombine", reflect.TypeOf((*MockConstructionBackend)(nil).ConstructionCombine), arg0, arg1) +} + +// ConstructionDerive mocks base method. +func (m *MockConstructionBackend) ConstructionDerive(arg0 context.Context, arg1 *types.ConstructionDeriveRequest) (*types.ConstructionDeriveResponse, *types.Error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConstructionDerive", arg0, arg1) + ret0, _ := ret[0].(*types.ConstructionDeriveResponse) + ret1, _ := ret[1].(*types.Error) + return ret0, ret1 +} + +// ConstructionDerive indicates an expected call of ConstructionDerive. +func (mr *MockConstructionBackendMockRecorder) ConstructionDerive(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConstructionDerive", reflect.TypeOf((*MockConstructionBackend)(nil).ConstructionDerive), arg0, arg1) +} + +// ConstructionHash mocks base method. +func (m *MockConstructionBackend) ConstructionHash(arg0 context.Context, arg1 *types.ConstructionHashRequest) (*types.TransactionIdentifierResponse, *types.Error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConstructionHash", arg0, arg1) + ret0, _ := ret[0].(*types.TransactionIdentifierResponse) + ret1, _ := ret[1].(*types.Error) + return ret0, ret1 +} + +// ConstructionHash indicates an expected call of ConstructionHash. +func (mr *MockConstructionBackendMockRecorder) ConstructionHash(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConstructionHash", reflect.TypeOf((*MockConstructionBackend)(nil).ConstructionHash), arg0, arg1) +} + +// ConstructionMetadata mocks base method. +func (m *MockConstructionBackend) ConstructionMetadata(arg0 context.Context, arg1 *types.ConstructionMetadataRequest) (*types.ConstructionMetadataResponse, *types.Error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConstructionMetadata", arg0, arg1) + ret0, _ := ret[0].(*types.ConstructionMetadataResponse) + ret1, _ := ret[1].(*types.Error) + return ret0, ret1 +} + +// ConstructionMetadata indicates an expected call of ConstructionMetadata. +func (mr *MockConstructionBackendMockRecorder) ConstructionMetadata(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConstructionMetadata", reflect.TypeOf((*MockConstructionBackend)(nil).ConstructionMetadata), arg0, arg1) +} + +// ConstructionParse mocks base method. +func (m *MockConstructionBackend) ConstructionParse(arg0 context.Context, arg1 *types.ConstructionParseRequest) (*types.ConstructionParseResponse, *types.Error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConstructionParse", arg0, arg1) + ret0, _ := ret[0].(*types.ConstructionParseResponse) + ret1, _ := ret[1].(*types.Error) + return ret0, ret1 +} + +// ConstructionParse indicates an expected call of ConstructionParse. +func (mr *MockConstructionBackendMockRecorder) ConstructionParse(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConstructionParse", reflect.TypeOf((*MockConstructionBackend)(nil).ConstructionParse), arg0, arg1) +} + +// ConstructionPayloads mocks base method. +func (m *MockConstructionBackend) ConstructionPayloads(arg0 context.Context, arg1 *types.ConstructionPayloadsRequest) (*types.ConstructionPayloadsResponse, *types.Error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConstructionPayloads", arg0, arg1) + ret0, _ := ret[0].(*types.ConstructionPayloadsResponse) + ret1, _ := ret[1].(*types.Error) + return ret0, ret1 +} + +// ConstructionPayloads indicates an expected call of ConstructionPayloads. +func (mr *MockConstructionBackendMockRecorder) ConstructionPayloads(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConstructionPayloads", reflect.TypeOf((*MockConstructionBackend)(nil).ConstructionPayloads), arg0, arg1) +} + +// ConstructionPreprocess mocks base method. +func (m *MockConstructionBackend) ConstructionPreprocess(arg0 context.Context, arg1 *types.ConstructionPreprocessRequest) (*types.ConstructionPreprocessResponse, *types.Error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConstructionPreprocess", arg0, arg1) + ret0, _ := ret[0].(*types.ConstructionPreprocessResponse) + ret1, _ := ret[1].(*types.Error) + return ret0, ret1 +} + +// ConstructionPreprocess indicates an expected call of ConstructionPreprocess. +func (mr *MockConstructionBackendMockRecorder) ConstructionPreprocess(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConstructionPreprocess", reflect.TypeOf((*MockConstructionBackend)(nil).ConstructionPreprocess), arg0, arg1) +} + +// ConstructionSubmit mocks base method. +func (m *MockConstructionBackend) ConstructionSubmit(arg0 context.Context, arg1 *types.ConstructionSubmitRequest) (*types.TransactionIdentifierResponse, *types.Error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConstructionSubmit", arg0, arg1) + ret0, _ := ret[0].(*types.TransactionIdentifierResponse) + ret1, _ := ret[1].(*types.Error) + return ret0, ret1 +} + +// ConstructionSubmit indicates an expected call of ConstructionSubmit. +func (mr *MockConstructionBackendMockRecorder) ConstructionSubmit(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConstructionSubmit", reflect.TypeOf((*MockConstructionBackend)(nil).ConstructionSubmit), arg0, arg1) +} + +// ShouldHandleRequest mocks base method. +func (m *MockConstructionBackend) ShouldHandleRequest(arg0 any) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ShouldHandleRequest", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// ShouldHandleRequest indicates an expected call of ShouldHandleRequest. +func (mr *MockConstructionBackendMockRecorder) ShouldHandleRequest(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShouldHandleRequest", reflect.TypeOf((*MockConstructionBackend)(nil).ShouldHandleRequest), arg0) +} diff --git a/server/service/rosetta.go b/server/service/rosetta.go index 5e564e6..99d496f 100644 --- a/server/service/rosetta.go +++ b/server/service/rosetta.go @@ -14,6 +14,6 @@ var NodeVersion = fmt.Sprintf( ) const ( - MiddlewareVersion = "0.1.13" + MiddlewareVersion = "0.1.42" BlockchainName = "flare" ) diff --git a/server/service/service_account.go b/server/service/service_account.go index 91388f0..01e5c52 100644 --- a/server/service/service_account.go +++ b/server/service/service_account.go @@ -5,29 +5,52 @@ import ( "errors" "fmt" + "github.com/ava-labs/coreth/interfaces" "github.com/coinbase/rosetta-sdk-go/server" "github.com/coinbase/rosetta-sdk-go/types" "github.com/coinbase/rosetta-sdk-go/utils" - - ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ava-labs/avalanche-rosetta/client" "github.com/ava-labs/avalanche-rosetta/mapper" - "github.com/ava-labs/coreth/interfaces" ) +// AccountBackend represents a backend that implements /account family of apis for a subset of requests. +// Endpoint handlers in this file delegates requests to corresponding backends based on the request. +// Each backend implements a ShouldHandleRequest method to determine whether that backend should handle the given request. +// +// P-chain and C-chain atomic transaction logic are implemented in pchain.Backend and cchainatomictx.Backend respectively. +// Eventually, the C-chain non-atomic transaction logic implemented in this file should be extracted to its own backend as well. +type AccountBackend interface { + // ShouldHandleRequest returns whether a given request should be handled by this backend + ShouldHandleRequest(req interface{}) bool + // AccountBalance implements /account/balance endpoint for this backend + AccountBalance(ctx context.Context, req *types.AccountBalanceRequest) (*types.AccountBalanceResponse, *types.Error) + // AccountCoins implements /account/coins endpoint for this backend + AccountCoins(ctx context.Context, req *types.AccountCoinsRequest) (*types.AccountCoinsResponse, *types.Error) +} + // AccountService implements the /account/* endpoints type AccountService struct { - config *Config - client client.Client + config *Config + client client.Client + cChainAtomicTxBackend AccountBackend + pChainBackend AccountBackend } // NewAccountService returns a new network servicer -func NewAccountService(config *Config, client client.Client) server.AccountAPIServicer { +func NewAccountService( + config *Config, + client client.Client, + pChainBackend AccountBackend, + cChainAtomicTxBackend AccountBackend, +) server.AccountAPIServicer { return &AccountService{ - config: config, - client: client, + config: config, + client: client, + cChainAtomicTxBackend: cChainAtomicTxBackend, + pChainBackend: pChainBackend, } } @@ -37,11 +60,20 @@ func (s AccountService) AccountBalance( req *types.AccountBalanceRequest, ) (*types.AccountBalanceResponse, *types.Error) { if s.config.IsOfflineMode() { - return nil, errUnavailableOffline + return nil, ErrUnavailableOffline + } + + if s.pChainBackend.ShouldHandleRequest(req) { + return s.pChainBackend.AccountBalance(ctx, req) } if req.AccountIdentifier == nil { - return nil, wrapError(errInvalidInput, "account identifier is not provided") + return nil, WrapError(ErrInvalidInput, "account identifier is not provided") + } + + // If the address is in Bech32 format, we check the atomic balance + if s.cChainAtomicTxBackend.ShouldHandleRequest(req) { + return s.cChainAtomicTxBackend.AccountBalance(ctx, req) } header, terr := blockHeaderFromInput(ctx, s.client, req.BlockIdentifier) @@ -49,25 +81,25 @@ func (s AccountService) AccountBalance( return nil, terr } - address := ethcommon.HexToAddress(req.AccountIdentifier.Address) + address := common.HexToAddress(req.AccountIdentifier.Address) nonce, err := s.client.NonceAt(ctx, address, header.Number) if err != nil { - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } metadata := &accountMetadata{ Nonce: nonce, } - metadataMap, err := marshalJSONMap(metadata) + metadataMap, err := mapper.MarshalJSONMap(metadata) if err != nil { - return nil, wrapError(errInternalError, err) + return nil, WrapError(ErrInternalError, err) } flrBalance, err := s.client.BalanceAt(ctx, address, header.Number) if err != nil { - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } balances := []*types.Amount{} @@ -82,7 +114,7 @@ func (s AccountService) AccountBalance( balances = append(balances, mapper.FlareAmount(flrBalance)) continue } - return nil, wrapError(errCallInvalidParams, errors.New("non-flr currencies must specify contractAddress in metadata")) + return nil, WrapError(ErrCallInvalidParams, errors.New("non-flr currencies must specify contractAddress in metadata")) } identifierAddress := req.AccountIdentifier.Address @@ -92,14 +124,14 @@ func (s AccountService) AccountBalance( data, err := hexutil.Decode(BalanceOfMethodPrefix + identifierAddress) if err != nil { - return nil, wrapError(errCallInvalidParams, fmt.Errorf("%w: marshalling balanceOf call msg data failed", err)) + return nil, WrapError(ErrCallInvalidParams, fmt.Errorf("%w: marshalling balanceOf call msg data failed", err)) } - contractAddress := ethcommon.HexToAddress(value.(string)) + contractAddress := common.HexToAddress(value.(string)) callMsg := interfaces.CallMsg{To: &contractAddress, Data: data} response, err := s.client.CallContract(ctx, callMsg, header.Number) if err != nil { - return nil, wrapError(errInternalError, err) + return nil, WrapError(ErrInternalError, err) } amount := mapper.Erc20Amount(response, currency, false) @@ -123,7 +155,16 @@ func (s AccountService) AccountCoins( req *types.AccountCoinsRequest, ) (*types.AccountCoinsResponse, *types.Error) { if s.config.IsOfflineMode() { - return nil, errUnavailableOffline + return nil, ErrUnavailableOffline + } + + if s.pChainBackend.ShouldHandleRequest(req) { + return s.pChainBackend.AccountCoins(ctx, req) } - return nil, errNotImplemented + + if s.cChainAtomicTxBackend.ShouldHandleRequest(req) { + return s.cChainAtomicTxBackend.AccountCoins(ctx, req) + } + + return nil, ErrNotImplemented } diff --git a/server/service/service_account_test.go b/server/service/service_account_test.go new file mode 100644 index 0000000..1b365bc --- /dev/null +++ b/server/service/service_account_test.go @@ -0,0 +1,143 @@ +package service + +import ( + "context" + "testing" + + "github.com/coinbase/rosetta-sdk-go/types" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/ava-labs/avalanche-rosetta/constants" +) + +func TestAccountBalance(t *testing.T) { + ctrl := gomock.NewController(t) + pBackendMock := NewMockAccountBackend(ctrl) + cBackendMock := NewMockAccountBackend(ctrl) + + service := AccountService{ + config: &Config{Mode: ModeOnline}, + pChainBackend: pBackendMock, + cChainAtomicTxBackend: cBackendMock, + } + t.Run("p-chain request is delegated to p-chain backend", func(t *testing.T) { + req := &types.AccountBalanceRequest{ + NetworkIdentifier: &types.NetworkIdentifier{ + Network: constants.TestnetNetwork, + SubNetworkIdentifier: &types.SubNetworkIdentifier{ + Network: constants.PChain.String(), + }, + }, + AccountIdentifier: &types.AccountIdentifier{ + Address: "P-fuji15f9g0h5xkr5cp47n6u3qxj6yjtzzzrdr23a3tl", + }, + } + + expectedResp := &types.AccountBalanceResponse{} + pBackendMock.EXPECT().ShouldHandleRequest(req).Return(true) + pBackendMock.EXPECT().AccountBalance(gomock.Any(), req).Return(expectedResp, nil) + + resp, err := service.AccountBalance(context.Background(), req) + + require.Nil(t, err) + require.Equal(t, expectedResp, resp) + }) + + t.Run("c-chain atomic request is delegated to c-chain atomic tx backend", func(t *testing.T) { + req := &types.AccountBalanceRequest{ + NetworkIdentifier: &types.NetworkIdentifier{ + Network: constants.TestnetNetwork, + }, + AccountIdentifier: &types.AccountIdentifier{ + Address: "C-fuji15f9g0h5xkr5cp47n6u3qxj6yjtzzzrdr23a3tl", + }, + } + + expectedResp := &types.AccountBalanceResponse{} + pBackendMock.EXPECT().ShouldHandleRequest(req).Return(false) + cBackendMock.EXPECT().ShouldHandleRequest(req).Return(true) + cBackendMock.EXPECT().AccountBalance(gomock.Any(), req).Return(expectedResp, nil) + + resp, err := service.AccountBalance(context.Background(), req) + + require.Nil(t, err) + require.Equal(t, expectedResp, resp) + }) +} + +func TestAccountCoins(t *testing.T) { + ctrl := gomock.NewController(t) + pBackendMock := NewMockAccountBackend(ctrl) + cBackendMock := NewMockAccountBackend(ctrl) + + service := AccountService{ + config: &Config{Mode: ModeOnline}, + pChainBackend: pBackendMock, + cChainAtomicTxBackend: cBackendMock, + } + t.Run("p-chain request is delegated to p-chain backend", func(t *testing.T) { + req := &types.AccountCoinsRequest{ + NetworkIdentifier: &types.NetworkIdentifier{ + Network: constants.TestnetNetwork, + SubNetworkIdentifier: &types.SubNetworkIdentifier{ + Network: constants.PChain.String(), + }, + }, + AccountIdentifier: &types.AccountIdentifier{ + Address: "P-fuji15f9g0h5xkr5cp47n6u3qxj6yjtzzzrdr23a3tl", + }, + } + + expectedResp := &types.AccountCoinsResponse{} + + pBackendMock.EXPECT().ShouldHandleRequest(req).Return(true) + pBackendMock.EXPECT().AccountCoins(gomock.Any(), req).Return(expectedResp, nil) + + resp, err := service.AccountCoins(context.Background(), req) + + require.Nil(t, err) + require.Equal(t, expectedResp, resp) + }) + + t.Run("c-chain atomic request is delegated to c-chain atomic tx backend", func(t *testing.T) { + req := &types.AccountCoinsRequest{ + NetworkIdentifier: &types.NetworkIdentifier{ + Network: constants.TestnetNetwork, + }, + AccountIdentifier: &types.AccountIdentifier{ + Address: "C-fuji15f9g0h5xkr5cp47n6u3qxj6yjtzzzrdr23a3tl", + }, + } + + expectedResp := &types.AccountCoinsResponse{} + + pBackendMock.EXPECT().ShouldHandleRequest(req).Return(false) + cBackendMock.EXPECT().ShouldHandleRequest(req).Return(true) + cBackendMock.EXPECT().AccountCoins(gomock.Any(), req).Return(expectedResp, nil) + + resp, err := service.AccountCoins(context.Background(), req) + + require.Nil(t, err) + require.Equal(t, expectedResp, resp) + }) + + t.Run("c-chain regular request is not supported", func(t *testing.T) { + req := &types.AccountCoinsRequest{ + NetworkIdentifier: &types.NetworkIdentifier{ + Network: constants.TestnetNetwork, + }, + AccountIdentifier: &types.AccountIdentifier{ + Address: "0x197E90f9FAD81970bA7976f33CbD77088E5D7cf7", + }, + } + + pBackendMock.EXPECT().ShouldHandleRequest(req).Return(false) + cBackendMock.EXPECT().ShouldHandleRequest(req).Return(false) + + resp, err := service.AccountCoins(context.Background(), req) + + require.Equal(t, ErrNotImplemented, err) + require.Nil(t, resp) + }) +} diff --git a/server/service/service_block.go b/server/service/service_block.go index 3fe8044..0bda70c 100644 --- a/server/service/service_block.go +++ b/server/service/service_block.go @@ -5,31 +5,55 @@ import ( "math/big" "strings" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/coreth/core" "github.com/coinbase/rosetta-sdk-go/server" "github.com/coinbase/rosetta-sdk-go/types" "github.com/coinbase/rosetta-sdk-go/utils" - - corethTypes "github.com/ava-labs/coreth/core/types" - ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common" "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/constants" "github.com/ava-labs/avalanche-rosetta/mapper" + + ethtypes "github.com/ava-labs/coreth/core/types" ) +// BlockBackend represents a backend that implements /block family of apis for a subset of requests +// Endpoint handlers in this file delegates requests to corresponding backends based on the request. +// Each backend implements a ShouldHandleRequest method to determine whether that backend should handle the given request. +// +// P-chain support is implemented in pchain.Backend which implements this interface. +// Eventually, the C-chain block logic implemented in this file should be extracted to its own backend as well. +type BlockBackend interface { + // ShouldHandleRequest returns whether a given request should be handled by this backend + ShouldHandleRequest(req interface{}) bool + // Block implements /block endpoint for this backend + Block(ctx context.Context, request *types.BlockRequest) (*types.BlockResponse, *types.Error) + // BlockTransaction implements /block/transaction endpoint for this backend + BlockTransaction(ctx context.Context, request *types.BlockTransactionRequest) (*types.BlockTransactionResponse, *types.Error) +} + // BlockService implements the /block/* endpoints type BlockService struct { - config *Config - client client.Client + config *Config + client client.Client + pChainBackend BlockBackend genesisBlock *types.Block } // NewBlockService returns a new block servicer -func NewBlockService(config *Config, c client.Client) server.BlockAPIServicer { +func NewBlockService( + config *Config, + c client.Client, + pChainBackend BlockBackend, +) server.BlockAPIServicer { return &BlockService{ - config: config, - client: c, - genesisBlock: makeGenesisBlock(config.GenesisBlockHash), + config: config, + client: c, + pChainBackend: pChainBackend, + genesisBlock: makeGenesisBlock(config.GenesisBlockHash), } } @@ -39,14 +63,18 @@ func (s *BlockService) Block( request *types.BlockRequest, ) (*types.BlockResponse, *types.Error) { if s.config.IsOfflineMode() { - return nil, errUnavailableOffline + return nil, ErrUnavailableOffline } if request.BlockIdentifier == nil { - return nil, errBlockInvalidInput + return nil, ErrBlockInvalidInput } if request.BlockIdentifier.Hash == nil && request.BlockIdentifier.Index == nil { - return nil, errBlockInvalidInput + return nil, ErrBlockInvalidInput + } + + if s.pChainBackend.ShouldHandleRequest(request) { + return s.pChainBackend.Block(ctx, request) } if s.isGenesisBlockRequest(request.BlockIdentifier) { @@ -58,20 +86,20 @@ func (s *BlockService) Block( var ( blockIdentifier *types.BlockIdentifier parentBlockIdentifier *types.BlockIdentifier - block *corethTypes.Block + block *ethtypes.Block err error ) if hash := request.BlockIdentifier.Hash; hash != nil { - block, err = s.client.BlockByHash(ctx, ethcommon.HexToHash(*hash)) + block, err = s.client.BlockByHash(ctx, common.HexToHash(*hash)) } else if index := request.BlockIdentifier.Index; block == nil && index != nil { block, err = s.client.BlockByNumber(ctx, big.NewInt(*index)) } if err != nil { if strings.Contains(err.Error(), "not found") { - return nil, errBlockNotFound + return nil, ErrBlockNotFound } - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } blockIdentifier = &types.BlockIdentifier{ @@ -82,7 +110,7 @@ func (s *BlockService) Block( if block.ParentHash().String() != s.config.GenesisBlockHash { parentBlock, err := s.client.HeaderByHash(ctx, block.ParentHash()) if err != nil { - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } parentBlockIdentifier = &types.BlockIdentifier{ @@ -98,7 +126,7 @@ func (s *BlockService) Block( return nil, terr } - crosstx, terr := s.parseCrossChainTransactions(block) + crosstx, terr := s.parseCrossChainTransactions(request.NetworkIdentifier, block) if terr != nil { return nil, terr } @@ -120,22 +148,26 @@ func (s *BlockService) BlockTransaction( request *types.BlockTransactionRequest, ) (*types.BlockTransactionResponse, *types.Error) { if s.config.IsOfflineMode() { - return nil, errUnavailableOffline + return nil, ErrUnavailableOffline } if request.BlockIdentifier == nil { - return nil, wrapError(errInvalidInput, "block identifier is not provided") + return nil, WrapError(ErrInvalidInput, "block identifier is not provided") + } + + if s.pChainBackend.ShouldHandleRequest(request) { + return s.pChainBackend.BlockTransaction(ctx, request) } - header, err := s.client.HeaderByHash(ctx, ethcommon.HexToHash(request.BlockIdentifier.Hash)) + header, err := s.client.HeaderByHash(ctx, common.HexToHash(request.BlockIdentifier.Hash)) if err != nil { - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } - hash := ethcommon.HexToHash(request.TransactionIdentifier.Hash) + hash := common.HexToHash(request.TransactionIdentifier.Hash) tx, pending, err := s.client.TransactionByHash(ctx, hash) if err != nil { - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } if pending { return nil, nil @@ -143,7 +175,7 @@ func (s *BlockService) BlockTransaction( trace, flattened, err := s.client.TraceTransaction(ctx, tx.Hash().String()) if err != nil { - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } transaction, terr := s.fetchTransaction(ctx, tx, header, trace, flattened) @@ -158,13 +190,13 @@ func (s *BlockService) BlockTransaction( func (s *BlockService) fetchTransactions( ctx context.Context, - block *corethTypes.Block, + block *ethtypes.Block, ) ([]*types.Transaction, *types.Error) { transactions := []*types.Transaction{} trace, flattened, err := s.client.TraceBlockByHash(ctx, block.Hash().String()) if err != nil { - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } for i, tx := range block.Transactions() { @@ -181,37 +213,42 @@ func (s *BlockService) fetchTransactions( func (s *BlockService) fetchTransaction( ctx context.Context, - tx *corethTypes.Transaction, - header *corethTypes.Header, + tx *ethtypes.Transaction, + header *ethtypes.Header, trace *client.Call, flattened []*client.FlatCall, ) (*types.Transaction, *types.Error) { - msg, err := tx.AsMessage(s.config.Signer(), header.BaseFee) + msg, err := core.TransactionToMessage(tx, s.config.Signer(), header.BaseFee) if err != nil { - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } receipt, err := s.client.TransactionReceipt(ctx, tx.Hash()) if err != nil { - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } - transaction, err := mapper.Transaction(header, tx, &msg, receipt, trace, flattened, s.client, s.config.IsAnalyticsMode(), s.config.TokenWhiteList, s.config.IndexUnknownTokens) + transaction, err := mapper.Transaction(header, tx, msg, receipt, trace, flattened, s.client, s.config.IsAnalyticsMode(), s.config.TokenWhiteList, s.config.IndexUnknownTokens) if err != nil { - return nil, wrapError(errInternalError, err) + return nil, WrapError(ErrInternalError, err) } return transaction, nil } func (s *BlockService) parseCrossChainTransactions( - block *corethTypes.Block, + networkIdentifier *types.NetworkIdentifier, + block *ethtypes.Block, ) ([]*types.Transaction, *types.Error) { result := []*types.Transaction{} - crossTxs, err := mapper.CrossChainTransactions(s.config.FlareAssetID, block, s.config.AP5Activation) + // This map is used to create addresses for cross chain export outputs + chainIDToAliasMapping := map[ids.ID]constants.ChainIDAlias{ + ids.Empty: constants.PChain, + } + crossTxs, err := mapper.CrossChainTransactions(networkIdentifier, chainIDToAliasMapping, s.config.FlareAssetID, block, s.config.AP5Activation) if err != nil { - return nil, wrapError(errInternalError, err) + return nil, WrapError(ErrInternalError, err) } for _, tx := range crossTxs { diff --git a/server/service/service_call.go b/server/service/service_call.go index 084f9c6..8e251ed 100644 --- a/server/service/service_call.go +++ b/server/service/service_call.go @@ -4,10 +4,11 @@ import ( "context" "encoding/json" - "github.com/ava-labs/avalanche-rosetta/client" "github.com/coinbase/rosetta-sdk-go/server" "github.com/coinbase/rosetta-sdk-go/types" "github.com/ethereum/go-ethereum/common" + + "github.com/ava-labs/avalanche-rosetta/client" ) // CallService implements /call/* endpoints @@ -36,14 +37,14 @@ func (s CallService) Call( req *types.CallRequest, ) (*types.CallResponse, *types.Error) { if s.config.IsOfflineMode() { - return nil, errUnavailableOffline + return nil, ErrUnavailableOffline } switch req.Method { case "eth_getTransactionReceipt": return s.callGetTransactionReceipt(ctx, req) default: - return nil, errCallInvalidMethod + return nil, ErrCallInvalidMethod } } @@ -53,26 +54,26 @@ func (s CallService) callGetTransactionReceipt( ) (*types.CallResponse, *types.Error) { var input GetTransactionReceiptInput if err := types.UnmarshalMap(req.Parameters, &input); err != nil { - return nil, wrapError(errCallInvalidParams, err) + return nil, WrapError(ErrCallInvalidParams, err) } if len(input.TxHash) == 0 { - return nil, wrapError(errCallInvalidParams, "tx_hash missing from params") + return nil, WrapError(ErrCallInvalidParams, "tx_hash missing from params") } receipt, err := s.client.TransactionReceipt(ctx, common.HexToHash(input.TxHash)) if err != nil { - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } jsonOutput, err := receipt.MarshalJSON() if err != nil { - return nil, wrapError(errInternalError, err) + return nil, WrapError(ErrInternalError, err) } var receiptMap map[string]interface{} if err := json.Unmarshal(jsonOutput, &receiptMap); err != nil { - return nil, wrapError(errInternalError, err) + return nil, WrapError(ErrInternalError, err) } return &types.CallResponse{Result: receiptMap}, nil diff --git a/server/service/service_construction.go b/server/service/service_construction.go index 8b0c6f2..de8c981 100644 --- a/server/service/service_construction.go +++ b/server/service/service_construction.go @@ -3,43 +3,93 @@ package service import ( "context" "encoding/json" + "errors" "fmt" "math/big" + "github.com/ava-labs/coreth/core" + "github.com/ava-labs/coreth/interfaces" "github.com/coinbase/rosetta-sdk-go/parser" "github.com/coinbase/rosetta-sdk-go/server" "github.com/coinbase/rosetta-sdk-go/types" "github.com/coinbase/rosetta-sdk-go/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" "golang.org/x/crypto/sha3" - ethtypes "github.com/ava-labs/coreth/core/types" - "github.com/ava-labs/coreth/interfaces" - ethcommon "github.com/ethereum/go-ethereum/common" - "github.com/ava-labs/avalanche-rosetta/client" "github.com/ava-labs/avalanche-rosetta/mapper" - "github.com/ethereum/go-ethereum/common/hexutil" - ethcrypto "github.com/ethereum/go-ethereum/crypto" + + ethtypes "github.com/ava-labs/coreth/core/types" ) const ( - padLength = 32 + // 68 Bytes = methodID (4 Bytes) + param 1 (32 Bytes) + param 2 (32 Bytes) + genericTransferBytesLength = 68 + genericUnwrapBytesLength = 68 - transferFnSignature = "transfer(address,uint256)" // do not include spaces in the string - transferDataLength = 68 // 4 (method id) + 2*32 (args) + requiredPaddingBytes = 32 + defaultUnwrapChainID = 0 + + // do not include spaces in the Fn Signature strings + transferFnSignature = "transfer(address,uint256)" + unwrapFnSignature = "unwrap(uint256,uint256)" +) + +var ( + // preallocate methodIDs used in parse functions + transferMethodID = hexutil.Encode(getMethodID(transferFnSignature)) + unwrapMethodID = hexutil.Encode(getMethodID(unwrapFnSignature)) ) +// ConstructionBackend represents a backend that implements /construction family of apis for a subset of requests. +// Endpoint handlers in this file delegates requests to corresponding backends based on the request. +// Each backend implements a ShouldHandleRequest method to determine whether that backend should handle the given request. +// +// P-chain and C-chain atomic transaction logic are implemented in pchain.Backend and cchainatomictx.Backend respectively. +// Eventually, the C-chain non-atomic transaction logic implemented in this file should be extracted to its own backend as well. +type ConstructionBackend interface { + // ShouldHandleRequest returns whether a given request should be handled by this backend + ShouldHandleRequest(req interface{}) bool + // ConstructionDerive implements /construction/derive endpoint for this backend + ConstructionDerive(ctx context.Context, req *types.ConstructionDeriveRequest) (*types.ConstructionDeriveResponse, *types.Error) + // ConstructionPreprocess implements /construction/preprocess endpoint for this backend + ConstructionPreprocess(ctx context.Context, req *types.ConstructionPreprocessRequest) (*types.ConstructionPreprocessResponse, *types.Error) + // ConstructionMetadata implements /construction/metadata endpoint for this backend + ConstructionMetadata(ctx context.Context, req *types.ConstructionMetadataRequest) (*types.ConstructionMetadataResponse, *types.Error) + // ConstructionPayloads implements /construction/payloads endpoint for this backend + ConstructionPayloads(ctx context.Context, req *types.ConstructionPayloadsRequest) (*types.ConstructionPayloadsResponse, *types.Error) + // ConstructionParse implements /construction/parse endpoint for this backend + ConstructionParse(ctx context.Context, req *types.ConstructionParseRequest) (*types.ConstructionParseResponse, *types.Error) + // ConstructionCombine implements /construction/combine endpoint for this backend + ConstructionCombine(ctx context.Context, req *types.ConstructionCombineRequest) (*types.ConstructionCombineResponse, *types.Error) + // ConstructionHash implements /construction/hash endpoint for this backend + ConstructionHash(ctx context.Context, req *types.ConstructionHashRequest) (*types.TransactionIdentifierResponse, *types.Error) + // ConstructionSubmit implements /construction/submit endpoint for this backend + ConstructionSubmit(ctx context.Context, req *types.ConstructionSubmitRequest) (*types.TransactionIdentifierResponse, *types.Error) +} + // ConstructionService implements /construction/* endpoints type ConstructionService struct { - config *Config - client client.Client + config *Config + client client.Client + cChainAtomicTxBackend ConstructionBackend + pChainBackend ConstructionBackend } // NewConstructionService returns a new construction servicer -func NewConstructionService(config *Config, client client.Client) server.ConstructionAPIServicer { +func NewConstructionService( + config *Config, + client client.Client, + pChainBackend ConstructionBackend, + cChainAtomicTxBackend ConstructionBackend, +) server.ConstructionAPIServicer { return &ConstructionService{ - config: config, - client: client, + config: config, + client: client, + cChainAtomicTxBackend: cChainAtomicTxBackend, + pChainBackend: pChainBackend, } } @@ -54,24 +104,32 @@ func (s ConstructionService) ConstructionMetadata( req *types.ConstructionMetadataRequest, ) (*types.ConstructionMetadataResponse, *types.Error) { if s.config.IsOfflineMode() { - return nil, errUnavailableOffline + return nil, ErrUnavailableOffline + } + + if s.pChainBackend.ShouldHandleRequest(req) { + return s.pChainBackend.ConstructionMetadata(ctx, req) + } + + if s.cChainAtomicTxBackend.ShouldHandleRequest(req) { + return s.cChainAtomicTxBackend.ConstructionMetadata(ctx, req) } var input options - if err := unmarshalJSONMap(req.Options, &input); err != nil { - return nil, wrapError(errInvalidInput, err) + if err := mapper.UnmarshalJSONMap(req.Options, &input); err != nil { + return nil, WrapError(ErrInvalidInput, err) } if len(input.From) == 0 { - return nil, wrapError(errInvalidInput, "from address is not provided") + return nil, WrapError(ErrInvalidInput, "from address is not provided") } var nonce uint64 var err error if input.Nonce == nil { - nonce, err = s.client.NonceAt(ctx, ethcommon.HexToAddress(input.From), nil) + nonce, err = s.client.NonceAt(ctx, common.HexToAddress(input.From), nil) if err != nil { - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } } else { nonce = input.Nonce.Uint64() @@ -80,7 +138,7 @@ func (s ConstructionService) ConstructionMetadata( var gasPrice *big.Int if input.GasPrice == nil { if gasPrice, err = s.client.SuggestGasPrice(ctx); err != nil { - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } if input.SuggestedFeeMultiplier != nil { @@ -96,18 +154,41 @@ func (s ConstructionService) ConstructionMetadata( var gasLimit uint64 if input.GasLimit == nil { - if input.Currency == nil || utils.Equal(input.Currency, mapper.FlareCurrency) { + if input.Currency == nil || types.Hash(input.Currency) == types.Hash(mapper.FlareCurrency) { + var err error if utils.Equal(input.Currency, mapper.FlareCurrency) && len(input.ContractAddress) > 0 { - gasLimit, err = s.getGenericContractCallGasLimit(ctx, input.ContractAddress, input.From, input.Data) + var data []byte + data, err = hexutil.Decode(input.ContractData) + if err == nil { + gasLimit, err = s.getGenericContractCallGasLimit(ctx, input.ContractAddress, input.From, data) + } } else { gasLimit, err = s.getNativeTransferGasLimit(ctx, input.To, input.From, input.Value) } + if err != nil { + return nil, WrapError(ErrClientError, err) + } } else { - gasLimit, err = s.getErc20TransferGasLimit(ctx, input.To, input.From, input.Value, input.Currency) - } - - if err != nil { - return nil, wrapError(errClientError, err) + switch { + case input.Metadata != nil: + if !input.Metadata.UnwrapBridgeTx { + return nil, WrapError(ErrInvalidInput, "UnwrapBridgeTx must be populated if input.Metadata is provided") + } + + gasLimit, err = s.getBridgeUnwrapTransferGasLimit(ctx, input.From, input.Value, input.Currency) + case len(input.ContractAddress) > 0: + var contractData []byte + contractData, err = hexutil.Decode(input.ContractData) + if err != nil { + return nil, WrapError(ErrClientError, err) + } + gasLimit, err = s.getGenericContractCallGasLimit(ctx, input.ContractAddress, input.From, contractData) + default: + gasLimit, err = s.getErc20TransferGasLimit(ctx, input.To, input.From, input.Value, input.Currency) + } + if err != nil { + return nil, WrapError(ErrClientError, err) + } } } else { gasLimit = input.GasLimit.Uint64() @@ -117,14 +198,20 @@ func (s ConstructionService) ConstructionMetadata( Nonce: nonce, GasPrice: gasPrice, GasLimit: gasLimit, + ContractData: input.ContractData, MethodSignature: input.MethodSignature, MethodArgs: input.MethodArgs, - Data: input.Data, } - metadataMap, err := marshalJSONMap(metadata) + if input.Metadata != nil { + if input.Metadata.UnwrapBridgeTx { + metadata.UnwrapBridgeTx = true + } + } + + metadataMap, err := mapper.MarshalJSONMap(metadata) if err != nil { - return nil, wrapError(errInternalError, err) + return nil, WrapError(ErrInternalError, err) } suggestedFee := gasPrice.Int64() * int64(gasLimit) @@ -144,17 +231,25 @@ func (s ConstructionService) ConstructionHash( req *types.ConstructionHashRequest, ) (*types.TransactionIdentifierResponse, *types.Error) { if len(req.SignedTransaction) == 0 { - return nil, wrapError(errInvalidInput, "signed transaction value is not provided") + return nil, WrapError(ErrInvalidInput, "signed transaction value is not provided") + } + + if s.pChainBackend.ShouldHandleRequest(req) { + return s.pChainBackend.ConstructionHash(ctx, req) + } + + if s.cChainAtomicTxBackend.ShouldHandleRequest(req) { + return s.cChainAtomicTxBackend.ConstructionHash(ctx, req) } var wrappedTx signedTransactionWrapper if err := json.Unmarshal([]byte(req.SignedTransaction), &wrappedTx); err != nil { - return nil, wrapError(errInvalidInput, err) + return nil, WrapError(ErrInvalidInput, err) } var signedTx ethtypes.Transaction if err := signedTx.UnmarshalJSON(wrappedTx.SignedTransaction); err != nil { - return nil, wrapError(errInvalidInput, err) + return nil, WrapError(ErrInvalidInput, err) } return &types.TransactionIdentifierResponse{ @@ -174,20 +269,28 @@ func (s ConstructionService) ConstructionCombine( req *types.ConstructionCombineRequest, ) (*types.ConstructionCombineResponse, *types.Error) { if len(req.UnsignedTransaction) == 0 { - return nil, wrapError(errInvalidInput, "transaction data is not provided") + return nil, WrapError(ErrInvalidInput, "transaction data is not provided") } if len(req.Signatures) == 0 { - return nil, wrapError(errInvalidInput, "signature is not provided") + return nil, WrapError(ErrInvalidInput, "signature is not provided") + } + + if s.pChainBackend.ShouldHandleRequest(req) { + return s.pChainBackend.ConstructionCombine(ctx, req) + } + + if s.cChainAtomicTxBackend.ShouldHandleRequest(req) { + return s.cChainAtomicTxBackend.ConstructionCombine(ctx, req) } var unsignedTx transaction if err := json.Unmarshal([]byte(req.UnsignedTransaction), &unsignedTx); err != nil { - return nil, wrapError(errInvalidInput, err) + return nil, WrapError(ErrInvalidInput, err) } ethTransaction := ethtypes.NewTransaction( unsignedTx.Nonce, - ethcommon.HexToAddress(unsignedTx.To), + common.HexToAddress(unsignedTx.To), unsignedTx.Value, unsignedTx.GasLimit, unsignedTx.GasPrice, @@ -197,19 +300,22 @@ func (s ConstructionService) ConstructionCombine( signer := ethtypes.LatestSignerForChainID(unsignedTx.ChainID) signedTx, err := ethTransaction.WithSignature(signer, req.Signatures[0].Bytes) if err != nil { - return nil, wrapError(errInvalidInput, err) + return nil, WrapError(ErrInvalidInput, err) } signedTxJSON, err := signedTx.MarshalJSON() if err != nil { - return nil, wrapError(errInternalError, err) + return nil, WrapError(ErrInternalError, err) } - wrappedSignedTx := signedTransactionWrapper{SignedTransaction: signedTxJSON, Currency: unsignedTx.Currency} + wrappedSignedTx := signedTransactionWrapper{ + SignedTransaction: signedTxJSON, + Currency: unsignedTx.Currency, + } wrappedSignedTxJSON, err := json.Marshal(wrappedSignedTx) if err != nil { - return nil, wrapError(errInternalError, err) + return nil, WrapError(ErrInternalError, err) } return &types.ConstructionCombineResponse{ @@ -226,17 +332,25 @@ func (s ConstructionService) ConstructionDerive( req *types.ConstructionDeriveRequest, ) (*types.ConstructionDeriveResponse, *types.Error) { if req.PublicKey == nil { - return nil, wrapError(errInvalidInput, "public key is not provided") + return nil, WrapError(ErrInvalidInput, "public key is not provided") + } + + if s.pChainBackend.ShouldHandleRequest(req) { + return s.pChainBackend.ConstructionDerive(ctx, req) } - key, err := ethcrypto.DecompressPubkey(req.PublicKey.Bytes) + if s.cChainAtomicTxBackend.ShouldHandleRequest(req) { + return s.cChainAtomicTxBackend.ConstructionDerive(ctx, req) + } + + key, err := crypto.DecompressPubkey(req.PublicKey.Bytes) if err != nil { - return nil, wrapError(errInvalidInput, err) + return nil, WrapError(ErrInvalidInput, err) } return &types.ConstructionDeriveResponse{ AccountIdentifier: &types.AccountIdentifier{ - Address: ethcrypto.PubkeyToAddress(*key).Hex(), + Address: crypto.PubkeyToAddress(*key).Hex(), }, }, nil } @@ -250,21 +364,29 @@ func (s ConstructionService) ConstructionParse( ctx context.Context, req *types.ConstructionParseRequest, ) (*types.ConstructionParseResponse, *types.Error) { + if s.pChainBackend.ShouldHandleRequest(req) { + return s.pChainBackend.ConstructionParse(ctx, req) + } + + if s.cChainAtomicTxBackend.ShouldHandleRequest(req) { + return s.cChainAtomicTxBackend.ConstructionParse(ctx, req) + } + var tx transaction if !req.Signed { if err := json.Unmarshal([]byte(req.Transaction), &tx); err != nil { - return nil, wrapError(errInvalidInput, err) + return nil, WrapError(ErrInvalidInput, err) } } else { var wrappedTx signedTransactionWrapper if err := json.Unmarshal([]byte(req.Transaction), &wrappedTx); err != nil { - return nil, wrapError(errInvalidInput, err) + return nil, WrapError(ErrInvalidInput, err) } var t ethtypes.Transaction if err := t.UnmarshalJSON(wrappedTx.SignedTransaction); err != nil { - return nil, wrapError(errInvalidInput, err) + return nil, WrapError(ErrInvalidInput, err) } tx.To = t.To().String() @@ -276,35 +398,81 @@ func (s ConstructionService) ConstructionParse( tx.ChainID = s.config.ChainID tx.Currency = wrappedTx.Currency - msg, err := t.AsMessage(s.config.Signer(), nil) + msg, err := core.TransactionToMessage(&t, s.config.Signer(), nil) if err != nil { - return nil, wrapError(errInvalidInput, err) + return nil, WrapError(ErrInvalidInput, err) } - tx.From = msg.From().Hex() + tx.From = msg.From.Hex() + } + + metadata := &parseMetadata{ + Nonce: tx.Nonce, + GasPrice: tx.GasPrice, + GasLimit: tx.GasLimit, + ChainID: tx.ChainID, + } + metaMap, err := mapper.MarshalJSONMap(metadata) + if err != nil { + return nil, WrapError(ErrInternalError, err) } - var opMethod string - var value *big.Int - var toAddressHex string + var ( + ops []*types.Operation + checkFrom *string + wrappedErr *types.Error + ) if len(tx.Data) != 0 { - methodID := hexutil.Encode(tx.Data[:4]) - tokenTransferMethodID := hexutil.Encode(getMethodID(transferFnSignature)) - if methodID == tokenTransferMethodID { - // Erc20 transfer - toAddress, amountSent, err := parseErc20TransferData(tx.Data) - if err != nil { - return nil, wrapError(errInvalidInput, err) - } + switch hexutil.Encode(tx.Data[:4]) { + case transferMethodID: + ops, checkFrom, wrappedErr = createTransferOps(tx) + case unwrapMethodID: + ops, checkFrom, wrappedErr = createUnwrapOps(tx) + default: + ops, checkFrom, wrappedErr = createGenericContractCallOps(tx) + } + } else { + ops, checkFrom, wrappedErr = createTransferOps(tx) + } + if wrappedErr != nil { + return nil, wrappedErr + } - value = amountSent - opMethod = mapper.OpErc20Transfer - toAddressHex = toAddress.Hex() - } else { - // Generic contract call - value = tx.Value - opMethod = mapper.OpCall - toAddressHex = tx.To + if req.Signed { + return &types.ConstructionParseResponse{ + Operations: ops, + AccountIdentifierSigners: []*types.AccountIdentifier{ + { + Address: *checkFrom, + }, + }, + Metadata: metaMap, + }, nil + } + + return &types.ConstructionParseResponse{ + Operations: ops, + AccountIdentifierSigners: []*types.AccountIdentifier{}, + Metadata: metaMap, + }, nil +} + +func createTransferOps(tx transaction) ([]*types.Operation, *string, *types.Error) { + var ( + opMethod string + value *big.Int + toAddressHex string + ) + + // Erc20 transfer + if len(tx.Data) != 0 { + toAddress, amountSent, err := parseErc20TransferData(tx.Data) + if err != nil { + return nil, nil, WrapError(ErrInvalidInput, err) } + + value = amountSent + opMethod = mapper.OpErc20Transfer + toAddressHex = toAddress.Hex() } else { value = tx.Value opMethod = mapper.OpCall @@ -314,13 +482,16 @@ func (s ConstructionService) ConstructionParse( // Ensure valid from address checkFrom, ok := ChecksumAddress(tx.From) if !ok { - return nil, wrapError(errInvalidInput, fmt.Errorf("%s is not a valid address", tx.From)) + return nil, nil, WrapError( + ErrInvalidInput, + fmt.Errorf("%s is not a valid address", tx.From), + ) } // Ensure valid to address checkTo, ok := ChecksumAddress(toAddressHex) if !ok { - return nil, wrapError(errInvalidInput, fmt.Errorf("%s is not a valid address", tx.To)) + return nil, nil, WrapError(ErrInvalidInput, fmt.Errorf("%s is not a valid address", tx.To)) } ops := []*types.Operation{ @@ -356,35 +527,89 @@ func (s ConstructionService) ConstructionParse( }, }, } + return ops, &checkFrom, nil +} - metadata := &parseMetadata{ - Nonce: tx.Nonce, - GasPrice: tx.GasPrice, - GasLimit: tx.GasLimit, - ChainID: tx.ChainID, - } - metaMap, err := marshalJSONMap(metadata) +func createUnwrapOps(tx transaction) ([]*types.Operation, *string, *types.Error) { + amount, _, err := parseUnwrapData(tx.Data) if err != nil { - return nil, wrapError(errInternalError, err) + return nil, nil, WrapError(ErrInvalidInput, err) } - if req.Signed { - return &types.ConstructionParseResponse{ - Operations: ops, - AccountIdentifierSigners: []*types.AccountIdentifier{ - { - Address: checkFrom, - }, + // Ensure valid from address + checkFrom, ok := ChecksumAddress(tx.From) + if !ok { + return nil, nil, WrapError( + ErrInvalidInput, + fmt.Errorf("%s is not a valid address", tx.From), + ) + } + + ops := []*types.Operation{ + { + Type: mapper.OpErc20Burn, + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, }, - Metadata: metaMap, - }, nil + Account: &types.AccountIdentifier{ + Address: checkFrom, + }, + Amount: &types.Amount{ + Value: new(big.Int).Neg(amount).String(), + Currency: tx.Currency, + }, + }, } + return ops, &checkFrom, nil +} - return &types.ConstructionParseResponse{ - Operations: ops, - AccountIdentifierSigners: []*types.AccountIdentifier{}, - Metadata: metaMap, - }, nil +func createGenericContractCallOps(tx transaction) ([]*types.Operation, *string, *types.Error) { + value := tx.Value + + // Ensure valid from address + checkFrom, ok := ChecksumAddress(tx.From) + if !ok { + return nil, nil, WrapError( + ErrInvalidInput, + fmt.Errorf("%s is not a valid address", tx.From), + ) + } + + // Ensure valid to address + checkTo, ok := ChecksumAddress(tx.To) + if !ok { + return nil, nil, WrapError(ErrInvalidInput, fmt.Errorf("%s is not a valid address", tx.To)) + } + + ops := []*types.Operation{ + { + Type: mapper.OpCall, + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Account: &types.AccountIdentifier{ + Address: checkFrom, + }, + Amount: &types.Amount{ + Value: new(big.Int).Neg(value).String(), + Currency: tx.Currency, + }, + }, + { + Type: mapper.OpCall, + OperationIdentifier: &types.OperationIdentifier{ + Index: 1, + }, + Account: &types.AccountIdentifier{ + Address: checkTo, + }, + Amount: &types.Amount{ + Value: value.String(), + Currency: tx.Currency, + }, + }, + } + return ops, &checkFrom, nil } // ConstructionPayloads implements /construction/payloads endpoint @@ -402,9 +627,59 @@ func (s ConstructionService) ConstructionPayloads( ctx context.Context, req *types.ConstructionPayloadsRequest, ) (*types.ConstructionPayloadsResponse, *types.Error) { - operationDescriptions, err := s.CreateOperationDescription(req.Operations, req.Metadata) + if s.pChainBackend.ShouldHandleRequest(req) { + return s.pChainBackend.ConstructionPayloads(ctx, req) + } + + if s.cChainAtomicTxBackend.ShouldHandleRequest(req) { + return s.cChainAtomicTxBackend.ConstructionPayloads(ctx, req) + } + + var ( + tx *ethtypes.Transaction + unsignedTx *transaction + checkFrom *string + wrappedErr *types.Error + ) + + switch { + case isUnwrapRequest(req.Metadata): + tx, unsignedTx, checkFrom, wrappedErr = s.createUnwrapPayload(req) + case isGenericContractCall(req.Metadata): + tx, unsignedTx, checkFrom, wrappedErr = s.createGenericContractCallPayload(req) + default: + tx, unsignedTx, checkFrom, wrappedErr = s.createTransferPayload(req) + } + if wrappedErr != nil { + return nil, wrappedErr + } + + // Construct SigningPayload + signer := ethtypes.LatestSignerForChainID(s.config.ChainID) + + payload := &types.SigningPayload{ + AccountIdentifier: &types.AccountIdentifier{Address: *checkFrom}, + Bytes: signer.Hash(tx).Bytes(), + SignatureType: types.EcdsaRecovery, + } + + unsignedTxJSON, err := json.Marshal(unsignedTx) + if err != nil { + return nil, WrapError(ErrInternalError, err) + } + + return &types.ConstructionPayloadsResponse{ + UnsignedTransaction: string(unsignedTxJSON), + Payloads: []*types.SigningPayload{payload}, + }, nil +} + +func (s ConstructionService) createTransferPayload( + req *types.ConstructionPayloadsRequest, +) (*ethtypes.Transaction, *transaction, *string, *types.Error) { + operationDescriptions, err := createTransferOperationDescription(req.Operations, req.Metadata) if err != nil { - return nil, wrapError(errInvalidInput, err) + return nil, nil, nil, WrapError(ErrInvalidInput, err.Error()) } descriptions := &parser.Descriptions{ @@ -414,20 +689,11 @@ func (s ConstructionService) ConstructionPayloads( matches, err := parser.MatchOperations(descriptions, req.Operations) if err != nil { - return nil, wrapError(errInvalidInput, "unclear intent") - } - - var metadata metadata - if err := unmarshalJSONMap(req.Metadata, &metadata); err != nil { - return nil, wrapError(errInvalidInput, err) + return nil, nil, nil, WrapError(ErrInvalidInput, "unclear intent") } toOp, amount := matches[1].First() toAddress := toOp.Account.Address - nonce := metadata.Nonce - gasPrice := metadata.GasPrice - gasLimit := metadata.GasLimit - chainID := s.config.ChainID fromOp, _ := matches[0].First() fromAddress := fromOp.Account.Address @@ -435,35 +701,46 @@ func (s ConstructionService) ConstructionPayloads( checkFrom, ok := ChecksumAddress(fromAddress) if !ok { - return nil, wrapError(errInvalidInput, fmt.Errorf("%s is not a valid address", fromAddress)) + return nil, nil, nil, WrapError( + ErrInvalidInput, + fmt.Errorf("%s is not a valid address", fromAddress), + ) } checkTo, ok := ChecksumAddress(toAddress) if !ok { - return nil, wrapError(errInvalidInput, fmt.Errorf("%s is not a valid address", toAddress)) + return nil, nil, nil, WrapError( + ErrInvalidInput, + fmt.Errorf("%s is not a valid address", toAddress), + ) } var transferData []byte - var sendToAddress ethcommon.Address - if utils.Equal(fromCurrency, mapper.FlareCurrency) { - if len(metadata.MethodSignature) == 0 { - transferData = []byte{} - sendToAddress = ethcommon.HexToAddress(checkTo) - } else { - transferData = metadata.Data - sendToAddress = ethcommon.HexToAddress(checkTo) - } + var sendToAddress common.Address + if types.Hash(fromCurrency) == types.Hash(mapper.FlareCurrency) { + transferData = []byte{} + sendToAddress = common.HexToAddress(checkTo) } else { contract, ok := fromCurrency.Metadata[mapper.ContractAddressMetadata].(string) if !ok { - return nil, wrapError(errInvalidInput, + return nil, nil, nil, WrapError(ErrInvalidInput, fmt.Errorf("%s currency doesn't have a contract address in metadata", fromCurrency.Symbol)) } transferData = generateErc20TransferData(toAddress, amount) - sendToAddress = ethcommon.HexToAddress(contract) + sendToAddress = common.HexToAddress(contract) amount = big.NewInt(0) } + var metadata metadata + if err := mapper.UnmarshalJSONMap(req.Metadata, &metadata); err != nil { + return nil, nil, nil, WrapError(ErrInvalidInput, err) + } + + nonce := metadata.Nonce + gasPrice := metadata.GasPrice + gasLimit := metadata.GasLimit + chainID := s.config.ChainID + tx := ethtypes.NewTransaction( nonce, sendToAddress, @@ -484,35 +761,15 @@ func (s ConstructionService) ConstructionPayloads( ChainID: chainID, Currency: fromCurrency, } - - payload := &types.SigningPayload{ - AccountIdentifier: &types.AccountIdentifier{Address: checkFrom}, - Bytes: s.config.Signer().Hash(tx).Bytes(), - SignatureType: types.EcdsaRecovery, - } - - unsignedTxJSON, err := json.Marshal(unsignedTx) - if err != nil { - return nil, wrapError(errInternalError, err) - } - - return &types.ConstructionPayloadsResponse{ - UnsignedTransaction: string(unsignedTxJSON), - Payloads: []*types.SigningPayload{payload}, - }, nil + return tx, unsignedTx, &checkFrom, nil } -// ConstructionPreprocess implements /construction/preprocess endpoint. -// -// Preprocess is called prior to /construction/payloads to construct a request for -// any metadata that is needed for transaction construction given (i.e. account nonce). -func (s ConstructionService) ConstructionPreprocess( - ctx context.Context, - req *types.ConstructionPreprocessRequest, -) (*types.ConstructionPreprocessResponse, *types.Error) { - operationDescriptions, err := s.CreateOperationDescription(req.Operations, req.Metadata) +func (s ConstructionService) createUnwrapPayload( + req *types.ConstructionPayloadsRequest, +) (*ethtypes.Transaction, *transaction, *string, *types.Error) { + operationDescriptions, err := s.CreateUnwrapOperationDescription(req.Operations) if err != nil { - return nil, wrapError(errInvalidInput, err) + return nil, nil, nil, WrapError(ErrInvalidInput, err.Error()) } descriptions := &parser.Descriptions{ @@ -522,67 +779,253 @@ func (s ConstructionService) ConstructionPreprocess( matches, err := parser.MatchOperations(descriptions, req.Operations) if err != nil { - return nil, wrapError(errInvalidInput, "unclear intent") - } - - if len(matches) != 2 { - return nil, wrapError(errInvalidInput, "Must have only two operations") + return nil, nil, nil, WrapError(ErrInvalidInput, "unclear intent") } - fromOp, _ := matches[0].First() + fromOp, amount := matches[0].First() fromAddress := fromOp.Account.Address - toOp, amount := matches[1].First() - toAddress := toOp.Account.Address - fromCurrency := fromOp.Amount.Currency + // op match will return a negative amount since it's from a balance losing funds + amount = new(big.Int).Neg(amount) + checkFrom, ok := ChecksumAddress(fromAddress) if !ok { - return nil, wrapError(errInvalidInput, fmt.Errorf("%s is not a valid address", fromAddress)) + return nil, nil, nil, WrapError( + ErrInvalidInput, + fmt.Errorf("%s is not a valid address", fromAddress), + ) } - checkTo, ok := ChecksumAddress(toAddress) + + contract, ok := fromCurrency.Metadata[mapper.ContractAddressMetadata].(string) if !ok { - return nil, wrapError(errInvalidInput, fmt.Errorf("%s is not a valid address", toAddress)) + return nil, nil, nil, WrapError( + ErrInvalidInput, + fmt.Errorf( + "%s currency doesn't have a contract address in metadata", + fromCurrency.Symbol, + ), + ) } - preprocessOptions := &options{ - From: checkFrom, - To: checkTo, - Value: amount, - SuggestedFeeMultiplier: req.SuggestedFeeMultiplier, - Currency: fromCurrency, + if !mapper.EqualFoldContains(s.config.BridgeTokenList, contract) { + return nil, nil, nil, WrapError( + ErrInvalidInput, + fmt.Errorf( + "%s contract address not in configured list of supported bridge tokens", + contract, + ), + ) } - if v, ok := req.Metadata["gas_price"]; ok { - stringObj, ok := v.(string) - if !ok { - return nil, wrapError(errInvalidInput, fmt.Errorf("%s is not a valid gas price string", v)) - } - bigObj, ok := new(big.Int).SetString(stringObj, 10) - if !ok { - return nil, wrapError(errInvalidInput, fmt.Errorf("%s is not a valid gas price", v)) - } - preprocessOptions.GasPrice = bigObj + unwrapData := generateBridgeUnwrapTransferData(amount, big.NewInt(defaultUnwrapChainID)) + sendToAddress := common.HexToAddress(contract) + + var metadata metadata + if err := mapper.UnmarshalJSONMap(req.Metadata, &metadata); err != nil { + return nil, nil, nil, WrapError(ErrInvalidInput, err) } - if v, ok := req.Metadata["gas_limit"]; ok { - stringObj, ok := v.(string) - if !ok { - return nil, wrapError(errInvalidInput, fmt.Errorf("%s is not a valid gas limit string", v)) - } - bigObj, ok := new(big.Int).SetString(stringObj, 10) - if !ok { - return nil, wrapError(errInvalidInput, fmt.Errorf("%s is not a valid gas limit", v)) - } - preprocessOptions.GasLimit = bigObj + + nonce := metadata.Nonce + gasPrice := metadata.GasPrice + gasLimit := metadata.GasLimit + chainID := s.config.ChainID + + // amount refers to native currency being transferred, which should be zero in the case of an unwrap + amount = big.NewInt(0) + tx := ethtypes.NewTransaction( + nonce, + sendToAddress, + amount, + gasLimit, + gasPrice, + unwrapData, + ) + + unsignedTx := &transaction{ + From: checkFrom, + To: sendToAddress.Hex(), + Value: amount, + Data: tx.Data(), + Nonce: tx.Nonce(), + GasPrice: gasPrice, + GasLimit: tx.Gas(), + ChainID: chainID, + Currency: fromCurrency, + } + return tx, unsignedTx, &checkFrom, nil +} + +func (s ConstructionService) createGenericContractCallPayload(req *types.ConstructionPayloadsRequest) (*ethtypes.Transaction, *transaction, *string, *types.Error) { + operationDescriptions, err := createGenericContractCallOperationDescription(req.Operations) + if err != nil { + return nil, nil, nil, WrapError(ErrInvalidInput, err.Error()) + } + + descriptions := &parser.Descriptions{ + OperationDescriptions: operationDescriptions, + ErrUnmatched: true, + } + + matches, err := parser.MatchOperations(descriptions, req.Operations) + if err != nil { + return nil, nil, nil, WrapError(ErrInvalidInput, "unclear intent") + } + + if len(matches) != 2 { + return nil, nil, nil, WrapError(ErrInvalidInput, "Must have only two operations") + } + + fromOp, amount := matches[0].First() // we check about that [amount] is 0 + fromAddress := fromOp.Account.Address + fromCurrency := fromOp.Amount.Currency + toOp, _ := matches[1].First() + toAddress := toOp.Account.Address + + checkFrom, ok := ChecksumAddress(fromAddress) + if !ok { + return nil, nil, nil, WrapError( + ErrInvalidInput, + fmt.Errorf("%s is not a valid address", fromAddress), + ) + } + checkTo, ok := ChecksumAddress(toAddress) + if !ok { + return nil, nil, nil, WrapError( + ErrInvalidInput, + fmt.Errorf("%s is not a valid address", checkTo), + ) + } + + var metadata metadata + if err := mapper.UnmarshalJSONMap(req.Metadata, &metadata); err != nil { + return nil, nil, nil, WrapError(ErrInvalidInput, err) + } + + sendToAddress := common.HexToAddress(checkTo) + contractData, err := hexutil.Decode(metadata.ContractData) + if err != nil { + return nil, nil, nil, WrapError(ErrInvalidInput, err.Error()) + } + + nonce := metadata.Nonce + gasPrice := metadata.GasPrice + gasLimit := metadata.GasLimit + chainID := s.config.ChainID + + tx := ethtypes.NewTransaction( + nonce, + sendToAddress, + amount, + gasLimit, + gasPrice, + contractData, + ) + + unsignedTx := &transaction{ + From: checkFrom, + To: sendToAddress.Hex(), + Value: amount, + Data: tx.Data(), + Nonce: tx.Nonce(), + GasPrice: gasPrice, + GasLimit: tx.Gas(), + ChainID: chainID, + Currency: fromCurrency, + } + return tx, unsignedTx, &checkFrom, nil +} + +// ConstructionPreprocess implements /construction/preprocess endpoint. +// +// Preprocess is called prior to /construction/payloads to construct a request for +// any metadata that is needed for transaction construction given (i.e. account nonce). +func (s ConstructionService) ConstructionPreprocess( + ctx context.Context, + req *types.ConstructionPreprocessRequest, +) (*types.ConstructionPreprocessResponse, *types.Error) { + if s.pChainBackend.ShouldHandleRequest(req) { + return s.pChainBackend.ConstructionPreprocess(ctx, req) + } + if s.cChainAtomicTxBackend.ShouldHandleRequest(req) { + return s.cChainAtomicTxBackend.ConstructionPreprocess(ctx, req) + } + + var ( + operationDescriptions []*parser.OperationDescription + preprocessOptions *options + err error + terr *types.Error + ) + + switch { + case isUnwrapRequest(req.Metadata): + operationDescriptions, err = s.CreateUnwrapOperationDescription(req.Operations) + if err != nil { + return nil, WrapError(ErrInvalidInput, err.Error()) + } + preprocessOptions, terr = createUnwrapPreprocessOptions(operationDescriptions, req) + if terr != nil { + return nil, terr + } + case isGenericContractCall(req.Metadata): + // To ensure we don't conflict with ERC-20 transfer handling (which are also contract calls), we populate + // the "method_signature" key in metadata (what is used in this check). + operationDescriptions, err = createGenericContractCallOperationDescription(req.Operations) + if err != nil { + return nil, WrapError(ErrInvalidInput, err.Error()) + } + preprocessOptions, terr = createGenericContractCallPreprocessOptions(operationDescriptions, req) + if terr != nil { + return nil, terr + } + default: + operationDescriptions, err = createTransferOperationDescription(req.Operations, req.Metadata) + if err != nil { + return nil, WrapError(ErrInvalidInput, err.Error()) + } + preprocessOptions, terr = createTransferPreprocessOptions(operationDescriptions, req) + if terr != nil { + return nil, terr + } + } + + if v, ok := req.Metadata["gas_price"]; ok { + stringObj, ok := v.(string) + if !ok { + return nil, WrapError( + ErrInvalidInput, + fmt.Errorf("%s is not a valid gas price string", v), + ) + } + bigObj, ok := new(big.Int).SetString(stringObj, 10) + if !ok { + return nil, WrapError(ErrInvalidInput, fmt.Errorf("%s is not a valid gas price", v)) + } + preprocessOptions.GasPrice = bigObj + } + if v, ok := req.Metadata["gas_limit"]; ok { + stringObj, ok := v.(string) + if !ok { + return nil, WrapError( + ErrInvalidInput, + fmt.Errorf("%s is not a valid gas limit string", v), + ) + } + bigObj, ok := new(big.Int).SetString(stringObj, 10) + if !ok { + return nil, WrapError(ErrInvalidInput, fmt.Errorf("%s is not a valid gas limit", v)) + } + preprocessOptions.GasLimit = bigObj } if v, ok := req.Metadata["nonce"]; ok { stringObj, ok := v.(string) if !ok { - return nil, wrapError(errInvalidInput, fmt.Errorf("%s is not a valid nonce string", v)) + return nil, WrapError(ErrInvalidInput, fmt.Errorf("%s is not a valid nonce string", v)) } bigObj, ok := new(big.Int).SetString(stringObj, 10) if !ok { - return nil, wrapError(errInvalidInput, fmt.Errorf("%s is not a valid nonce", v)) + return nil, WrapError(ErrInvalidInput, fmt.Errorf("%s is not a valid nonce", v)) } preprocessOptions.Nonce = bigObj } @@ -590,24 +1033,23 @@ func (s ConstructionService) ConstructionPreprocess( if v, ok := req.Metadata["method_signature"]; ok { methodSigStringObj := v.(string) if !ok { - return nil, wrapError( - errCallInvalidMethod, + return nil, WrapError( + ErrCallInvalidMethod, fmt.Errorf("%s is not a valid signature string", v), ) } data, err := constructContractCallDataGeneric(methodSigStringObj, req.Metadata["method_args"]) if err != nil { - return nil, wrapError(errCallInvalidParams, err) + return nil, WrapError(ErrCallInvalidParams, err) } - preprocessOptions.ContractAddress = checkTo - preprocessOptions.Data = data + preprocessOptions.ContractData = hexutil.Encode(data) preprocessOptions.MethodSignature = methodSigStringObj preprocessOptions.MethodArgs = req.Metadata["method_args"] } - marshaled, err := marshalJSONMap(preprocessOptions) + marshaled, err := mapper.MarshalJSONMap(preprocessOptions) if err != nil { - return nil, wrapError(errInternalError, err) + return nil, WrapError(ErrInternalError, err) } return &types.ConstructionPreprocessResponse{ @@ -623,25 +1065,33 @@ func (s ConstructionService) ConstructionSubmit( req *types.ConstructionSubmitRequest, ) (*types.TransactionIdentifierResponse, *types.Error) { if s.config.IsOfflineMode() { - return nil, errUnavailableOffline + return nil, ErrUnavailableOffline } if len(req.SignedTransaction) == 0 { - return nil, wrapError(errInvalidInput, "signed transaction value is not provided") + return nil, WrapError(ErrInvalidInput, "signed transaction value is not provided") + } + + if s.pChainBackend.ShouldHandleRequest(req) { + return s.pChainBackend.ConstructionSubmit(ctx, req) + } + + if s.cChainAtomicTxBackend.ShouldHandleRequest(req) { + return s.cChainAtomicTxBackend.ConstructionSubmit(ctx, req) } var wrappedTx signedTransactionWrapper if err := json.Unmarshal([]byte(req.SignedTransaction), &wrappedTx); err != nil { - return nil, wrapError(errInvalidInput, err) + return nil, WrapError(ErrInvalidInput, err) } var signedTx ethtypes.Transaction if err := signedTx.UnmarshalJSON(wrappedTx.SignedTransaction); err != nil { - return nil, wrapError(errInvalidInput, err) + return nil, WrapError(ErrInvalidInput, err) } if err := s.client.SendTransaction(ctx, &signedTx); err != nil { - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } return &types.TransactionIdentifierResponse{ @@ -651,25 +1101,30 @@ func (s ConstructionService) ConstructionSubmit( }, nil } -func (s ConstructionService) CreateOperationDescription( +func createTransferOperationDescription( operations []*types.Operation, metadata map[string]interface{}, ) ([]*parser.OperationDescription, error) { if len(operations) != 2 { - return nil, fmt.Errorf("invalid number of operations") + return nil, errors.New("invalid number of operations") } - currency := operations[0].Amount.Currency + firstCurrency := operations[0].Amount.Currency + secondCurrency := operations[1].Amount.Currency + + if firstCurrency == nil || secondCurrency == nil { + return nil, errors.New("invalid currency on operation") + } - if currency == nil || operations[1].Amount.Currency == nil { - return nil, fmt.Errorf("invalid currency on operation") + if types.Hash(firstCurrency) != types.Hash(secondCurrency) { + return nil, errors.New("currency info doesn't match between the operations") } - if !utils.Equal(currency, operations[1].Amount.Currency) { - return nil, fmt.Errorf("currency info doesn't match between the operations") + if types.Hash(firstCurrency) == types.Hash(mapper.FlareCurrency) { + return createOperationDescriptionTransfer(mapper.FlareCurrency, mapper.OpCall, false), nil } - if _, ok := metadata["method_signature"]; ok && utils.Equal(currency, mapper.FlareCurrency) { + if _, ok := metadata["method_signature"]; ok && utils.Equal(firstCurrency, mapper.FlareCurrency) { const base = 10 i := new(big.Int) i.SetString(operations[0].Amount.Value, base) @@ -680,30 +1135,141 @@ func (s ConstructionService) CreateOperationDescription( return nil, fmt.Errorf("for contract call both values should be zero") } } - return s.createOperationDescription(currency, mapper.OpCall, true), nil + return createOperationDescriptionTransfer(firstCurrency, mapper.OpCall, true), nil } - if utils.Equal(currency, mapper.FlareCurrency) { - return s.createOperationDescription(currency, mapper.OpCall, false), nil + // Not Native Avax, we require contractInfo in metadata. + if _, ok := firstCurrency.Metadata[mapper.ContractAddressMetadata].(string); !ok { + return nil, errors.New("non-native currency must have contractAddress in metadata") } - // ERC-20s must have contract address in metadata - if _, ok := currency.Metadata[mapper.ContractAddressMetadata].(string); !ok { - return nil, fmt.Errorf("contractAddress must be populated in currency metadata") + return createOperationDescriptionTransfer(firstCurrency, mapper.OpErc20Transfer, false), nil +} + +func (s ConstructionService) CreateUnwrapOperationDescription( + operations []*types.Operation, +) ([]*parser.OperationDescription, error) { + if len(operations) != 1 { + return nil, errors.New("invalid number of operations") + } + + firstCurrency := operations[0].Amount.Currency + + if types.Hash(firstCurrency) == types.Hash(mapper.FlareCurrency) { + return nil, errors.New("cannot unwrap native avax") } + tokenAddress, firstOk := firstCurrency.Metadata[mapper.ContractAddressMetadata].(string) - return s.createOperationDescription(currency, mapper.OpErc20Transfer, false), nil + // Not Native Avax, we require contractInfo in metadata + if !firstOk { + return nil, errors.New("non-native currency must have contractAddress in metadata") + } + + if !mapper.EqualFoldContains(s.config.BridgeTokenList, tokenAddress) { + return nil, errors.New("only configured bridge tokens may use try to use unwrap function") + } + + return []*parser.OperationDescription{ + { + Type: mapper.OpErc20Burn, + Account: &parser.AccountDescription{ + Exists: true, + }, + Amount: &parser.AmountDescription{ + Exists: true, + Sign: parser.NegativeAmountSign, + Currency: firstCurrency, + }, + }, + }, nil +} + +func createTransferPreprocessOptions( + operationDescriptions []*parser.OperationDescription, + req *types.ConstructionPreprocessRequest, +) (*options, *types.Error) { + descriptions := &parser.Descriptions{ + OperationDescriptions: operationDescriptions, + ErrUnmatched: true, + } + matches, err := parser.MatchOperations(descriptions, req.Operations) + if err != nil { + return nil, WrapError(ErrInvalidInput, "unclear intent") + } + + fromOp, _ := matches[0].First() + fromAddress := fromOp.Account.Address + toOp, amount := matches[1].First() + toAddress := toOp.Account.Address + + fromCurrency := fromOp.Amount.Currency + + checkFrom, ok := ChecksumAddress(fromAddress) + if !ok { + return nil, WrapError(ErrInvalidInput, fmt.Errorf("%s is not a valid address", fromAddress)) + } + checkTo, ok := ChecksumAddress(toAddress) + if !ok { + return nil, WrapError(ErrInvalidInput, fmt.Errorf("%s is not a valid address", toAddress)) + } + + return &options{ + From: checkFrom, + To: checkTo, + Value: amount, + SuggestedFeeMultiplier: req.SuggestedFeeMultiplier, + Currency: fromCurrency, + }, nil } -func (s ConstructionService) createOperationDescription( +func createUnwrapPreprocessOptions( + operationDescriptions []*parser.OperationDescription, + req *types.ConstructionPreprocessRequest, +) (*options, *types.Error) { + descriptions := &parser.Descriptions{ + OperationDescriptions: operationDescriptions, + ErrUnmatched: true, + } + matches, err := parser.MatchOperations(descriptions, req.Operations) + if err != nil { + return nil, WrapError(ErrInvalidInput, "unclear intent") + } + + fromOp, amount := matches[0].First() + fromAddress := fromOp.Account.Address + + // op match will return a negative amount since it's from a balance losing funds + amount = new(big.Int).Neg(amount) + + fromCurrency := fromOp.Amount.Currency + + checkFrom, ok := ChecksumAddress(fromAddress) + if !ok { + return nil, WrapError(ErrInvalidInput, fmt.Errorf("%s is not a valid address", fromAddress)) + } + + metadata := &metadataOptions{ + UnwrapBridgeTx: true, + } + + return &options{ + From: checkFrom, + Value: amount, + SuggestedFeeMultiplier: req.SuggestedFeeMultiplier, + Currency: fromCurrency, + Metadata: metadata, + }, nil +} + +func createOperationDescriptionTransfer( currency *types.Currency, - opType string, + opCode string, isContractCall bool, ) []*parser.OperationDescription { if isContractCall { return []*parser.OperationDescription{ { - Type: opType, + Type: opCode, Account: &parser.AccountDescription{ Exists: true, }, @@ -714,7 +1280,7 @@ func (s ConstructionService) createOperationDescription( }, }, { - Type: opType, + Type: opCode, Account: &parser.AccountDescription{ Exists: true, }, @@ -728,9 +1294,8 @@ func (s ConstructionService) createOperationDescription( } return []*parser.OperationDescription{ - // Send { - Type: opType, + Type: opCode, Account: &parser.AccountDescription{ Exists: true, }, @@ -740,10 +1305,8 @@ func (s ConstructionService) createOperationDescription( Currency: currency, }, }, - - // Receive { - Type: opType, + Type: opCode, Account: &parser.AccountDescription{ Exists: true, }, @@ -757,53 +1320,208 @@ func (s ConstructionService) createOperationDescription( } func (s ConstructionService) getNativeTransferGasLimit( - ctx context.Context, - to string, - from string, - value *big.Int, + ctx context.Context, toAddress string, + fromAddress string, value *big.Int, ) (uint64, error) { - // Guard against malformed inputs that may have been generated using - // a previous version of avalanche-rosetta. - if len(to) == 0 || value == nil { + if len(toAddress) == 0 || value == nil { + // We guard against malformed inputs that may have been generated using + // a previous version of avalanche-rosetta. return nativeTransferGasLimit, nil } - - toAddr := ethcommon.HexToAddress(to) - return s.client.EstimateGas(ctx, interfaces.CallMsg{ - From: ethcommon.HexToAddress(from), - To: &toAddr, + to := common.HexToAddress(toAddress) + gasLimit, err := s.client.EstimateGas(ctx, interfaces.CallMsg{ + From: common.HexToAddress(fromAddress), + To: &to, Value: value, }) + if err != nil { + return 0, err + } + return gasLimit, nil } -// Ref: https://goethereumbook.org/en/transfer-tokens/#set-gas-limit func (s ConstructionService) getErc20TransferGasLimit( + ctx context.Context, toAddress string, + fromAddress string, value *big.Int, currency *types.Currency, +) (uint64, error) { + contract, ok := currency.Metadata[mapper.ContractAddressMetadata] + if len(toAddress) == 0 || value == nil || !ok { + return erc20TransferGasLimit, nil + } + // ToAddress for erc20 transfers is the contract address + contractAddress := common.HexToAddress(contract.(string)) + data := generateErc20TransferData(toAddress, value) + gasLimit, err := s.client.EstimateGas(ctx, interfaces.CallMsg{ + From: common.HexToAddress(fromAddress), + To: &contractAddress, + Data: data, + }) + if err != nil { + return 0, err + } + return gasLimit, nil +} + +func (s ConstructionService) getBridgeUnwrapTransferGasLimit( ctx context.Context, - to string, - from string, + fromAddress string, value *big.Int, currency *types.Currency, ) (uint64, error) { contract, ok := currency.Metadata[mapper.ContractAddressMetadata] - if len(to) == 0 || value == nil || !ok { - return erc20TransferGasLimit, nil + if len(fromAddress) == 0 || value == nil || !ok { + return unwrapGasLimit, nil } + // ToAddress for bridge unwrap is the contract address + contractAddress := common.HexToAddress(contract.(string)) + chainID := big.NewInt(defaultUnwrapChainID) + data := generateBridgeUnwrapTransferData(value, chainID) - contractAddress := ethcommon.HexToAddress(contract.(string)) - return s.client.EstimateGas(ctx, interfaces.CallMsg{ - From: ethcommon.HexToAddress(from), + gasLimit, err := s.client.EstimateGas(ctx, interfaces.CallMsg{ + From: common.HexToAddress(fromAddress), To: &contractAddress, - Data: generateErc20TransferData(to, value), + Data: data, + }) + if err != nil { + return 0, err + } + return gasLimit, nil +} + +func (s ConstructionService) getGenericContractCallGasLimit( + ctx context.Context, + toAddress string, + fromAddress string, + data []byte, +) (uint64, error) { + contractAddress := common.HexToAddress(toAddress) + gasLimit, err := s.client.EstimateGas(ctx, interfaces.CallMsg{ + From: common.HexToAddress(fromAddress), + To: &contractAddress, + Data: data, }) + if err != nil { + return 0, err + } + return gasLimit, nil +} + +func createGenericContractCallPreprocessOptions( + operationDescriptions []*parser.OperationDescription, + req *types.ConstructionPreprocessRequest, +) (*options, *types.Error) { + descriptions := &parser.Descriptions{ + OperationDescriptions: operationDescriptions, + ErrUnmatched: true, + } + + matches, err := parser.MatchOperations(descriptions, req.Operations) + if err != nil { + return nil, WrapError(ErrInvalidInput, "unclear intent") + } + + fromOp, _ := matches[0].First() + fromAddress := fromOp.Account.Address + toOp, amount := matches[1].First() + toAddress := toOp.Account.Address + + fromCurrency := fromOp.Amount.Currency + + checkFrom, ok := ChecksumAddress(fromAddress) + if !ok { + return nil, WrapError(ErrInvalidInput, fmt.Errorf("%s is not a valid address", fromAddress)) + } + checkTo, ok := ChecksumAddress(toAddress) + if !ok { + return nil, WrapError(ErrInvalidInput, fmt.Errorf("%s is not a valid address", toAddress)) + } + + v, ok := req.Metadata["method_signature"] + if !ok { + return nil, WrapError(ErrInvalidInput, errors.New("method_signature is not in metadata")) + } + methodSigStringObj, ok := v.(string) + if !ok { + return nil, WrapError(ErrInvalidInput, fmt.Errorf("%s is not a valid method signature string", v)) + } + data, err := constructContractCallDataGeneric(methodSigStringObj, req.Metadata["method_args"]) + if err != nil { + return nil, WrapError(ErrInvalidInput, err.Error()) + } + + return &options{ + From: checkFrom, + To: checkTo, + Value: amount, + SuggestedFeeMultiplier: req.SuggestedFeeMultiplier, + Currency: fromCurrency, + ContractAddress: checkTo, + ContractData: hexutil.Encode(data), + MethodSignature: methodSigStringObj, + MethodArgs: req.Metadata["method_args"], + }, nil } -// Ref: https://goethereumbook.org/en/transfer-tokens/#forming-the-data-field -func generateErc20TransferData(to string, value *big.Int) []byte { - toAddr := ethcommon.HexToAddress(to) +func createGenericContractCallOperationDescription(operations []*types.Operation) ([]*parser.OperationDescription, error) { + if len(operations) != 2 { + return nil, errors.New("invalid number of operations") + } + + firstCurrency := operations[0].Amount.Currency + secondCurrency := operations[1].Amount.Currency + bigZero := big.NewInt(0) + if firstCurrency == nil || secondCurrency == nil { + return nil, errors.New("invalid currency on operation") + } + if types.Hash(firstCurrency) != types.Hash(secondCurrency) { + return nil, errors.New("from and to currencies are not equal") + } + + i, ok := new(big.Int).SetString(operations[0].Amount.Value, base10) + if !ok { + return nil, errors.New("operation 0 does not have a valid amount") + } + if i.Cmp(bigZero) != 0 { + return nil, errors.New("for generic call both values should be zero") + } + j, ok := new(big.Int).SetString(operations[1].Amount.Value, base10) + if !ok { + return nil, errors.New("operation 1 does not have a valid amount") + } + if j.Cmp(bigZero) != 0 { + return nil, errors.New("for generic call both values should be zero") + } + + return []*parser.OperationDescription{ + { + Type: mapper.OpCall, + Account: &parser.AccountDescription{ + Exists: true, + }, + Amount: &parser.AmountDescription{ + Exists: true, + Sign: parser.AnyAmountSign, + }, + }, + { + Type: mapper.OpCall, + Account: &parser.AccountDescription{ + Exists: true, + }, + Amount: &parser.AmountDescription{ + Exists: true, + Sign: parser.AnyAmountSign, + }, + }, + }, nil +} + +func generateErc20TransferData(toAddress string, value *big.Int) []byte { + to := common.HexToAddress(toAddress) methodID := getMethodID(transferFnSignature) - paddedAddress := ethcommon.LeftPadBytes(toAddr.Bytes(), padLength) - paddedAmount := ethcommon.LeftPadBytes(value.Bytes(), padLength) + paddedAddress := common.LeftPadBytes(to.Bytes(), requiredPaddingBytes) + paddedAmount := common.LeftPadBytes(value.Bytes(), requiredPaddingBytes) var data []byte data = append(data, methodID...) @@ -812,45 +1530,83 @@ func generateErc20TransferData(to string, value *big.Int) []byte { return data } -// Ref: https://goethereumbook.org/en/transfer-tokens/#forming-the-data-field -func parseErc20TransferData(data []byte) (*ethcommon.Address, *big.Int, error) { - if len(data) != transferDataLength { - return nil, nil, fmt.Errorf("incorrect length for data array") +func generateBridgeUnwrapTransferData(value *big.Int, chainID *big.Int) []byte { + methodID := getMethodID(unwrapFnSignature) + + paddedAmount := common.LeftPadBytes(value.Bytes(), requiredPaddingBytes) + paddedChainID := common.LeftPadBytes(chainID.Bytes(), requiredPaddingBytes) + + var data []byte + data = append(data, methodID...) + data = append(data, paddedAmount...) + data = append(data, paddedChainID...) + return data +} + +func parseErc20TransferData(data []byte) (*common.Address, *big.Int, error) { + if len(data) != genericTransferBytesLength { + return nil, nil, errors.New("incorrect length for data array") + } + if hexutil.Encode(data[:4]) != transferMethodID { + return nil, nil, errors.New("incorrect methodID signature") } - methodBytes, addrBytes, amtBytes := data[:4], data[5:36], data[37:] + address := common.BytesToAddress(data[5:36]) + amount := new(big.Int).SetBytes(data[37:]) + return &address, amount, nil +} - if hexutil.Encode(methodBytes) != hexutil.Encode(getMethodID(transferFnSignature)) { - return nil, nil, fmt.Errorf("incorrect methodID signature") +func parseUnwrapData(data []byte) (*big.Int, *big.Int, error) { + if len(data) != genericUnwrapBytesLength { + return nil, nil, errors.New("incorrect length for data array") + } + if hexutil.Encode(data[:4]) != unwrapMethodID { + return nil, nil, errors.New("incorrect methodID signature") } - addr := ethcommon.BytesToAddress(addrBytes) - amt := new(big.Int).SetBytes(amtBytes) - return &addr, amt, nil + amount := new(big.Int).SetBytes(data[5:36]) + chainID := new(big.Int).SetBytes(data[37:]) + + if chainID.Uint64() != 0 { + return nil, nil, errors.New("incorrect chainId value") + } + + return amount, chainID, nil } -// Ref: https://goethereumbook.org/en/transfer-tokens/#forming-the-data-field -func getMethodID(signature string) []byte { - bytes := []byte(signature) - hash := sha3.NewLegacyKeccak256() - hash.Write(bytes) - return hash.Sum(nil)[:4] +func isUnwrapRequest(metadata map[string]interface{}) bool { + if isUnwrap, ok := metadata["bridge_unwrap"]; ok { + return isUnwrap.(bool) + } + return false } -func (s ConstructionService) getGenericContractCallGasLimit( - ctx context.Context, - toAddress string, - fromAddress string, - data []byte, -) (uint64, error) { - contractAddress := ethcommon.HexToAddress(toAddress) - gasLimit, err := s.client.EstimateGas(ctx, interfaces.CallMsg{ - From: ethcommon.HexToAddress(fromAddress), - To: &contractAddress, - Data: data, - }) - if err != nil { - return 0, err +func isGenericContractCall(metadata map[string]interface{}) bool { + if isUnwrap, ok := metadata["bridge_unwrap"]; ok { + unwrapCall, isBool := isUnwrap.(bool) + if !isBool { + panic(fmt.Sprintf("bridge_unwrap value in the metadata must be boolean, got:%s", isUnwrap)) + } + + if unwrapCall { + // If bridge_unwrap flag is true, we can return false right away, otherwise we should + // continue to check the method signature length. + return false + } } - return gasLimit, nil + + if signature, ok := metadata["method_signature"]; ok { + if strSignature, ok := signature.(string); ok { + return len(strSignature) > 0 + } + } + return false +} + +func getMethodID(signature string) []byte { + transferSignature := []byte(signature) + hash := sha3.NewLegacyKeccak256() + hash.Write(transferSignature) + methodID := hash.Sum(nil)[:4] + return methodID } diff --git a/server/service/service_construction_test.go b/server/service/service_construction_test.go index 2844700..ab0ca79 100644 --- a/server/service/service_construction_test.go +++ b/server/service/service_construction_test.go @@ -7,14 +7,16 @@ import ( "math/big" "testing" - "github.com/ava-labs/avalanche-rosetta/mapper" - mocks "github.com/ava-labs/avalanche-rosetta/mocks/client" "github.com/ava-labs/coreth/interfaces" - "github.com/coinbase/rosetta-sdk-go/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/mapper" ) const ( @@ -26,11 +28,17 @@ const ( ) func TestConstructionMetadata(t *testing.T) { - client := &mocks.Client{} + ctrl := gomock.NewController(t) + client := client.NewMockClient(ctrl) ctx := context.Background() + skippedBackend := NewMockConstructionBackend(ctrl) + skippedBackend.EXPECT().ShouldHandleRequest(gomock.Any()).Return(false).AnyTimes() + service := ConstructionService{ - config: &Config{Mode: ModeOnline}, - client: client, + config: &Config{Mode: ModeOnline}, + client: client, + pChainBackend: skippedBackend, + cChainAtomicTxBackend: skippedBackend, } t.Run("unavailable in offline mode", func(t *testing.T) { @@ -44,8 +52,8 @@ func TestConstructionMetadata(t *testing.T) { context.Background(), &types.ConstructionMetadataRequest{}, ) - assert.Nil(t, resp) - assert.Equal(t, errUnavailableOffline.Code, err.Code) + require.Nil(t, resp) + require.Equal(t, ErrUnavailableOffline.Code, err.Code) }) t.Run("requires from address", func(t *testing.T) { @@ -53,31 +61,28 @@ func TestConstructionMetadata(t *testing.T) { context.Background(), &types.ConstructionMetadataRequest{}, ) - assert.Nil(t, resp) - assert.Equal(t, errInvalidInput.Code, err.Code) - assert.Equal(t, "from address is not provided", err.Details["error"]) + require.Nil(t, resp) + require.Equal(t, ErrInvalidInput.Code, err.Code) + require.Equal(t, "from address is not provided", err.Details["error"]) }) t.Run("basic native transfer", func(t *testing.T) { to := common.HexToAddress(defaultToAddress) - client.On( - "NonceAt", + client.EXPECT().NonceAt( ctx, common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), (*big.Int)(nil), ).Return( uint64(0), nil, - ).Once() - client.On( - "SuggestGasPrice", + ) + client.EXPECT().SuggestGasPrice( ctx, ).Return( big.NewInt(1000000000), nil, - ).Once() - client.On( - "EstimateGas", + ) + client.EXPECT().EstimateGas( ctx, interfaces.CallMsg{ From: common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), @@ -87,21 +92,95 @@ func TestConstructionMetadata(t *testing.T) { ).Return( uint64(21001), nil, - ).Once() - input := map[string]interface{}{"from": "0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309", "to": "0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d", "value": "0x9864aac3510d02"} + ) + input := map[string]interface{}{ + "from": "0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309", + "to": "0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d", + "value": "0x9864aac3510d02", + } resp, err := service.ConstructionMetadata( ctx, &types.ConstructionMetadataRequest{ Options: input, }, ) - assert.Nil(t, err) + require.Nil(t, err) metadata := &metadata{ GasPrice: big.NewInt(1000000000), GasLimit: 21_001, Nonce: 0, } - assert.Equal(t, &types.ConstructionMetadataResponse{ + require.Equal(t, &types.ConstructionMetadataResponse{ + Metadata: forceMarshalMap(t, metadata), + SuggestedFee: []*types.Amount{ + { + Value: "21001000000000", + Currency: mapper.FlareCurrency, + }, + }, + }, resp) + }) + t.Run("basic unwrap transfer", func(t *testing.T) { + contractAddress := common.HexToAddress(defaultContractAddress) + client.EXPECT().NonceAt( + ctx, + common.HexToAddress(defaultFromAddress), + (*big.Int)(nil), + ).Return( + uint64(0), + nil, + ) + client.EXPECT().SuggestGasPrice( + ctx, + ).Return( + big.NewInt(1000000000), + nil, + ) + client.EXPECT().EstimateGas( + ctx, + interfaces.CallMsg{ + From: common.HexToAddress(defaultFromAddress), + To: &contractAddress, + Data: common.Hex2Bytes( + "6e28667100000000000000000000000000000000000000000000000000000000b4d360e30000000000000000000000000000000000000000000000000000000000000000", + ), + }, + ).Return( + uint64(21001), + nil, + ) + currencyMetadata := map[string]interface{}{ + "contractAddress": defaultContractAddress, + } + currency := map[string]interface{}{ + "symbol": defaultSymbol, + "decimals": defaultDecimals, + "metadata": currencyMetadata, + } + inputMetadata := map[string]interface{}{ + "bridge_unwrap": true, + } + input := map[string]interface{}{ + "from": defaultFromAddress, + "to": "0x920eb8ca79f07eb3bfc39c324c8113948ed3104c", + "value": "0xb4d360e3", + "currency": currency, + "metadata": inputMetadata, + } + resp, err := service.ConstructionMetadata( + ctx, + &types.ConstructionMetadataRequest{ + Options: input, + }, + ) + require.Nil(t, err) + metadata := &metadata{ + GasPrice: big.NewInt(1000000000), + GasLimit: 21_001, + Nonce: 0, + UnwrapBridgeTx: true, + } + require.Equal(t, &types.ConstructionMetadataResponse{ Metadata: forceMarshalMap(t, metadata), SuggestedFee: []*types.Amount{ { @@ -113,34 +192,33 @@ func TestConstructionMetadata(t *testing.T) { }) t.Run("basic erc20 transfer", func(t *testing.T) { contractAddress := common.HexToAddress(defaultContractAddress) - client.On( - "NonceAt", + client.EXPECT().NonceAt( ctx, common.HexToAddress(defaultFromAddress), (*big.Int)(nil), ).Return( uint64(0), nil, - ).Once() - client.On( - "SuggestGasPrice", + ) + client.EXPECT().SuggestGasPrice( ctx, ).Return( big.NewInt(1000000000), nil, - ).Once() - client.On( - "EstimateGas", + ) + client.EXPECT().EstimateGas( ctx, interfaces.CallMsg{ From: common.HexToAddress(defaultFromAddress), To: &contractAddress, - Data: common.Hex2Bytes("a9059cbb000000000000000000000000920eb8ca79f07eb3bfc39c324c8113948ed3104c00000000000000000000000000000000000000000000000000000000b4d360e3"), + Data: common.Hex2Bytes( + "a9059cbb000000000000000000000000920eb8ca79f07eb3bfc39c324c8113948ed3104c00000000000000000000000000000000000000000000000000000000b4d360e3", + ), }, ).Return( uint64(21001), nil, - ).Once() + ) currencyMetadata := map[string]interface{}{ "contractAddress": defaultContractAddress, } @@ -149,20 +227,25 @@ func TestConstructionMetadata(t *testing.T) { "decimals": defaultDecimals, "metadata": currencyMetadata, } - input := map[string]interface{}{"from": defaultFromAddress, "to": "0x920eb8ca79f07eb3bfc39c324c8113948ed3104c", "value": "0xb4d360e3", "currency": currency} + input := map[string]interface{}{ + "from": defaultFromAddress, + "to": "0x920eb8ca79f07eb3bfc39c324c8113948ed3104c", + "value": "0xb4d360e3", + "currency": currency, + } resp, err := service.ConstructionMetadata( ctx, &types.ConstructionMetadataRequest{ Options: input, }, ) - assert.Nil(t, err) + require.Nil(t, err) metadata := &metadata{ GasPrice: big.NewInt(1000000000), GasLimit: 21_001, Nonce: 0, } - assert.Equal(t, &types.ConstructionMetadataResponse{ + require.Equal(t, &types.ConstructionMetadataResponse{ Metadata: forceMarshalMap(t, metadata), SuggestedFee: []*types.Amount{ { @@ -175,38 +258,45 @@ func TestConstructionMetadata(t *testing.T) { } func TestContructionHash(t *testing.T) { - service := ConstructionService{} + ctrl := gomock.NewController(t) + skippedBackend := NewMockConstructionBackend(ctrl) + skippedBackend.EXPECT().ShouldHandleRequest(gomock.Any()).Return(false).AnyTimes() + + service := ConstructionService{ + pChainBackend: skippedBackend, + cChainAtomicTxBackend: skippedBackend, + } t.Run("no transaction", func(t *testing.T) { resp, err := service.ConstructionHash( context.Background(), &types.ConstructionHashRequest{}, ) - assert.Nil(t, resp) - assert.Equal(t, errInvalidInput.Code, err.Code) - assert.Equal(t, "signed transaction value is not provided", err.Details["error"]) + require.Nil(t, resp) + require.Equal(t, ErrInvalidInput.Code, err.Code) + require.Equal(t, "signed transaction value is not provided", err.Details["error"]) }) t.Run("invalid transaction", func(t *testing.T) { resp, err := service.ConstructionHash(context.Background(), &types.ConstructionHashRequest{ SignedTransaction: "{}", }) - assert.Nil(t, resp) - assert.Equal(t, errInvalidInput.Code, err.Code) + require.Nil(t, resp) + require.Equal(t, ErrInvalidInput.Code, err.Code) }) t.Run("valid transaction", func(t *testing.T) { signed := `{"nonce":"0x6","gasPrice":"0x6d6e2edc00","gas":"0x5208","to":"0x85ad9d1fcf50b72255e4288dca0ad29f5f509409","value":"0xde0b6b3a7640000","input":"0x","v":"0x150f6","r":"0x64d46cc17cbdbcf73b204a6979172eb3148237ecd369181b105e92b0d7fa49a7","s":"0x285063de57245f532a14b13f605bed047a9d20ebfd0db28e01bc8cc9eaac40ee","hash":"0x92ea9280c1653aa9042c7a4d3a608c2149db45064609c18b270c7c73738e2a46"}` request := signedTransactionWrapper{SignedTransaction: []byte(signed), Currency: nil} - json, marshalErr := json.Marshal(request) - assert.Nil(t, marshalErr) + json, err := json.Marshal(request) + require.NoError(t, err) - resp, err := service.ConstructionHash(context.Background(), &types.ConstructionHashRequest{ + resp, terr := service.ConstructionHash(context.Background(), &types.ConstructionHashRequest{ SignedTransaction: string(json), }) - assert.Nil(t, err) - assert.Equal( + require.Nil(t, terr) + require.Equal( t, "0x92ea9280c1653aa9042c7a4d3a608c2149db45064609c18b270c7c73738e2a46", resp.TransactionIdentifier.Hash, @@ -219,8 +309,8 @@ func TestContructionHash(t *testing.T) { resp, err := service.ConstructionHash(context.Background(), &types.ConstructionHashRequest{ SignedTransaction: signed, }) - assert.Nil(t, err) - assert.Equal( + require.Nil(t, err) + require.Equal( t, "0x92ea9280c1653aa9042c7a4d3a608c2149db45064609c18b270c7c73738e2a46", resp.TransactionIdentifier.Hash, @@ -233,22 +323,28 @@ func TestContructionHash(t *testing.T) { resp, err := service.ConstructionHash(context.Background(), &types.ConstructionHashRequest{ SignedTransaction: signed, }) - assert.Contains(t, err.Details["error"].(string), "nonce") - assert.Nil(t, resp) + require.Contains(t, err.Details["error"].(string), "nonce") + require.Nil(t, resp) }) } func TestConstructionDerive(t *testing.T) { - service := ConstructionService{} + ctrl := gomock.NewController(t) + skippedBackend := NewMockConstructionBackend(ctrl) + skippedBackend.EXPECT().ShouldHandleRequest(gomock.Any()).Return(false).AnyTimes() + service := ConstructionService{ + pChainBackend: skippedBackend, + cChainAtomicTxBackend: skippedBackend, + } t.Run("no public key", func(t *testing.T) { resp, err := service.ConstructionDerive( context.Background(), &types.ConstructionDeriveRequest{}, ) - assert.Nil(t, resp) - assert.Equal(t, errInvalidInput.Code, err.Code) - assert.Equal(t, "public key is not provided", err.Details["error"]) + require.Nil(t, resp) + require.Equal(t, ErrInvalidInput.Code, err.Code) + require.Equal(t, "public key is not provided", err.Details["error"]) }) t.Run("invalid public key", func(t *testing.T) { @@ -261,9 +357,9 @@ func TestConstructionDerive(t *testing.T) { }, }, ) - assert.Nil(t, resp) - assert.Equal(t, errInvalidInput.Code, err.Code) - assert.Equal(t, "invalid public key", err.Details["error"]) + require.Nil(t, resp) + require.Equal(t, ErrInvalidInput.Code, err.Code) + require.Equal(t, "invalid public key", err.Details["error"]) }) t.Run("valid public key", func(t *testing.T) { @@ -279,8 +375,8 @@ func TestConstructionDerive(t *testing.T) { }, }, ) - assert.Nil(t, err) - assert.Equal( + require.Nil(t, err) + require.Equal( t, "0x156daFC6e9A1304fD5C9AB686acB4B3c802FE3f7", resp.AccountIdentifier.Address, @@ -289,31 +385,33 @@ func TestConstructionDerive(t *testing.T) { } func forceMarshalMap(t *testing.T, i interface{}) map[string]interface{} { - m, err := marshalJSONMap(i) - if err != nil { - t.Fatalf("could not marshal map %s", types.PrintStruct(i)) - } - + m, err := mapper.MarshalJSONMap(i) + require.NoError(t, err) return m } func TestPreprocessMetadata(t *testing.T) { ctx := context.Background() - client := &mocks.Client{} + ctrl := gomock.NewController(t) + client := client.NewMockClient(ctrl) networkIdentifier := &types.NetworkIdentifier{ Network: "testnet", Blockchain: "flare", } + skippedBackend := NewMockConstructionBackend(ctrl) + skippedBackend.EXPECT().ShouldHandleRequest(gomock.Any()).Return(false).AnyTimes() service := ConstructionService{ - config: &Config{Mode: ModeOnline}, - client: client, + config: &Config{Mode: ModeOnline}, + client: client, + pChainBackend: skippedBackend, + cChainAtomicTxBackend: skippedBackend, } intent := `[{"operation_identifier":{"index":0},"type":"CALL","account":{"address":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"},"amount":{"value":"-42894881044106498","currency":{"symbol":"FLR","decimals":18}}},{"operation_identifier":{"index":1},"type":"CALL","account":{"address":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d"},"amount":{"value":"42894881044106498","currency":{"symbol":"FLR","decimals":18}}}]` t.Run("currency info doesn't match between the operations", func(t *testing.T) { unclearIntent := `[{"operation_identifier":{"index":0},"type":"CALL","account":{"address":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"},"amount":{"value":"-42894881044106498","currency":{"symbol":"FLR","decimals":18}}},{"operation_identifier":{"index":1},"type":"CALL","account":{"address":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d"},"amount":{"value":"42894881044106498","currency":{"symbol":"NOAX","decimals":18}}}]` var ops []*types.Operation - assert.NoError(t, json.Unmarshal([]byte(unclearIntent), &ops)) + require.NoError(t, json.Unmarshal([]byte(unclearIntent), &ops)) preprocessResponse, err := service.ConstructionPreprocess( ctx, &types.ConstructionPreprocessRequest{ @@ -321,12 +419,12 @@ func TestPreprocessMetadata(t *testing.T) { Operations: ops, }, ) - assert.Nil(t, preprocessResponse) - assert.Equal(t, "currency info doesn't match between the operations", err.Details["error"]) + require.Nil(t, preprocessResponse) + require.Equal(t, "currency info doesn't match between the operations", err.Details["error"]) }) t.Run("basic flow", func(t *testing.T) { var ops []*types.Operation - assert.NoError(t, json.Unmarshal([]byte(intent), &ops)) + require.NoError(t, json.Unmarshal([]byte(intent), &ops)) preprocessResponse, err := service.ConstructionPreprocess( ctx, &types.ConstructionPreprocessRequest{ @@ -337,8 +435,8 @@ func TestPreprocessMetadata(t *testing.T) { assert.Nil(t, err) optionsRaw := `{"from":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309","to":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d","value":"0x9864aac3510d02", "currency":{"symbol":"FLR","decimals":18}}` var opt options - assert.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) - assert.Equal(t, &types.ConstructionPreprocessResponse{ + require.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) + require.Equal(t, &types.ConstructionPreprocessResponse{ Options: forceMarshalMap(t, &opt), }, preprocessResponse) @@ -348,16 +446,14 @@ func TestPreprocessMetadata(t *testing.T) { Nonce: 0, } - client.On( - "SuggestGasPrice", + client.EXPECT().SuggestGasPrice( ctx, ).Return( big.NewInt(1000000000), nil, - ).Once() + ) to := common.HexToAddress("0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d") - client.On( - "EstimateGas", + client.EXPECT().EstimateGas( ctx, interfaces.CallMsg{ From: common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), @@ -367,22 +463,24 @@ func TestPreprocessMetadata(t *testing.T) { ).Return( uint64(21001), nil, - ).Once() - client.On( - "NonceAt", + ) + client.EXPECT().NonceAt( ctx, common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), (*big.Int)(nil), ).Return( uint64(0), nil, - ).Once() - metadataResponse, err := service.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{ - NetworkIdentifier: networkIdentifier, - Options: forceMarshalMap(t, &opt), - }) - assert.Nil(t, err) - assert.Equal(t, &types.ConstructionMetadataResponse{ + ) + metadataResponse, err := service.ConstructionMetadata( + ctx, + &types.ConstructionMetadataRequest{ + NetworkIdentifier: networkIdentifier, + Options: forceMarshalMap(t, &opt), + }, + ) + require.Nil(t, err) + require.Equal(t, &types.ConstructionMetadataResponse{ Metadata: forceMarshalMap(t, metadata), SuggestedFee: []*types.Amount{ { @@ -395,11 +493,11 @@ func TestPreprocessMetadata(t *testing.T) { t.Run("basic flow (backwards compatible)", func(t *testing.T) { var ops []*types.Operation - assert.NoError(t, json.Unmarshal([]byte(intent), &ops)) + require.NoError(t, json.Unmarshal([]byte(intent), &ops)) optionsRaw := `{"from":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"}` var opt options - assert.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) + require.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) metadata := &metadata{ GasPrice: big.NewInt(1000000000), @@ -407,28 +505,29 @@ func TestPreprocessMetadata(t *testing.T) { Nonce: 0, } - client.On( - "SuggestGasPrice", + client.EXPECT().SuggestGasPrice( ctx, ).Return( big.NewInt(1000000000), nil, - ).Once() - client.On( - "NonceAt", + ) + client.EXPECT().NonceAt( ctx, common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), (*big.Int)(nil), ).Return( uint64(0), nil, - ).Once() - metadataResponse, err := service.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{ - NetworkIdentifier: networkIdentifier, - Options: forceMarshalMap(t, &opt), - }) - assert.Nil(t, err) - assert.Equal(t, &types.ConstructionMetadataResponse{ + ) + metadataResponse, err := service.ConstructionMetadata( + ctx, + &types.ConstructionMetadataRequest{ + NetworkIdentifier: networkIdentifier, + Options: forceMarshalMap(t, &opt), + }, + ) + require.Nil(t, err) + require.Equal(t, &types.ConstructionMetadataResponse{ Metadata: forceMarshalMap(t, metadata), SuggestedFee: []*types.Amount{ { @@ -441,7 +540,7 @@ func TestPreprocessMetadata(t *testing.T) { t.Run("custom gas price flow", func(t *testing.T) { var ops []*types.Operation - assert.NoError(t, json.Unmarshal([]byte(intent), &ops)) + require.NoError(t, json.Unmarshal([]byte(intent), &ops)) preprocessResponse, err := service.ConstructionPreprocess( ctx, &types.ConstructionPreprocessRequest{ @@ -455,8 +554,8 @@ func TestPreprocessMetadata(t *testing.T) { assert.Nil(t, err) optionsRaw := `{"from":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309","to":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d","value":"0x9864aac3510d02","gas_price":"0x4190ab00", "currency":{"decimals":18, "symbol":"FLR"}}` var opt options - assert.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) - assert.Equal(t, &types.ConstructionPreprocessResponse{ + require.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) + require.Equal(t, &types.ConstructionPreprocessResponse{ Options: forceMarshalMap(t, &opt), }, preprocessResponse) @@ -467,8 +566,7 @@ func TestPreprocessMetadata(t *testing.T) { } to := common.HexToAddress("0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d") - client.On( - "EstimateGas", + client.EXPECT().EstimateGas( ctx, interfaces.CallMsg{ From: common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), @@ -478,22 +576,24 @@ func TestPreprocessMetadata(t *testing.T) { ).Return( uint64(21000), nil, - ).Once() - client.On( - "NonceAt", + ) + client.EXPECT().NonceAt( ctx, common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), (*big.Int)(nil), ).Return( uint64(0), nil, - ).Once() - metadataResponse, err := service.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{ - NetworkIdentifier: networkIdentifier, - Options: forceMarshalMap(t, &opt), - }) - assert.Nil(t, err) - assert.Equal(t, &types.ConstructionMetadataResponse{ + ) + metadataResponse, err := service.ConstructionMetadata( + ctx, + &types.ConstructionMetadataRequest{ + NetworkIdentifier: networkIdentifier, + Options: forceMarshalMap(t, &opt), + }, + ) + require.Nil(t, err) + require.Equal(t, &types.ConstructionMetadataResponse{ Metadata: forceMarshalMap(t, metadata), SuggestedFee: []*types.Amount{ { @@ -506,7 +606,7 @@ func TestPreprocessMetadata(t *testing.T) { t.Run("custom gas price flow (ignore multiplier)", func(t *testing.T) { var ops []*types.Operation - assert.NoError(t, json.Unmarshal([]byte(intent), &ops)) + require.NoError(t, json.Unmarshal([]byte(intent), &ops)) multiplier := float64(1.1) preprocessResponse, err := service.ConstructionPreprocess( ctx, @@ -522,8 +622,8 @@ func TestPreprocessMetadata(t *testing.T) { assert.Nil(t, err) optionsRaw := `{"from":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309","to":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d","value":"0x9864aac3510d02","gas_price":"0x4190ab00","suggested_fee_multiplier":1.1, "currency":{"decimals":18, "symbol":"FLR"}}` var opt options - assert.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) - assert.Equal(t, &types.ConstructionPreprocessResponse{ + require.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) + require.Equal(t, &types.ConstructionPreprocessResponse{ Options: forceMarshalMap(t, &opt), }, preprocessResponse) @@ -534,8 +634,7 @@ func TestPreprocessMetadata(t *testing.T) { } to := common.HexToAddress("0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d") - client.On( - "EstimateGas", + client.EXPECT().EstimateGas( ctx, interfaces.CallMsg{ From: common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), @@ -545,22 +644,24 @@ func TestPreprocessMetadata(t *testing.T) { ).Return( uint64(21000), nil, - ).Once() - client.On( - "NonceAt", + ) + client.EXPECT().NonceAt( ctx, common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), (*big.Int)(nil), ).Return( uint64(0), nil, - ).Once() - metadataResponse, err := service.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{ - NetworkIdentifier: networkIdentifier, - Options: forceMarshalMap(t, &opt), - }) - assert.Nil(t, err) - assert.Equal(t, &types.ConstructionMetadataResponse{ + ) + metadataResponse, err := service.ConstructionMetadata( + ctx, + &types.ConstructionMetadataRequest{ + NetworkIdentifier: networkIdentifier, + Options: forceMarshalMap(t, &opt), + }, + ) + require.Nil(t, err) + require.Equal(t, &types.ConstructionMetadataResponse{ Metadata: forceMarshalMap(t, metadata), SuggestedFee: []*types.Amount{ { @@ -573,7 +674,7 @@ func TestPreprocessMetadata(t *testing.T) { t.Run("fee multiplier", func(t *testing.T) { var ops []*types.Operation - assert.NoError(t, json.Unmarshal([]byte(intent), &ops)) + require.NoError(t, json.Unmarshal([]byte(intent), &ops)) multiplier := float64(1.1) preprocessResponse, err := service.ConstructionPreprocess( ctx, @@ -586,8 +687,8 @@ func TestPreprocessMetadata(t *testing.T) { assert.Nil(t, err) optionsRaw := `{"from":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309","to":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d","value":"0x9864aac3510d02","suggested_fee_multiplier":1.1, "currency":{"decimals":18, "symbol":"FLR"}}` var opt options - assert.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) - assert.Equal(t, &types.ConstructionPreprocessResponse{ + require.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) + require.Equal(t, &types.ConstructionPreprocessResponse{ Options: forceMarshalMap(t, &opt), }, preprocessResponse) @@ -598,8 +699,7 @@ func TestPreprocessMetadata(t *testing.T) { } to := common.HexToAddress("0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d") - client.On( - "EstimateGas", + client.EXPECT().EstimateGas( ctx, interfaces.CallMsg{ From: common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), @@ -609,29 +709,30 @@ func TestPreprocessMetadata(t *testing.T) { ).Return( uint64(21000), nil, - ).Once() - client.On( - "SuggestGasPrice", + ) + client.EXPECT().SuggestGasPrice( ctx, ).Return( big.NewInt(1000000000), nil, - ).Once() - client.On( - "NonceAt", + ) + client.EXPECT().NonceAt( ctx, common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), (*big.Int)(nil), ).Return( uint64(0), nil, - ).Once() - metadataResponse, err := service.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{ - NetworkIdentifier: networkIdentifier, - Options: forceMarshalMap(t, &opt), - }) - assert.Nil(t, err) - assert.Equal(t, &types.ConstructionMetadataResponse{ + ) + metadataResponse, err := service.ConstructionMetadata( + ctx, + &types.ConstructionMetadataRequest{ + NetworkIdentifier: networkIdentifier, + Options: forceMarshalMap(t, &opt), + }, + ) + require.Nil(t, err) + require.Equal(t, &types.ConstructionMetadataResponse{ Metadata: forceMarshalMap(t, metadata), SuggestedFee: []*types.Amount{ { @@ -644,7 +745,7 @@ func TestPreprocessMetadata(t *testing.T) { t.Run("custom nonce", func(t *testing.T) { var ops []*types.Operation - assert.NoError(t, json.Unmarshal([]byte(intent), &ops)) + require.NoError(t, json.Unmarshal([]byte(intent), &ops)) multiplier := float64(1.1) preprocessResponse, err := service.ConstructionPreprocess( ctx, @@ -660,8 +761,8 @@ func TestPreprocessMetadata(t *testing.T) { assert.Nil(t, err) optionsRaw := `{"from":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309","to":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d","value":"0x9864aac3510d02","suggested_fee_multiplier":1.1, "nonce":"0x1", "currency":{"decimals":18, "symbol":"FLR"}}` var opt options - assert.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) - assert.Equal(t, &types.ConstructionPreprocessResponse{ + require.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) + require.Equal(t, &types.ConstructionPreprocessResponse{ Options: forceMarshalMap(t, &opt), }, preprocessResponse) @@ -672,8 +773,7 @@ func TestPreprocessMetadata(t *testing.T) { } to := common.HexToAddress("0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d") - client.On( - "EstimateGas", + client.EXPECT().EstimateGas( ctx, interfaces.CallMsg{ From: common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), @@ -683,20 +783,22 @@ func TestPreprocessMetadata(t *testing.T) { ).Return( uint64(21000), nil, - ).Once() - client.On( - "SuggestGasPrice", + ) + client.EXPECT().SuggestGasPrice( ctx, ).Return( big.NewInt(1000000000), nil, - ).Once() - metadataResponse, err := service.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{ - NetworkIdentifier: networkIdentifier, - Options: forceMarshalMap(t, &opt), - }) - assert.Nil(t, err) - assert.Equal(t, &types.ConstructionMetadataResponse{ + ) + metadataResponse, err := service.ConstructionMetadata( + ctx, + &types.ConstructionMetadataRequest{ + NetworkIdentifier: networkIdentifier, + Options: forceMarshalMap(t, &opt), + }, + ) + require.Nil(t, err) + require.Equal(t, &types.ConstructionMetadataResponse{ Metadata: forceMarshalMap(t, metadata), SuggestedFee: []*types.Amount{ { @@ -709,7 +811,7 @@ func TestPreprocessMetadata(t *testing.T) { t.Run("custom gas limit", func(t *testing.T) { var ops []*types.Operation - assert.NoError(t, json.Unmarshal([]byte(intent), &ops)) + require.NoError(t, json.Unmarshal([]byte(intent), &ops)) multiplier := float64(1.1) preprocessResponse, err := service.ConstructionPreprocess( ctx, @@ -725,8 +827,8 @@ func TestPreprocessMetadata(t *testing.T) { assert.Nil(t, err) optionsRaw := `{"from":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309","to":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d","value":"0x9864aac3510d02","suggested_fee_multiplier":1.1,"gas_limit":"0x9c40", "currency":{"decimals":18, "symbol":"FLR"}}` var opt options - assert.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) - assert.Equal(t, &types.ConstructionPreprocessResponse{ + require.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) + require.Equal(t, &types.ConstructionPreprocessResponse{ Options: forceMarshalMap(t, &opt), }, preprocessResponse) @@ -736,28 +838,29 @@ func TestPreprocessMetadata(t *testing.T) { Nonce: 0, } - client.On( - "SuggestGasPrice", + client.EXPECT().SuggestGasPrice( ctx, ).Return( big.NewInt(1000000000), nil, - ).Once() - client.On( - "NonceAt", + ) + client.EXPECT().NonceAt( ctx, common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), (*big.Int)(nil), ).Return( uint64(0), nil, - ).Once() - metadataResponse, err := service.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{ - NetworkIdentifier: networkIdentifier, - Options: forceMarshalMap(t, &opt), - }) - assert.Nil(t, err) - assert.Equal(t, &types.ConstructionMetadataResponse{ + ) + metadataResponse, err := service.ConstructionMetadata( + ctx, + &types.ConstructionMetadataRequest{ + NetworkIdentifier: networkIdentifier, + Options: forceMarshalMap(t, &opt), + }, + ) + require.Nil(t, err) + require.Equal(t, &types.ConstructionMetadataResponse{ Metadata: forceMarshalMap(t, metadata), SuggestedFee: []*types.Amount{ { @@ -773,20 +876,13 @@ func TestPreprocessMetadata(t *testing.T) { tokenList := []string{defaultContractAddress} service := ConstructionService{ - config: &Config{Mode: ModeOnline, TokenWhiteList: tokenList}, - client: client, + config: &Config{Mode: ModeOnline, TokenWhiteList: tokenList}, + client: client, + pChainBackend: skippedBackend, + cChainAtomicTxBackend: skippedBackend, } - currency := &types.Currency{Symbol: defaultSymbol, Decimals: defaultDecimals} - client.On( - "ContractInfo", - common.HexToAddress(defaultContractAddress), - true, - ).Return( - currency, - nil, - ).Once() var ops []*types.Operation - assert.NoError(t, json.Unmarshal([]byte(erc20Intent), &ops)) + require.NoError(t, json.Unmarshal([]byte(erc20Intent), &ops)) preprocessResponse, err := service.ConstructionPreprocess( ctx, &types.ConstructionPreprocessRequest{ @@ -794,11 +890,11 @@ func TestPreprocessMetadata(t *testing.T) { Operations: ops, }, ) - assert.Nil(t, err) + require.Nil(t, err) optionsRaw := `{"from":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309","to":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d","value":"0x9864aac3510d02", "currency":{"symbol":"TEST","decimals":18, "metadata": {"contractAddress": "0x30e5449b6712Adf4156c8c474250F6eA4400eB82"}}}` var opt options - assert.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) - assert.Equal(t, &types.ConstructionPreprocessResponse{ + require.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) + require.Equal(t, &types.ConstructionPreprocessResponse{ Options: forceMarshalMap(t, &opt), }, preprocessResponse) @@ -808,42 +904,44 @@ func TestPreprocessMetadata(t *testing.T) { Nonce: 0, } - client.On( - "SuggestGasPrice", + client.EXPECT().SuggestGasPrice( ctx, ).Return( big.NewInt(1000000000), nil, - ).Once() + ) contractAddress := common.HexToAddress(defaultContractAddress) - client.On( - "EstimateGas", + client.EXPECT().EstimateGas( ctx, interfaces.CallMsg{ From: common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), To: &contractAddress, - Data: common.Hex2Bytes("a9059cbb00000000000000000000000057B414a0332B5CaB885a451c2a28a07d1e9b8a8d000000000000000000000000000000000000000000000000009864aac3510d02"), + Data: common.Hex2Bytes( + "a9059cbb00000000000000000000000057B414a0332B5CaB885a451c2a28a07d1e9b8a8d000000000000000000000000000000000000000000000000009864aac3510d02", + ), }, ).Return( uint64(21001), nil, - ).Once() - client.On( - "NonceAt", + ) + client.EXPECT().NonceAt( ctx, common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), (*big.Int)(nil), ).Return( uint64(0), nil, - ).Once() + ) - metadataResponse, err := service.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{ - NetworkIdentifier: networkIdentifier, - Options: forceMarshalMap(t, &opt), - }) - assert.Nil(t, err) - assert.Equal(t, &types.ConstructionMetadataResponse{ + metadataResponse, err := service.ConstructionMetadata( + ctx, + &types.ConstructionMetadataRequest{ + NetworkIdentifier: networkIdentifier, + Options: forceMarshalMap(t, &opt), + }, + ) + require.Nil(t, err) + require.Equal(t, &types.ConstructionMetadataResponse{ Metadata: forceMarshalMap(t, metadata), SuggestedFee: []*types.Amount{ { @@ -854,22 +952,203 @@ func TestPreprocessMetadata(t *testing.T) { }, metadataResponse) }) - t.Run("generic contract call flow", func(t *testing.T) { - contractCallIntent := `[{"operation_identifier":{"index":0},"type":"CALL","account":{"address":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"},"amount":{"value":"0","currency":{"symbol":"FLR","decimals":18}}},{"operation_identifier":{"index":1},"type":"CALL","account":{"address":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d"},"amount":{"value":"0","currency":{"symbol":"FLR","decimals":18}}}]` + t.Run("basic unwrap flow", func(t *testing.T) { + unwrapIntent := `[{"operation_identifier":{"index":0},"type":"ERC20_BURN","account":{"address":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"},"amount":{"value":"-42894881044106498","currency":{"symbol":"TEST","decimals":18, "metadata": {"contractAddress": "0x30e5449b6712Adf4156c8c474250F6eA4400eB82"}}}}]` + bridgeTokenList := []string{defaultContractAddress} + skippedBackend := NewMockConstructionBackend(ctrl) + skippedBackend.EXPECT().ShouldHandleRequest(gomock.Any()).Return(false).AnyTimes() + service := ConstructionService{ - config: &Config{Mode: ModeOnline}, - client: client, + config: &Config{ + Mode: ModeOnline, + BridgeTokenList: bridgeTokenList, + }, + client: client, + pChainBackend: skippedBackend, + cChainAtomicTxBackend: skippedBackend, + } + var ops []*types.Operation + require.NoError(t, json.Unmarshal([]byte(unwrapIntent), &ops)) + + requestMetadata := map[string]interface{}{ + "bridge_unwrap": true, + } + preprocessResponse, err := service.ConstructionPreprocess( + ctx, + &types.ConstructionPreprocessRequest{ + NetworkIdentifier: networkIdentifier, + Operations: ops, + Metadata: requestMetadata, + }, + ) + require.Nil(t, err) + optionsRaw := `{"metadata": {"bridge_unwrap":true}, "from":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309","value":"0x9864aac3510d02", "currency":{"symbol":"TEST","decimals":18, "metadata": {"contractAddress": "0x30e5449b6712Adf4156c8c474250F6eA4400eB82"}}}` + var opt options + require.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) + require.Equal(t, &types.ConstructionPreprocessResponse{ + Options: forceMarshalMap(t, &opt), + }, preprocessResponse) + + metadata := &metadata{ + GasPrice: big.NewInt(1000000000), + GasLimit: 21_001, + Nonce: 0, + UnwrapBridgeTx: true, } - currency := &types.Currency{Symbol: defaultSymbol, Decimals: defaultDecimals} - client.On( - "ContractInfo", - common.HexToAddress(defaultContractAddress), - true, + client.EXPECT().SuggestGasPrice( + ctx, ).Return( - currency, + big.NewInt(1000000000), nil, - ).Once() + ) + contractAddress := common.HexToAddress(defaultContractAddress) + client.EXPECT().EstimateGas( + ctx, + interfaces.CallMsg{ + From: common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), + To: &contractAddress, + Data: common.Hex2Bytes( + "6e286671000000000000000000000000000000000000000000000000009864aac3510d020000000000000000000000000000000000000000000000000000000000000000", + ), + }, + ).Return( + uint64(21001), + nil, + ) + client.EXPECT().NonceAt( + ctx, + common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), + (*big.Int)(nil), + ).Return( + uint64(0), + nil, + ) + + metadataResponse, err := service.ConstructionMetadata( + ctx, + &types.ConstructionMetadataRequest{ + NetworkIdentifier: networkIdentifier, + Options: forceMarshalMap(t, &opt), + }, + ) + require.Nil(t, err) + require.Equal(t, &types.ConstructionMetadataResponse{ + Metadata: forceMarshalMap(t, metadata), + SuggestedFee: []*types.Amount{ + { + Value: "21001000000000", + Currency: mapper.FlareCurrency, + }, + }, + }, metadataResponse) + }) + + t.Run("arbitrary contract call flow", func(t *testing.T) { + contractCallIntent := `[{"operation_identifier":{"index":0},"type":"CALL","account":{"address":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"},"amount":{"value":"0","currency":{"symbol":"TEST","decimals":18}}},{"operation_identifier":{"index":1},"type":"CALL","account":{"address":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d"},"amount":{"value":"0","currency":{"symbol":"TEST","decimals":18}}}]` + service := ConstructionService{ + config: &Config{Mode: ModeOnline}, + client: client, + pChainBackend: skippedBackend, + cChainAtomicTxBackend: skippedBackend, + } + + var ops []*types.Operation + require.NoError(t, json.Unmarshal([]byte(contractCallIntent), &ops)) + + requestMetadata := map[string]interface{}{ + "bridge_unwrap": false, + "method_signature": `deploy(bytes32,address,address,address,address)`, + "method_args": []string{"0x3100000000000000000000000000000000000000000000000000000000000000", "0x323e3ab04a3795ad79cc92378fcdb0a0aec51ba5", "0x14e37c2e9cd255404bd35b4542fd9ccaa070aed6", "0x323e3ab04a3795ad79cc92378fcdb0a0aec51ba5", "0x14e37c2e9cd255404bd35b4542fd9ccaa070aed6"}, + } + + preprocessResponse, err := service.ConstructionPreprocess( + ctx, + &types.ConstructionPreprocessRequest{ + NetworkIdentifier: networkIdentifier, + Operations: ops, + Metadata: requestMetadata, + }, + ) + require.Nil(t, err) + + optionsRaw := `{"from":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309","to":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d","value":"0x0", "currency":{"symbol":"TEST","decimals":18}, "contract_address": "0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d", "method_signature": "deploy(bytes32,address,address,address,address)", "data": "0xb0d78b753100000000000000000000000000000000000000000000000000000000000000000000000000000000000000323e3ab04a3795ad79cc92378fcdb0a0aec51ba500000000000000000000000014e37c2e9cd255404bd35b4542fd9ccaa070aed6000000000000000000000000323e3ab04a3795ad79cc92378fcdb0a0aec51ba500000000000000000000000014e37c2e9cd255404bd35b4542fd9ccaa070aed6", "method_args":["0x3100000000000000000000000000000000000000000000000000000000000000","0x323e3ab04a3795ad79cc92378fcdb0a0aec51ba5","0x14e37c2e9cd255404bd35b4542fd9ccaa070aed6","0x323e3ab04a3795ad79cc92378fcdb0a0aec51ba5","0x14e37c2e9cd255404bd35b4542fd9ccaa070aed6"] }` + var opt options + require.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) + require.Equal(t, &types.ConstructionPreprocessResponse{ + Options: forceMarshalMap(t, &opt), + }, preprocessResponse) + + // call metadata API + metadata := &metadata{ + GasPrice: big.NewInt(1000000000), + GasLimit: 21_001, + Nonce: 0, + UnwrapBridgeTx: false, + ContractData: "0xb0d78b753100000000000000000000000000000000000000000000000000000000000000000000000000000000000000323e3ab04a3795ad79cc92378fcdb0a0aec51ba500000000000000000000000014e37c2e9cd255404bd35b4542fd9ccaa070aed6000000000000000000000000323e3ab04a3795ad79cc92378fcdb0a0aec51ba500000000000000000000000014e37c2e9cd255404bd35b4542fd9ccaa070aed6", + MethodSignature: "deploy(bytes32,address,address,address,address)", + MethodArgs: []string{"0x3100000000000000000000000000000000000000000000000000000000000000", "0x323e3ab04a3795ad79cc92378fcdb0a0aec51ba5", "0x14e37c2e9cd255404bd35b4542fd9ccaa070aed6", "0x323e3ab04a3795ad79cc92378fcdb0a0aec51ba5", "0x14e37c2e9cd255404bd35b4542fd9ccaa070aed6"}, + } + + client.EXPECT().SuggestGasPrice( + ctx, + ).Return( + big.NewInt(1000000000), + nil, + ) + contractAddress := common.HexToAddress(defaultToAddress) + data, _ := hexutil.Decode("0xb0d78b753100000000000000000000000000000000000000000000000000000000000000000000000000000000000000323e3ab04a3795ad79cc92378fcdb0a0aec51ba500000000000000000000000014e37c2e9cd255404bd35b4542fd9ccaa070aed6000000000000000000000000323e3ab04a3795ad79cc92378fcdb0a0aec51ba500000000000000000000000014e37c2e9cd255404bd35b4542fd9ccaa070aed6") + client.EXPECT().EstimateGas( + ctx, + interfaces.CallMsg{ + From: common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), + To: &contractAddress, + Data: data, + }, + ).Return( + uint64(21001), + nil, + ) + client.EXPECT().NonceAt( + ctx, + common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), + (*big.Int)(nil), + ).Return( + uint64(0), + nil, + ) + + metadataResponse, err := service.ConstructionMetadata( + ctx, + &types.ConstructionMetadataRequest{ + NetworkIdentifier: networkIdentifier, + Options: forceMarshalMap(t, &opt), + }, + ) + require.Nil(t, err) + require.Equal(t, &types.ConstructionMetadataResponse{ + Metadata: forceMarshalMap(t, metadata), + SuggestedFee: []*types.Amount{ + { + Value: "21001000000000", + Currency: mapper.FlareCurrency, + }, + }, + }, metadataResponse) + }) + + t.Run("generic contract call flow", func(t *testing.T) { + contractCallIntent := `[{"operation_identifier":{"index":0},"type":"CALL","account":{"address":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"},"amount":{"value":"0","currency":{"symbol":"FLR","decimals":18}}},{"operation_identifier":{"index":1},"type":"CALL","account":{"address":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d"},"amount":{"value":"0","currency":{"symbol":"FLR","decimals":18}}}]` + skippedBackend := NewMockConstructionBackend(ctrl) + skippedBackend.EXPECT().ShouldHandleRequest(gomock.Any()).Return(false).AnyTimes() + + service := ConstructionService{ + config: &Config{Mode: ModeOnline}, + client: client, + pChainBackend: skippedBackend, + cChainAtomicTxBackend: skippedBackend, + } + var ops []*types.Operation assert.NoError(t, json.Unmarshal([]byte(contractCallIntent), &ops)) requestMetadata := map[string]interface{}{ @@ -893,45 +1172,43 @@ func TestPreprocessMetadata(t *testing.T) { Options: forceMarshalMap(t, &opt), }, preprocessResponse) - data, _ := hexutil.Decode("0xb0d78b753100000000000000000000000000000000000000000000000000000000000000000000000000000000000000323e3ab04a3795ad79cc92378fcdb0a0aec51ba500000000000000000000000014e37c2e9cd255404bd35b4542fd9ccaa070aed6000000000000000000000000323e3ab04a3795ad79cc92378fcdb0a0aec51ba500000000000000000000000014e37c2e9cd255404bd35b4542fd9ccaa070aed6") + data := "0xb0d78b753100000000000000000000000000000000000000000000000000000000000000000000000000000000000000323e3ab04a3795ad79cc92378fcdb0a0aec51ba500000000000000000000000014e37c2e9cd255404bd35b4542fd9ccaa070aed6000000000000000000000000323e3ab04a3795ad79cc92378fcdb0a0aec51ba500000000000000000000000014e37c2e9cd255404bd35b4542fd9ccaa070aed6" metadata := &metadata{ GasPrice: big.NewInt(1000000000), GasLimit: 21_001, Nonce: 0, - Data: data, + ContractData: data, MethodSignature: "deploy(bytes32,address,address,address,address)", MethodArgs: []string{"0x3100000000000000000000000000000000000000000000000000000000000000", "0x323e3ab04a3795ad79cc92378fcdb0a0aec51ba5", "0x14e37c2e9cd255404bd35b4542fd9ccaa070aed6", "0x323e3ab04a3795ad79cc92378fcdb0a0aec51ba5", "0x14e37c2e9cd255404bd35b4542fd9ccaa070aed6"}, } - client.On( - "SuggestGasPrice", + client.EXPECT().SuggestGasPrice( ctx, ).Return( big.NewInt(1000000000), nil, - ).Once() + ) to := common.HexToAddress("0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d") - client.On( - "EstimateGas", + dataBytes, _ := hexutil.Decode(data) + client.EXPECT().EstimateGas( ctx, interfaces.CallMsg{ From: common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), To: &to, - Data: data, + Data: dataBytes, }, ).Return( uint64(21001), nil, - ).Once() - client.On( - "NonceAt", + ) + client.EXPECT().NonceAt( ctx, common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), (*big.Int)(nil), ).Return( uint64(0), nil, - ).Once() + ) metadataResponse, err := service.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{ NetworkIdentifier: networkIdentifier, Options: forceMarshalMap(t, &opt), @@ -950,20 +1227,16 @@ func TestPreprocessMetadata(t *testing.T) { t.Run("generic contract call flow with encoded args", func(t *testing.T) { contractCallIntent := `[{"operation_identifier":{"index":0},"type":"CALL","account":{"address":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"},"amount":{"value":"0","currency":{"symbol":"FLR","decimals":18}}},{"operation_identifier":{"index":1},"type":"CALL","account":{"address":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d"},"amount":{"value":"0","currency":{"symbol":"FLR","decimals":18}}}]` + skippedBackend := NewMockConstructionBackend(ctrl) + skippedBackend.EXPECT().ShouldHandleRequest(gomock.Any()).Return(false).AnyTimes() + service := ConstructionService{ - config: &Config{Mode: ModeOnline}, - client: client, + config: &Config{Mode: ModeOnline}, + client: client, + pChainBackend: skippedBackend, + cChainAtomicTxBackend: skippedBackend, } - currency := &types.Currency{Symbol: defaultSymbol, Decimals: defaultDecimals} - client.On( - "ContractInfo", - common.HexToAddress(defaultContractAddress), - true, - ).Return( - currency, - nil, - ).Once() var ops []*types.Operation assert.NoError(t, json.Unmarshal([]byte(contractCallIntent), &ops)) requestMetadata := map[string]interface{}{ @@ -987,45 +1260,43 @@ func TestPreprocessMetadata(t *testing.T) { Options: forceMarshalMap(t, &opt), }, preprocessResponse) - data, _ := hexutil.Decode("0x84dc7baa000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001") + data := "0x84dc7baa000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001" metadata := &metadata{ GasPrice: big.NewInt(1000000000), GasLimit: 21_001, Nonce: 0, - Data: data, + ContractData: data, MethodSignature: "cloneRandomDepositWallets(bytes32[])", MethodArgs: "000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001", } - client.On( - "SuggestGasPrice", + client.EXPECT().SuggestGasPrice( ctx, ).Return( big.NewInt(1000000000), nil, - ).Once() + ) to := common.HexToAddress("0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d") - client.On( - "EstimateGas", + dataBytes, _ := hexutil.Decode(data) + client.EXPECT().EstimateGas( ctx, interfaces.CallMsg{ From: common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), To: &to, - Data: data, + Data: dataBytes, }, ).Return( uint64(21001), nil, - ).Once() - client.On( - "NonceAt", + ) + client.EXPECT().NonceAt( ctx, common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), (*big.Int)(nil), ).Return( uint64(0), nil, - ).Once() + ) metadataResponse, err := service.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{ NetworkIdentifier: networkIdentifier, Options: forceMarshalMap(t, &opt), @@ -1042,3 +1313,153 @@ func TestPreprocessMetadata(t *testing.T) { }, metadataResponse) }) } + +func TestBackendDelegations(t *testing.T) { + ctrl := gomock.NewController(t) + + testCases := []string{ + "p-chain", + "c-chain-atomic-tx", + } + + makeBackends := func(currentBackend int) []*MockConstructionBackend { + backends := make([]*MockConstructionBackend, len(testCases)) + for i := range backends { + backends[i] = NewMockConstructionBackend(ctrl) + + if i == currentBackend { + backends[i].EXPECT().ShouldHandleRequest(gomock.Any()).Return(true).Times(8) + break + } + + backends[i].EXPECT().ShouldHandleRequest(gomock.Any()).Return(false).Times(8) + } + return backends + } + + for idx, backendName := range testCases { + backends := makeBackends(idx) + + offlineService := ConstructionService{ + config: &Config{Mode: ModeOffline}, + pChainBackend: backends[0], + cChainAtomicTxBackend: backends[1], + } + + onlineService := ConstructionService{ + config: &Config{Mode: ModeOnline}, + pChainBackend: backends[0], + cChainAtomicTxBackend: backends[1], + } + + t.Run("Derive request is delegated to "+backendName, func(t *testing.T) { + req := &types.ConstructionDeriveRequest{ + PublicKey: &types.PublicKey{}, + } + + expectedResp := &types.ConstructionDeriveResponse{ + AccountIdentifier: &types.AccountIdentifier{ + Address: "P-fuji15f9g0h5xkr5cp47n6u3qxj6yjtzzzrdr23a3tl", + }, + } + + backends[idx].EXPECT().ConstructionDerive(gomock.Any(), req).Return(expectedResp, nil) + resp, err := offlineService.ConstructionDerive(context.Background(), req) + + require.Nil(t, err) + require.Equal(t, expectedResp, resp) + }) + + t.Run("Preprocess request is delegated to "+backendName, func(t *testing.T) { + req := &types.ConstructionPreprocessRequest{} + + expectedResp := &types.ConstructionPreprocessResponse{ + Options: map[string]interface{}{"key": "value"}, + } + + backends[idx].EXPECT().ConstructionPreprocess(gomock.Any(), req).Return(expectedResp, nil) + resp, err := offlineService.ConstructionPreprocess(context.Background(), req) + + require.Nil(t, err) + require.Equal(t, expectedResp, resp) + }) + + t.Run("Metadata request is delegated to "+backendName, func(t *testing.T) { + req := &types.ConstructionMetadataRequest{} + + expectedResp := &types.ConstructionMetadataResponse{ + Metadata: map[string]interface{}{"key": "value"}, + } + + backends[idx].EXPECT().ConstructionMetadata(gomock.Any(), req).Return(expectedResp, nil) + resp, err := onlineService.ConstructionMetadata(context.Background(), req) + + require.Nil(t, err) + require.Equal(t, expectedResp, resp) + }) + + t.Run("Payloads request is delegated to "+backendName, func(t *testing.T) { + req := &types.ConstructionPayloadsRequest{} + + expectedResp := &types.ConstructionPayloadsResponse{UnsignedTransaction: "unsignedtxn"} + + backends[idx].EXPECT().ConstructionPayloads(gomock.Any(), req).Return(expectedResp, nil) + resp, err := offlineService.ConstructionPayloads(context.Background(), req) + + require.Nil(t, err) + require.Equal(t, expectedResp, resp) + }) + + t.Run("Combine request is delegated to "+backendName, func(t *testing.T) { + req := &types.ConstructionCombineRequest{ + UnsignedTransaction: "unsignedtxn", + Signatures: []*types.Signature{{}}, + } + + expectedResp := &types.ConstructionCombineResponse{SignedTransaction: "unsignedtxn"} + + backends[idx].EXPECT().ConstructionCombine(gomock.Any(), req).Return(expectedResp, nil) + resp, err := offlineService.ConstructionCombine(context.Background(), req) + + require.Nil(t, err) + require.Equal(t, expectedResp, resp) + }) + + t.Run("Parse request is delegated to "+backendName, func(t *testing.T) { + req := &types.ConstructionParseRequest{} + expectedResp := &types.ConstructionParseResponse{} + + backends[idx].EXPECT().ConstructionParse(gomock.Any(), req).Return(expectedResp, nil) + resp, err := offlineService.ConstructionParse(context.Background(), req) + + require.Nil(t, err) + require.Equal(t, expectedResp, resp) + }) + + t.Run("Hash request is delegated to "+backendName, func(t *testing.T) { + req := &types.ConstructionHashRequest{SignedTransaction: "signedtxn"} + expectedResp := &types.TransactionIdentifierResponse{ + TransactionIdentifier: &types.TransactionIdentifier{Hash: "txn hash"}, + } + + backends[idx].EXPECT().ConstructionHash(gomock.Any(), req).Return(expectedResp, nil) + resp, err := offlineService.ConstructionHash(context.Background(), req) + + require.Nil(t, err) + require.Equal(t, expectedResp, resp) + }) + + t.Run("Submit request is delegated to "+backendName, func(t *testing.T) { + req := &types.ConstructionSubmitRequest{SignedTransaction: "signedtxn"} + expectedResp := &types.TransactionIdentifierResponse{ + TransactionIdentifier: &types.TransactionIdentifier{Hash: "txn hash"}, + } + + backends[idx].EXPECT().ConstructionSubmit(gomock.Any(), req).Return(expectedResp, nil) + resp, err := onlineService.ConstructionSubmit(context.Background(), req) + + require.Nil(t, err) + require.Equal(t, expectedResp, resp) + }) + } +} diff --git a/server/service/service_mempool.go b/server/service/service_mempool.go index 61d4cf9..6ff7f34 100644 --- a/server/service/service_mempool.go +++ b/server/service/service_mempool.go @@ -27,15 +27,15 @@ func NewMempoolService(config *Config, client client.Client) server.MempoolAPISe // Mempool implements the /mempool endpoint func (s MempoolService) Mempool( ctx context.Context, - req *types.NetworkRequest, + _ *types.NetworkRequest, ) (*types.MempoolResponse, *types.Error) { if s.config.IsOfflineMode() { - return nil, errUnavailableOffline + return nil, ErrUnavailableOffline } content, err := s.client.TxPoolContent(ctx) if err != nil { - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } return &types.MempoolResponse{ @@ -47,9 +47,9 @@ func (s MempoolService) Mempool( } // MempoolTransaction implements the /mempool/transaction endpoint -func (s MempoolService) MempoolTransaction( - ctx context.Context, - req *types.MempoolTransactionRequest, +func (MempoolService) MempoolTransaction( + _ context.Context, + _ *types.MempoolTransactionRequest, ) (*types.MempoolTransactionResponse, *types.Error) { - return nil, errNotImplemented + return nil, ErrNotImplemented } diff --git a/server/service/service_network.go b/server/service/service_network.go index 782715f..8bca36a 100644 --- a/server/service/service_network.go +++ b/server/service/service_network.go @@ -2,7 +2,6 @@ package service import ( "context" - "github.com/ava-labs/avalanchego/ids" "math/big" "github.com/coinbase/rosetta-sdk-go/server" @@ -10,35 +9,60 @@ import ( "github.com/coinbase/rosetta-sdk-go/utils" "github.com/ava-labs/avalanche-rosetta/client" + "github.com/ava-labs/avalanche-rosetta/constants" "github.com/ava-labs/avalanche-rosetta/mapper" ) +// NetworkBackend represents a backend that implements /block family of apis for a subset of requests +// Endpoint handlers in this file delegates requests to corresponding backends based on the request. +// Each backend implements a ShouldHandleRequest method to determine whether that backend should handle the given request. +// +// P-chain support is implemented in pchain.Backend which implements this interface. +// Eventually, the C-chain block logic implemented in this file should be extracted to its own backend as well. +type NetworkBackend interface { + // ShouldHandleRequest returns whether a given request should be handled by this backend + ShouldHandleRequest(req interface{}) bool + // NetworkIdentifier returns the identifier of the network it supports + NetworkIdentifier() *types.NetworkIdentifier + // NetworkStatus implements /network/status endpoint for this backend + NetworkStatus(ctx context.Context, request *types.NetworkRequest) (*types.NetworkStatusResponse, *types.Error) + // NetworkOptions implements /network/options endpoint for this backend + NetworkOptions(ctx context.Context, request *types.NetworkRequest) (*types.NetworkOptionsResponse, *types.Error) +} + // NetworkService implements all /network endpoints type NetworkService struct { - config *Config - client client.Client - genesisBlock *types.Block + config *Config + client client.Client + pChainBackend NetworkBackend + genesisBlock *types.Block } // NewNetworkService returns a new network servicer -func NewNetworkService(config *Config, client client.Client) server.NetworkAPIServicer { +func NewNetworkService( + config *Config, + client client.Client, + pChainBackend NetworkBackend, +) server.NetworkAPIServicer { genesisBlock := makeGenesisBlock(config.GenesisBlockHash) return &NetworkService{ - config: config, - client: client, - genesisBlock: genesisBlock, + config: config, + client: client, + pChainBackend: pChainBackend, + genesisBlock: genesisBlock, } } // NetworkList implements the /network/list endpoint func (s *NetworkService) NetworkList( - ctx context.Context, - request *types.MetadataRequest, + _ context.Context, + _ *types.MetadataRequest, ) (*types.NetworkListResponse, *types.Error) { return &types.NetworkListResponse{ NetworkIdentifiers: []*types.NetworkIdentifier{ s.config.NetworkID, + s.pChainBackend.NetworkIdentifier(), }, }, nil } @@ -49,19 +73,23 @@ func (s *NetworkService) NetworkStatus( request *types.NetworkRequest, ) (*types.NetworkStatusResponse, *types.Error) { if s.config.IsOfflineMode() { - return nil, errUnavailableOffline + return nil, ErrUnavailableOffline + } + + if s.pChainBackend.ShouldHandleRequest(request) { + return s.pChainBackend.NetworkStatus(ctx, request) } // Fetch peers - infoPeers, err := s.client.Peers_v1_11(ctx, []ids.NodeID{}) + infoPeers, err := s.client.Peers(ctx) if err != nil { - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } - peers := mapper.Peers_v1_11(infoPeers) + peers := mapper.Peers(infoPeers) // Check if all C/X chains are ready if err := checkBootstrapStatus(ctx, s.client); err != nil { - if err.Code == errNotReady.Code { + if err.Code == ErrNotReady.Code { return &types.NetworkStatusResponse{ CurrentBlockTimestamp: s.genesisBlock.Timestamp, CurrentBlockIdentifier: s.genesisBlock.BlockIdentifier, @@ -76,19 +104,19 @@ func (s *NetworkService) NetworkStatus( // Fetch the latest block blockHeader, err := s.client.HeaderByNumber(ctx, nil) if err != nil { - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } if blockHeader == nil { - return nil, wrapError(errClientError, "latest block not found") + return nil, WrapError(ErrClientError, "latest block not found") } // Fetch the genesis block genesisHeader, err := s.client.HeaderByNumber(ctx, big.NewInt(0)) if err != nil { - return nil, wrapError(errClientError, err) + return nil, WrapError(ErrClientError, err) } if genesisHeader == nil { - return nil, wrapError(errClientError, "genesis block not found") + return nil, WrapError(ErrClientError, "genesis block not found") } return &types.NetworkStatusResponse{ @@ -111,6 +139,10 @@ func (s *NetworkService) NetworkOptions( ctx context.Context, request *types.NetworkRequest, ) (*types.NetworkOptionsResponse, *types.Error) { + if s.pChainBackend.ShouldHandleRequest(request) { + return s.pChainBackend.NetworkOptions(ctx, request) + } + return &types.NetworkOptionsResponse{ Version: &types.Version{ RosettaVersion: types.RosettaAPIVersion, @@ -128,23 +160,23 @@ func (s *NetworkService) NetworkOptions( } func checkBootstrapStatus(ctx context.Context, client client.Client) *types.Error { - cReady, err := client.IsBootstrapped(ctx, "C") + cReady, err := client.IsBootstrapped(ctx, constants.CChain.String()) if err != nil { - return wrapError(errClientError, err) + return WrapError(ErrClientError, err) } - xReady, err := client.IsBootstrapped(ctx, "X") + xReady, err := client.IsBootstrapped(ctx, constants.XChain.String()) if err != nil { - return wrapError(errClientError, err) + return WrapError(ErrClientError, err) } if !cReady { - return wrapError(errNotReady, "C-Chain is not ready") + return WrapError(ErrNotReady, "C-Chain is not ready") } if !xReady { - return wrapError(errNotReady, "X-Chain is not ready") + return WrapError(ErrNotReady, "X-Chain is not ready") } return nil -} +} \ No newline at end of file diff --git a/server/service/types.go b/server/service/types.go index 6643505..7164f57 100644 --- a/server/service/types.go +++ b/server/service/types.go @@ -5,41 +5,55 @@ import ( "math/big" "strconv" - "github.com/ava-labs/avalanche-rosetta/mapper" "github.com/coinbase/rosetta-sdk-go/types" "github.com/ethereum/go-ethereum/common/hexutil" + + "github.com/ava-labs/avalanche-rosetta/mapper" ) const BalanceOfMethodPrefix = "0x70a08231000000000000000000000000" type options struct { - From string `json:"from"` - To string `json:"to"` - Value *big.Int `json:"value"` - SuggestedFeeMultiplier *float64 `json:"suggested_fee_multiplier,omitempty"` - GasPrice *big.Int `json:"gas_price,omitempty"` - GasLimit *big.Int `json:"gas_limit,omitempty"` - Nonce *big.Int `json:"nonce,omitempty"` - Currency *types.Currency `json:"currency,omitempty"` - ContractAddress string `json:"contract_address,omitempty"` - MethodSignature string `json:"method_signature,omitempty"` - MethodArgs interface{} `json:"method_args,omitempty"` - Data []byte `json:"data,omitempty"` + From string `json:"from"` + To string `json:"to"` + Value *big.Int `json:"value"` + SuggestedFeeMultiplier *float64 `json:"suggested_fee_multiplier,omitempty"` + GasPrice *big.Int `json:"gas_price,omitempty"` + GasLimit *big.Int `json:"gas_limit,omitempty"` + Nonce *big.Int `json:"nonce,omitempty"` + Currency *types.Currency `json:"currency,omitempty"` + Metadata *metadataOptions `json:"metadata,omitempty"` + + // Although [metadataOptions] should be used to specify the following fields, + // we specify it directly on [Options] to maintain compatibility with + // [rosetta-geth-sdk]. + // + // https://github.com/coinbase/rosetta-geth-sdk/blob/8b15156648b518a42ff24241073dc1b7ff21506d/client/types.go#L203-L205 + ContractAddress string `json:"contract_address,omitempty"` + MethodSignature string `json:"method_signature,omitempty"` + MethodArgs interface{} `json:"method_args,omitempty"` + ContractData string `json:"data,omitempty"` } type optionsWire struct { - From string `json:"from"` - To string `json:"to"` - Value string `json:"value"` - SuggestedFeeMultiplier *float64 `json:"suggested_fee_multiplier,omitempty"` - GasPrice string `json:"gas_price,omitempty"` - GasLimit string `json:"gas_limit,omitempty"` - Nonce string `json:"nonce,omitempty"` - Currency *types.Currency `json:"currency,omitempty"` - ContractAddress string `json:"contract_address,omitempty"` - MethodSignature string `json:"method_signature,omitempty"` - MethodArgs interface{} `json:"method_args,omitempty"` - Data string `json:"data,omitempty"` + From string `json:"from"` + To string `json:"to"` + Value string `json:"value"` + SuggestedFeeMultiplier *float64 `json:"suggested_fee_multiplier,omitempty"` + GasPrice string `json:"gas_price,omitempty"` + GasLimit string `json:"gas_limit,omitempty"` + Nonce string `json:"nonce,omitempty"` + Currency *types.Currency `json:"currency,omitempty"` + Metadata *metadataOptions `json:"metadata,omitempty"` + + ContractAddress string `json:"contract_address,omitempty"` + MethodSignature string `json:"method_signature,omitempty"` + MethodArgs interface{} `json:"method_args,omitempty"` + ContractData string `json:"data,omitempty"` +} + +type metadataOptions struct { + UnwrapBridgeTx bool `json:"bridge_unwrap"` } func (o *options) MarshalJSON() ([]byte, error) { @@ -48,10 +62,14 @@ func (o *options) MarshalJSON() ([]byte, error) { To: o.To, SuggestedFeeMultiplier: o.SuggestedFeeMultiplier, Currency: o.Currency, + Metadata: o.Metadata, ContractAddress: o.ContractAddress, MethodSignature: o.MethodSignature, MethodArgs: o.MethodArgs, + ContractData: o.ContractData, } + + // Manually convert any [big.Int] if o.Value != nil { ow.Value = hexutil.EncodeBig(o.Value) } @@ -64,8 +82,8 @@ func (o *options) MarshalJSON() ([]byte, error) { if o.Nonce != nil { ow.Nonce = hexutil.EncodeBig(o.Nonce) } - if len(o.Data) > 0 { - ow.Data = hexutil.Encode(o.Data) + if len(o.ContractData) > 0 { + ow.ContractData = o.ContractData } return json.Marshal(ow) @@ -80,10 +98,13 @@ func (o *options) UnmarshalJSON(data []byte) error { o.To = ow.To o.SuggestedFeeMultiplier = ow.SuggestedFeeMultiplier o.Currency = ow.Currency + o.Metadata = ow.Metadata o.ContractAddress = ow.ContractAddress o.MethodSignature = ow.MethodSignature o.MethodArgs = ow.MethodArgs + o.ContractData = ow.ContractData + // Manually decode any [big.Int] if len(ow.Value) > 0 { value, err := hexutil.DecodeBig(ow.Value) if err != nil { @@ -91,7 +112,6 @@ func (o *options) UnmarshalJSON(data []byte) error { } o.Value = value } - if len(ow.GasPrice) > 0 { gasPrice, err := hexutil.DecodeBig(ow.GasPrice) if err != nil { @@ -99,7 +119,6 @@ func (o *options) UnmarshalJSON(data []byte) error { } o.GasPrice = gasPrice } - if len(ow.GasLimit) > 0 { gasLimit, err := hexutil.DecodeBig(ow.GasLimit) if err != nil { @@ -107,7 +126,6 @@ func (o *options) UnmarshalJSON(data []byte) error { } o.GasLimit = gasLimit } - if len(ow.Nonce) > 0 { nonce, err := hexutil.DecodeBig(ow.Nonce) if err != nil { @@ -116,33 +134,35 @@ func (o *options) UnmarshalJSON(data []byte) error { o.Nonce = nonce } - if len(ow.Data) > 0 { - data, err := hexutil.Decode(ow.Data) - if err != nil { - return err - } - o.Data = data + if len(ow.ContractData) > 0 { + o.ContractData = ow.ContractData } return nil } type metadata struct { - Nonce uint64 `json:"nonce"` - GasPrice *big.Int `json:"gas_price"` - GasLimit uint64 `json:"gas_limit"` + Nonce uint64 `json:"nonce"` + GasPrice *big.Int `json:"gas_price"` + GasLimit uint64 `json:"gas_limit"` + + UnwrapBridgeTx bool `json:"bridge_unwrap"` + + ContractData string `json:"data,omitempty"` MethodSignature string `json:"method_signature,omitempty"` MethodArgs interface{} `json:"method_args,omitempty"` - Data []byte `json:"data,omitempty"` } type metadataWire struct { - Nonce string `json:"nonce"` - GasPrice string `json:"gas_price"` - GasLimit string `json:"gas_limit"` + Nonce string `json:"nonce"` + GasPrice string `json:"gas_price"` + GasLimit string `json:"gas_limit"` + + UnwrapBridgeTx bool `json:"bridge_unwrap"` + + ContractData string `json:"data,omitempty"` MethodSignature string `json:"method_signature,omitempty"` MethodArgs interface{} `json:"method_args,omitempty"` - Data string `json:"data,omitempty"` } func (m *metadata) MarshalJSON() ([]byte, error) { @@ -150,12 +170,14 @@ func (m *metadata) MarshalJSON() ([]byte, error) { Nonce: hexutil.Uint64(m.Nonce).String(), GasPrice: hexutil.EncodeBig(m.GasPrice), GasLimit: hexutil.Uint64(m.GasLimit).String(), + UnwrapBridgeTx: m.UnwrapBridgeTx, + ContractData: m.ContractData, MethodSignature: m.MethodSignature, MethodArgs: m.MethodArgs, } - if len(m.Data) > 0 { - mw.Data = hexutil.Encode(m.Data) + if len(m.ContractData) > 0 { + mw.ContractData = m.ContractData } return json.Marshal(mw) } @@ -166,6 +188,8 @@ func (m *metadata) UnmarshalJSON(data []byte) error { return err } + m.UnwrapBridgeTx = mw.UnwrapBridgeTx + m.ContractData = mw.ContractData m.MethodSignature = mw.MethodSignature m.MethodArgs = mw.MethodArgs @@ -187,12 +211,8 @@ func (m *metadata) UnmarshalJSON(data []byte) error { } m.Nonce = nonce - if len(mw.Data) > 0 { - mwData, err := hexutil.Decode(mw.Data) - if err != nil { - return err - } - m.Data = mwData + if len(mw.ContractData) > 0 { + m.ContractData = mw.ContractData } return nil diff --git a/test-localflare.sh b/test-localflare.sh new file mode 100755 index 0000000..d14a207 --- /dev/null +++ b/test-localflare.sh @@ -0,0 +1,148 @@ +#!/bin/bash + +set -e + +# Configuration +NETWORK_ID=localflare +ROSETTA_IMAGE="${ROSETTA_IMAGE:-rosetta-local}" +START_ROSETTA_SERVER_AFTER_BOOTSTRAP=${START_ROSETTA_SERVER_AFTER_BOOTSTRAP:-false} +MODE="${MODE:-online}" +ROSETTA_PORT=8080 +CI="${CI:-false}" + +if ! command -v npx &> /dev/null; then + log_error "npx command not found. Please install Node.js v20, yarn and npm first." + exit 1 +fi + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_header() { + echo -e "${BLUE}$1${NC}" +} + +cleanup() { + log_info "Cleaning up..." + #docker compose -f server/docker/docker-compose.yml down +} + +trap cleanup EXIT + +# Build Docker image +log_header "Localflare 5-Node Deployment with Rosetta" + +if [ ! $CI ]; then + docker build \ + --progress=plain \ + --build-arg ROSETTA_SRC=. \ + --build-arg ROSETTA_BRANCH=$(git rev-parse --abbrev-ref HEAD) \ + --tag ${ROSETTA_IMAGE} \ + -f ./server/Dockerfile \ + . +fi + +ROSETTA_IMAGE=$ROSETTA_IMAGE START_ROSETTA_SERVER_AFTER_BOOTSTRAP=$START_ROSETTA_SERVER_AFTER_BOOTSTRAP MODE=$MODE docker compose -f server/docker/docker-compose.yml up -d + +while true; do + HEALTHY="$(curl -m 10 -s "http://localhost:9650/ext/health" | jq -r ".healthy")" + if [ "$HEALTHY" != "true" ]; then + echo "Node1 still not healthy" + sleep 10 + else + break + fi +done + +log_info "Localflare cluster is healthy" + +# Bootstrap P-chain using go-flare test scripts +log_info "Bootstrapping P-chain using go-flare test scripts..." + +# Download the test scripts from go-flare repository +rm -rf tmp && mkdir tmp +git clone --depth=1 --branch=v1.11.13-rc0 https://github.com/flare-foundation/go-flare.git tmp/go-flare +npm install -g ts-node +yarn --cwd "tmp/go-flare/test-scripts" +yarn --cwd "tmp/go-flare/test-scripts" run p-chain-import +yarn --cwd "tmp/go-flare/test-scripts" run p-chain-export + +while [[ "$(curl -X POST --data '{ "jsonrpc": "2.0", "method": "platform.getHeight", "params": {}, "id": 1 }' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/P | jq -r .result.height)" -lt "2" ]] +do + echo "Block height not reached.. Block Height:" $(curl -X POST --data '{ "jsonrpc": "2.0", "method": "platform.getHeight", "params": {}, "id": 1 }' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/P | jq -r .result.height) + sleep 1 +done + +# Wait for rosetta to bind to port +while [[ "$(curl -X POST --data '{"metadata": {}}' -H 'content-type:application/json;' 127.0.0.1:8080/network/list | jq -r .network_identifiers[0].network)" != "localflare" ]] +do + echo "Rosetta API not ready yet.." + sleep 5 +done + +log_info "✓ Rosetta is ready!" + +# Test network list endpoint +log_info "Testing /network/list endpoint..." +NETWORK_LIST_RESPONSE=$(curl -s --location --request POST "http://127.0.0.1:${ROSETTA_PORT}/network/list" \ + --header 'Content-Type: application/json' \ + --data-raw '{"metadata":{}}') + +if echo "$NETWORK_LIST_RESPONSE" | jq empty 2>/dev/null; then + log_info "✓ Network list endpoint working" + log_info "Response: $NETWORK_LIST_RESPONSE" +else + log_error "Invalid JSON response from /network/list" + log_error "Response: $NETWORK_LIST_RESPONSE" + exit 1 +fi + +# Install rosetta-cli +log_info "Installing rosetta-cli..." +mkdir -p tmp/rosetta +if ! curl -o tmp/rosetta/install.sh -sSfL https://raw.githubusercontent.com/coinbase/mesh-cli/refs/tags/v0.10.4/scripts/install.sh; then + log_error "Failed to download rosetta-cli installer" + exit 1 +fi +sed -i 's/REPO="rosetta-cli"/REPO="mesh-cli"/g' tmp/rosetta/install.sh +if ! bash tmp/rosetta/install.sh -b tmp/rosetta/ v0.10.4; then + log_error "Failed to install rosetta-cli" + exit 1 +fi +log_info "✓ rosetta-cli installed successfully" + +# Run rosetta-cli check:construction +log_info "Running rosetta-cli check:construction..." + +# Update config.json to use correct port +cat server/rosetta-cli-conf/localflare/config.json | jq ".online_url = \"http://127.0.0.1:${ROSETTA_PORT}\"" > tmp/config-localflare.json +cp server/rosetta-cli-conf/localflare/localflare.ros tmp + +if cd tmp && timeout 5m ./rosetta/rosetta-cli --configuration-file=config-localflare.json check:construction; then + log_info "✓ rosetta-cli check:construction passed" +else + log_error "rosetta-cli check:construction failed" + exit 1 +fi + +cd .. +ROSETTA_IMAGE=$ROSETTA_IMAGE START_ROSETTA_SERVER_AFTER_BOOTSTRAP=$START_ROSETTA_SERVER_AFTER_BOOTSTRAP MODE=$MODE docker compose -f server/docker/docker-compose.yml down + +echo "" +log_info "✅ All tests passed!"