From e8bd4d56851b89a0c2e4b073004dd6ee6b351913 Mon Sep 17 00:00:00 2001 From: dlipicar Date: Fri, 4 Oct 2024 09:55:28 -0300 Subject: [PATCH] chore_: implement eth service for use in integration tests (#5903) --- node/get_status_node.go | 2 + node/status_node_services.go | 10 ++ services/eth/private_api.go | 151 ++++++++++++++++++ services/eth/private_api_nop.go | 12 ++ services/eth/service.go | 36 +++++ tests-functional/config.json | 2 +- .../docker-compose.test.status-go.yml | 6 +- tests-functional/pytest.ini | 1 + tests-functional/tests/test_cases.py | 81 ++++++++-- tests-functional/tests/test_eth_api.py | 53 ++++++ 10 files changed, 337 insertions(+), 17 deletions(-) create mode 100644 services/eth/private_api.go create mode 100644 services/eth/private_api_nop.go create mode 100644 services/eth/service.go create mode 100644 tests-functional/tests/test_eth_api.py diff --git a/node/get_status_node.go b/node/get_status_node.go index 17f5f5e13..0c91fa8fe 100644 --- a/node/get_status_node.go +++ b/node/get_status_node.go @@ -39,6 +39,7 @@ import ( "github.com/status-im/status-go/services/communitytokens" "github.com/status-im/status-go/services/connector" "github.com/status-im/status-go/services/ens" + "github.com/status-im/status-go/services/eth" "github.com/status-im/status-go/services/gif" localnotifications "github.com/status-im/status-go/services/local-notifications" "github.com/status-im/status-go/services/mailservers" @@ -132,6 +133,7 @@ type StatusNode struct { pendingTracker *transactions.PendingTxTracker connectorSrvc *connector.Service appGeneralSrvc *appgeneral.Service + ethSrvc *eth.Service accountsFeed event.Feed walletFeed event.Feed diff --git a/node/status_node_services.go b/node/status_node_services.go index d45ab1fde..c0b69c69b 100644 --- a/node/status_node_services.go +++ b/node/status_node_services.go @@ -40,6 +40,7 @@ import ( "github.com/status-im/status-go/services/communitytokens" "github.com/status-im/status-go/services/connector" "github.com/status-im/status-go/services/ens" + "github.com/status-im/status-go/services/eth" "github.com/status-im/status-go/services/ext" "github.com/status-im/status-go/services/gif" localnotifications "github.com/status-im/status-go/services/local-notifications" @@ -174,6 +175,8 @@ func (b *StatusNode) initServices(config *params.NodeConfig, mediaServer *server } services = append(services, lns) + services = append(services, b.ethService()) + b.peerSrvc.SetDiscoverer(b) for i := range services { @@ -617,6 +620,13 @@ func (b *StatusNode) peerService() *peer.Service { return b.peerSrvc } +func (b *StatusNode) ethService() *eth.Service { + if b.ethSrvc == nil { + b.ethSrvc = eth.NewService(b.rpcClient) + } + return b.ethSrvc +} + func registerWakuMailServer(wakuService *waku.Waku, config *params.WakuConfig) (err error) { var mailServer mailserver.WakuMailServer wakuService.RegisterMailServer(&mailServer) diff --git a/services/eth/private_api.go b/services/eth/private_api.go new file mode 100644 index 000000000..9acb4c301 --- /dev/null +++ b/services/eth/private_api.go @@ -0,0 +1,151 @@ +//go:build enable_private_api + +package eth + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + geth_rpc "github.com/ethereum/go-ethereum/rpc" + + "github.com/status-im/status-go/rpc" +) + +func privateAPIs(client *rpc.Client) (apis []geth_rpc.API) { + return []geth_rpc.API{ + { + Namespace: "ethclient", + Version: "1.0", + Service: NewPrivateAPI(client), + Public: true, + }, + } +} + +type PrivateAPI struct { + client *rpc.Client +} + +func NewPrivateAPI(client *rpc.Client) *PrivateAPI { + return &PrivateAPI{client: client} +} + +type blockResponse struct { + Header *types.Header `json:"header"` + Transactions types.Transactions `json:"transactions"` + Withdrawals types.Withdrawals `json:"withdrawals"` +} + +func newBlockResponse(b *types.Block) *blockResponse { + return &blockResponse{ + Header: b.Header(), + Transactions: b.Transactions(), + Withdrawals: b.Withdrawals(), + } +} + +func (pa *PrivateAPI) BlockByHash(ctx context.Context, chainId uint64, hash common.Hash) (*blockResponse, error) { + client, err := pa.client.EthClient(chainId) + if err != nil { + return nil, err + } + + block, err := client.BlockByHash(ctx, hash) + if err != nil { + return nil, err + } + + return newBlockResponse(block), nil +} + +func (pa *PrivateAPI) BlockByNumber(ctx context.Context, chainId uint64, number *hexutil.Big) (*blockResponse, error) { + client, err := pa.client.EthClient(chainId) + if err != nil { + return nil, err + } + + block, err := client.BlockByNumber(ctx, (*big.Int)(number)) + if err != nil { + return nil, err + } + + return newBlockResponse(block), nil +} + +func (pa *PrivateAPI) HeaderByHash(ctx context.Context, chainId uint64, hash common.Hash) (*types.Header, error) { + client, err := pa.client.EthClient(chainId) + if err != nil { + return nil, err + } + + return client.HeaderByHash(ctx, hash) +} + +func (pa *PrivateAPI) HeaderByNumber(ctx context.Context, chainId uint64, number *hexutil.Big) (*types.Header, error) { + client, err := pa.client.EthClient(chainId) + if err != nil { + return nil, err + } + + return client.HeaderByNumber(ctx, (*big.Int)(number)) +} + +type transactionByHashResponse struct { + Tx *types.Transaction `json:"tx"` + IsPending bool `json:"isPending"` +} + +func (pa *PrivateAPI) TransactionByHash(ctx context.Context, chainId uint64, txHash common.Hash) (*transactionByHashResponse, error) { + + client, err := pa.client.EthClient(chainId) + if err != nil { + return nil, err + } + + tx, isPending, err := client.TransactionByHash(ctx, txHash) + if err != nil { + return nil, err + } + + ret := &transactionByHashResponse{ + Tx: tx, + IsPending: isPending, + } + + return ret, nil +} + +func (pa *PrivateAPI) TransactionReceipt(ctx context.Context, chainId uint64, txHash common.Hash) (*types.Receipt, error) { + client, err := pa.client.EthClient(chainId) + if err != nil { + return nil, err + } + + return client.TransactionReceipt(ctx, txHash) +} + +func (pa *PrivateAPI) SuggestGasPrice(ctx context.Context, chainId uint64) (*hexutil.Big, error) { + client, err := pa.client.EthClient(chainId) + if err != nil { + return nil, err + } + + ret, err := client.SuggestGasPrice(ctx) + if err != nil { + return nil, err + } + + return (*hexutil.Big)(ret), nil +} + +func (pa *PrivateAPI) BlockNumber(ctx context.Context, chainId uint64) (uint64, error) { + client, err := pa.client.EthClient(chainId) + if err != nil { + return 0, err + } + + return client.BlockNumber(ctx) +} diff --git a/services/eth/private_api_nop.go b/services/eth/private_api_nop.go new file mode 100644 index 000000000..3a0b8e070 --- /dev/null +++ b/services/eth/private_api_nop.go @@ -0,0 +1,12 @@ +//go:build !enable_private_api + +package eth + +import ( + geth_rpc "github.com/ethereum/go-ethereum/rpc" + "github.com/status-im/status-go/rpc" +) + +func privateAPIs(*rpc.Client) (apis []geth_rpc.API) { + return nil +} diff --git a/services/eth/service.go b/services/eth/service.go new file mode 100644 index 000000000..abe4c1f8b --- /dev/null +++ b/services/eth/service.go @@ -0,0 +1,36 @@ +package eth + +import ( + "github.com/ethereum/go-ethereum/p2p" + geth_rpc "github.com/ethereum/go-ethereum/rpc" + + rpc_client "github.com/status-im/status-go/rpc" +) + +type Service struct { + rpcClient *rpc_client.Client +} + +func NewService( + rpcClient *rpc_client.Client, +) *Service { + return &Service{ + rpcClient: rpcClient, + } +} + +func (s *Service) APIs() []geth_rpc.API { + return privateAPIs(s.rpcClient) +} + +func (s *Service) Protocols() []p2p.Protocol { + return nil +} + +func (s *Service) Start() error { + return nil +} + +func (s *Service) Stop() error { + return nil +} diff --git a/tests-functional/config.json b/tests-functional/config.json index 387ba06b9..5834c973e 100644 --- a/tests-functional/config.json +++ b/tests-functional/config.json @@ -8,7 +8,7 @@ "HTTPHost": "0.0.0.0", "HTTPPort": 3333, "HTTPVirtualHosts": ["*", "status-go"], - "APIModules": "eth,admin,wallet,accounts,waku,wakuext", + "APIModules": "eth,admin,wallet,accounts,waku,wakuext,ethclient", "WalletConfig": { "Enabled": true }, diff --git a/tests-functional/docker-compose.test.status-go.yml b/tests-functional/docker-compose.test.status-go.yml index 8df073f79..4c81ecabe 100644 --- a/tests-functional/docker-compose.test.status-go.yml +++ b/tests-functional/docker-compose.test.status-go.yml @@ -5,7 +5,7 @@ services: context: ../ dockerfile: _assets/build/Dockerfile args: - build_tags: gowaku_no_rln + build_tags: gowaku_no_rln,enable_private_api build_target: statusd build_flags: -cover entrypoint: [ @@ -37,7 +37,7 @@ services: context: ../ dockerfile: _assets/build/Dockerfile args: - build_tags: gowaku_no_rln + build_tags: gowaku_no_rln,enable_private_api build_target: statusd build_flags: -cover entrypoint: [ @@ -72,7 +72,7 @@ services: dockerfile: Dockerfile.tests-rpc entrypoint: [ "pytest", - "-m", "wallet", + "-m", "wallet or ethclient", "--rpc_url=http://status-go:3333", "--rpc_url_2=http://status-go-no-funds:3333", "--anvil_url=http://anvil:8545", diff --git a/tests-functional/pytest.ini b/tests-functional/pytest.ini index 8af58c3f3..f198ae66e 100644 --- a/tests-functional/pytest.ini +++ b/tests-functional/pytest.ini @@ -10,3 +10,4 @@ markers = tx wakuext accounts + ethclient diff --git a/tests-functional/tests/test_cases.py b/tests-functional/tests/test_cases.py index 338e935cd..e4f057973 100644 --- a/tests-functional/tests/test_cases.py +++ b/tests-functional/tests/test_cases.py @@ -6,12 +6,14 @@ import jsonschema import time import requests from conftest import option, user_1, user_2 - +import pytest +from collections import namedtuple class RpcTestCase: + network_id = 31337 def setup_method(self): - self.network_id = 31337 + pass def _try_except_JSONDecodeError_KeyError(self, response, key: str): try: @@ -56,13 +58,19 @@ class RpcTestCase: return response + def rpc_valid_request(self, method, params=[], _id=None, client=None, url=None): + response = self.rpc_request(method, params, _id, client, url) + self.verify_is_valid_json_rpc_response(response, _id) + return response + def verify_json_schema(self, response, method): with open(f"{option.base_dir}/schemas/{method}", "r") as schema: jsonschema.validate(instance=response.json(), schema=json.load(schema)) - -class TransactionTestCase(RpcTestCase): +class WalletTestCase(RpcTestCase): + def setup_method(self): + super().setup_method() def wallet_create_multi_transaction(self, **kwargs): method = "wallet_createMultiTransaction" @@ -88,7 +96,7 @@ class TransactionTestCase(RpcTestCase): "fromAddress": user_1.address, "fromAmount": "0x5af3107a4000", "fromAsset": "ETH", - "multiTxType": "MultiTransactionSend", + "type": 0, # MultiTransactionSend "toAddress": user_2.address, "toAsset": "ETH", }, @@ -101,19 +109,66 @@ class TransactionTestCase(RpcTestCase): ], f"{option.password}", ] - response = self.rpc_request(method, params, 13) - return response + return self.rpc_request(method, params) + def send_valid_multi_transaction(self, **kwargs): + response = self.wallet_create_multi_transaction(**kwargs) + + tx_hash = None + self.verify_is_valid_json_rpc_response(response) + try: + tx_hash = response.json( + )["result"]["hashes"][str(self.network_id)][0] + except (KeyError, json.JSONDecodeError): + raise Exception(response.content) + return tx_hash + +class TransactionTestCase(WalletTestCase): def setup_method(self): super().setup_method() - response = self.wallet_create_multi_transaction() - try: - self.tx_hash = response.json( - )["result"]["hashes"][str(self.network_id)][0] - except (KeyError, json.JSONDecodeError): - raise Exception(response.content) + self.tx_hash = self.send_valid_multi_transaction() +class EthApiTestCase(WalletTestCase): + @pytest.fixture(autouse=True, scope='class') + def tx_data(self): + tx_hash = self.send_valid_multi_transaction() + self.wait_until_tx_not_pending(tx_hash) + + receipt = self.get_transaction_receipt(tx_hash) + try: + block_number = receipt.json()["result"]["blockNumber"] + block_hash = receipt.json()["result"]["blockHash"] + except (KeyError, json.JSONDecodeError): + raise Exception(receipt.content) + + TxData = namedtuple("TxData", ["tx_hash", "block_number", "block_hash"]) + return TxData(tx_hash, block_number, block_hash) + + def get_block_header(self, block_number): + method = "ethclient_headerByNumber" + params = [self.network_id, block_number] + return self.rpc_valid_request(method, params) + + def get_transaction_receipt(self, tx_hash): + method = "ethclient_transactionReceipt" + params = [self.network_id, tx_hash] + return self.rpc_valid_request(method, params) + + def wait_until_tx_not_pending(self, tx_hash, timeout=10): + method = "ethclient_transactionByHash" + params = [self.network_id, tx_hash] + response = self.rpc_valid_request(method, params) + + start_time = time.time() + while response.json()["result"]["isPending"] == True: + time_passed = time.time() - start_time + if time_passed >= timeout: + raise TimeoutError( + f"Tx {tx_hash} is still pending after {timeout} seconds") + time.sleep(0.5) + response = self.rpc_valid_request(method, params) + return response.json()["result"]["tx"] class SignalTestCase(RpcTestCase): diff --git a/tests-functional/tests/test_eth_api.py b/tests-functional/tests/test_eth_api.py new file mode 100644 index 000000000..aeb1aa426 --- /dev/null +++ b/tests-functional/tests/test_eth_api.py @@ -0,0 +1,53 @@ +import pytest +from conftest import option +from test_cases import EthApiTestCase + +def validateHeader(header, block_number, block_hash): + assert header["number"] == block_number + assert header["hash"] == block_hash + +def validateBlock(block, block_number, block_hash, expected_tx_hash): + validateHeader(block["header"], block_number, block_hash) + tx_hashes = [tx["hash"] for tx in block["transactions"]] + assert expected_tx_hash in tx_hashes + +def validateTransaction(tx, tx_hash): + assert tx["tx"]["hash"] == tx_hash + +def validateReceipt(receipt, tx_hash, block_number, block_hash): + assert receipt["transactionHash"] == tx_hash + assert receipt["blockNumber"] == block_number + assert receipt["blockHash"] == block_hash + +@pytest.mark.rpc +@pytest.mark.ethclient +class TestRpc(EthApiTestCase): + def test_block_number(self): + self.rpc_valid_request("ethclient_blockNumber", [self.network_id]) + + def test_suggest_gas_price(self): + self.rpc_valid_request("ethclient_suggestGasPrice", [self.network_id]) + + def test_header_by_number(self, tx_data): + response = self.rpc_valid_request("ethclient_headerByNumber", [self.network_id, tx_data.block_number]) + validateHeader(response.json()["result"], tx_data.block_number, tx_data.block_hash) + + def test_block_by_number(self, tx_data): + response = self.rpc_valid_request("ethclient_blockByNumber", [self.network_id, tx_data.block_number]) + validateBlock(response.json()["result"], tx_data.block_number, tx_data.block_hash, tx_data.tx_hash) + + def test_header_by_hash(self, tx_data): + response = self.rpc_valid_request("ethclient_headerByHash", [self.network_id, tx_data.block_hash]) + validateHeader(response.json()["result"], tx_data.block_number, tx_data.block_hash) + + def test_block_by_hash(self, tx_data): + response = self.rpc_valid_request("ethclient_blockByHash", [self.network_id, tx_data.block_hash]) + validateBlock(response.json()["result"], tx_data.block_number, tx_data.block_hash, tx_data.tx_hash) + + def test_transaction_by_hash(self, tx_data): + response = self.rpc_valid_request("ethclient_transactionByHash", [self.network_id, tx_data.tx_hash]) + validateTransaction(response.json()["result"], tx_data.tx_hash) + + def test_transaction_receipt(self, tx_data): + response = self.rpc_valid_request("ethclient_transactionReceipt", [self.network_id, tx_data.tx_hash]) + validateReceipt(response.json()["result"], tx_data.tx_hash, tx_data.block_number, tx_data.block_hash)