chore_: implement eth service for use in integration tests (#5903)

This commit is contained in:
dlipicar 2024-10-04 09:55:28 -03:00 committed by GitHub
parent c1dd9397f7
commit e8bd4d5685
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 337 additions and 17 deletions

View File

@ -39,6 +39,7 @@ import (
"github.com/status-im/status-go/services/communitytokens" "github.com/status-im/status-go/services/communitytokens"
"github.com/status-im/status-go/services/connector" "github.com/status-im/status-go/services/connector"
"github.com/status-im/status-go/services/ens" "github.com/status-im/status-go/services/ens"
"github.com/status-im/status-go/services/eth"
"github.com/status-im/status-go/services/gif" "github.com/status-im/status-go/services/gif"
localnotifications "github.com/status-im/status-go/services/local-notifications" localnotifications "github.com/status-im/status-go/services/local-notifications"
"github.com/status-im/status-go/services/mailservers" "github.com/status-im/status-go/services/mailservers"
@ -132,6 +133,7 @@ type StatusNode struct {
pendingTracker *transactions.PendingTxTracker pendingTracker *transactions.PendingTxTracker
connectorSrvc *connector.Service connectorSrvc *connector.Service
appGeneralSrvc *appgeneral.Service appGeneralSrvc *appgeneral.Service
ethSrvc *eth.Service
accountsFeed event.Feed accountsFeed event.Feed
walletFeed event.Feed walletFeed event.Feed

View File

@ -40,6 +40,7 @@ import (
"github.com/status-im/status-go/services/communitytokens" "github.com/status-im/status-go/services/communitytokens"
"github.com/status-im/status-go/services/connector" "github.com/status-im/status-go/services/connector"
"github.com/status-im/status-go/services/ens" "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/ext"
"github.com/status-im/status-go/services/gif" "github.com/status-im/status-go/services/gif"
localnotifications "github.com/status-im/status-go/services/local-notifications" 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, lns)
services = append(services, b.ethService())
b.peerSrvc.SetDiscoverer(b) b.peerSrvc.SetDiscoverer(b)
for i := range services { for i := range services {
@ -617,6 +620,13 @@ func (b *StatusNode) peerService() *peer.Service {
return b.peerSrvc 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) { func registerWakuMailServer(wakuService *waku.Waku, config *params.WakuConfig) (err error) {
var mailServer mailserver.WakuMailServer var mailServer mailserver.WakuMailServer
wakuService.RegisterMailServer(&mailServer) wakuService.RegisterMailServer(&mailServer)

151
services/eth/private_api.go Normal file
View File

@ -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)
}

View File

@ -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
}

36
services/eth/service.go Normal file
View File

@ -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
}

View File

@ -8,7 +8,7 @@
"HTTPHost": "0.0.0.0", "HTTPHost": "0.0.0.0",
"HTTPPort": 3333, "HTTPPort": 3333,
"HTTPVirtualHosts": ["*", "status-go"], "HTTPVirtualHosts": ["*", "status-go"],
"APIModules": "eth,admin,wallet,accounts,waku,wakuext", "APIModules": "eth,admin,wallet,accounts,waku,wakuext,ethclient",
"WalletConfig": { "WalletConfig": {
"Enabled": true "Enabled": true
}, },

View File

@ -5,7 +5,7 @@ services:
context: ../ context: ../
dockerfile: _assets/build/Dockerfile dockerfile: _assets/build/Dockerfile
args: args:
build_tags: gowaku_no_rln build_tags: gowaku_no_rln,enable_private_api
build_target: statusd build_target: statusd
build_flags: -cover build_flags: -cover
entrypoint: [ entrypoint: [
@ -37,7 +37,7 @@ services:
context: ../ context: ../
dockerfile: _assets/build/Dockerfile dockerfile: _assets/build/Dockerfile
args: args:
build_tags: gowaku_no_rln build_tags: gowaku_no_rln,enable_private_api
build_target: statusd build_target: statusd
build_flags: -cover build_flags: -cover
entrypoint: [ entrypoint: [
@ -72,7 +72,7 @@ services:
dockerfile: Dockerfile.tests-rpc dockerfile: Dockerfile.tests-rpc
entrypoint: [ entrypoint: [
"pytest", "pytest",
"-m", "wallet", "-m", "wallet or ethclient",
"--rpc_url=http://status-go:3333", "--rpc_url=http://status-go:3333",
"--rpc_url_2=http://status-go-no-funds:3333", "--rpc_url_2=http://status-go-no-funds:3333",
"--anvil_url=http://anvil:8545", "--anvil_url=http://anvil:8545",

View File

@ -10,3 +10,4 @@ markers =
tx tx
wakuext wakuext
accounts accounts
ethclient

View File

@ -6,12 +6,14 @@ import jsonschema
import time import time
import requests import requests
from conftest import option, user_1, user_2 from conftest import option, user_1, user_2
import pytest
from collections import namedtuple
class RpcTestCase: class RpcTestCase:
network_id = 31337
def setup_method(self): def setup_method(self):
self.network_id = 31337 pass
def _try_except_JSONDecodeError_KeyError(self, response, key: str): def _try_except_JSONDecodeError_KeyError(self, response, key: str):
try: try:
@ -56,13 +58,19 @@ class RpcTestCase:
return response 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): def verify_json_schema(self, response, method):
with open(f"{option.base_dir}/schemas/{method}", "r") as schema: with open(f"{option.base_dir}/schemas/{method}", "r") as schema:
jsonschema.validate(instance=response.json(), jsonschema.validate(instance=response.json(),
schema=json.load(schema)) schema=json.load(schema))
class WalletTestCase(RpcTestCase):
class TransactionTestCase(RpcTestCase): def setup_method(self):
super().setup_method()
def wallet_create_multi_transaction(self, **kwargs): def wallet_create_multi_transaction(self, **kwargs):
method = "wallet_createMultiTransaction" method = "wallet_createMultiTransaction"
@ -88,7 +96,7 @@ class TransactionTestCase(RpcTestCase):
"fromAddress": user_1.address, "fromAddress": user_1.address,
"fromAmount": "0x5af3107a4000", "fromAmount": "0x5af3107a4000",
"fromAsset": "ETH", "fromAsset": "ETH",
"multiTxType": "MultiTransactionSend", "type": 0, # MultiTransactionSend
"toAddress": user_2.address, "toAddress": user_2.address,
"toAsset": "ETH", "toAsset": "ETH",
}, },
@ -101,19 +109,66 @@ class TransactionTestCase(RpcTestCase):
], ],
f"{option.password}", f"{option.password}",
] ]
response = self.rpc_request(method, params, 13) return self.rpc_request(method, params)
return response
def setup_method(self): def send_valid_multi_transaction(self, **kwargs):
super().setup_method() response = self.wallet_create_multi_transaction(**kwargs)
response = self.wallet_create_multi_transaction() tx_hash = None
self.verify_is_valid_json_rpc_response(response)
try: try:
self.tx_hash = response.json( tx_hash = response.json(
)["result"]["hashes"][str(self.network_id)][0] )["result"]["hashes"][str(self.network_id)][0]
except (KeyError, json.JSONDecodeError): except (KeyError, json.JSONDecodeError):
raise Exception(response.content) raise Exception(response.content)
return tx_hash
class TransactionTestCase(WalletTestCase):
def setup_method(self):
super().setup_method()
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): class SignalTestCase(RpcTestCase):

View File

@ -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)