Skip to content

Commit 26c328f

Browse files
Merge pull request #85 from multiversx/mesh-cli-ci
Check implementation using Mesh CLI, within GitHub Actions
2 parents 0ab9266 + ccd8a5b commit 26c328f

12 files changed

Lines changed: 658 additions & 0 deletions

.flake8

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[flake8]
2+
ignore = E501
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Check with Mesh CLI
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- uses: actions/setup-python@v5
13+
with:
14+
python-version: 3.11
15+
16+
- uses: actions/checkout@v4
17+
18+
- name: Install dependencies
19+
run: |
20+
pip3 install requests bottle
21+
curl -sSfL https://raw.githubusercontent.com/coinbase/mesh-cli/master/scripts/install.sh | sh -s -- -b "$HOME/.local/bin"
22+
echo "$HOME/.local/bin" >> $GITHUB_PATH
23+
24+
- name: Build
25+
run: |
26+
cd $GITHUB_WORKSPACE/cmd/rosetta && go build .
27+
cd $GITHUB_WORKSPACE/systemtests && go build ./proxyToObserverAdapter.go
28+
29+
- name: check:data
30+
run: |
31+
PYTHONPATH=. python3 ./systemtests/check_with_mesh_cli.py --mode=data --network=testnet
32+
33+
- name: check:construction
34+
run: |
35+
PYTHONPATH=. python3 ./systemtests/check_with_mesh_cli.py --mode=construction --network=testnet
36+
37+

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
cmd/rosetta/rosetta
22
cmd/rosetta/logs/**
3+
systemtests/logs/**
4+
systemtests/**/check-data/**
5+
logs/**
36

47
**/__pycache__/**

systemtests/check_with_mesh_cli.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import shutil
2+
import signal
3+
import subprocess
4+
import sys
5+
import time
6+
from argparse import ArgumentParser
7+
from typing import Any
8+
9+
import requests
10+
11+
from systemtests import constants
12+
from systemtests.config import CONFIGURATIONS, Configuration
13+
14+
15+
def main() -> int:
16+
parser = ArgumentParser()
17+
parser.add_argument("--mode", choices=["data", "construction"], required=True)
18+
parser.add_argument("--network", choices=CONFIGURATIONS.keys(), required=True)
19+
args = parser.parse_args()
20+
21+
mode = args.mode
22+
configuration = CONFIGURATIONS[args.network]
23+
24+
process_rosetta = run_rosetta(configuration)
25+
process_adapter = run_proxy_to_observer_adapter(configuration)
26+
process_checker = run_mesh_cli(mode, configuration)
27+
28+
# Handle termination signals
29+
def signal_handler(sig: Any, frame: Any):
30+
process_rosetta.kill()
31+
process_adapter.kill()
32+
process_checker.kill()
33+
sys.exit(1)
34+
35+
signal.signal(signal.SIGINT, signal_handler)
36+
signal.signal(signal.SIGTERM, signal_handler)
37+
38+
# Wait for checker to finish
39+
exit_code = process_checker.wait()
40+
41+
process_rosetta.kill()
42+
process_adapter.kill()
43+
44+
time.sleep(1)
45+
46+
print(f"Checker finished with exit code: {exit_code}.")
47+
return exit_code
48+
49+
50+
def run_rosetta(configuration: Configuration):
51+
"""
52+
E.g.
53+
54+
rosetta --port=7091 --observer-http-url=http://localhost:8080 \
55+
--observer-actual-shard=0 --network-id=D --network-name=devnet --native-currency=EGLD \
56+
--first-historical-epoch=42 --num-historical-epochs=1
57+
"""
58+
59+
current_epoch = get_current_epoch(configuration)
60+
61+
command = [
62+
str(constants.PATH_ROSETTA),
63+
f"--port={constants.PORT_ROSETTA}",
64+
f"--observer-http-url=http://localhost:{constants.PORT_OBSERVER_SURROGATE}",
65+
f"--observer-actual-shard={configuration.network_shard}",
66+
f"--network-id={configuration.network_id}",
67+
f"--network-name={configuration.network_name}",
68+
f"--native-currency={configuration.native_currency}",
69+
f"--first-historical-epoch={current_epoch}",
70+
f"--num-historical-epochs={configuration.num_historical_epochs}",
71+
]
72+
73+
return subprocess.Popen(command)
74+
75+
76+
def run_proxy_to_observer_adapter(configuration: Configuration):
77+
command = [
78+
str(constants.PATH_PROXY_TO_OBSERVER_ADAPTER),
79+
f"--proxy={configuration.proxy_url}",
80+
f"--shard={configuration.network_shard}",
81+
f"--sleep={constants.ADAPTER_DELAY_IN_MILLISECONDS}"
82+
]
83+
84+
return subprocess.Popen(command)
85+
86+
87+
def run_mesh_cli(mode: str, configuration: Configuration):
88+
if mode == "data":
89+
return run_mesh_cli_with_check_data(configuration)
90+
elif mode == "construction":
91+
return run_mesh_cli_with_check_construction(configuration)
92+
else:
93+
raise ValueError(f"Unknown mode: {mode}")
94+
95+
96+
def run_mesh_cli_with_check_construction(configuration: Configuration):
97+
"""
98+
E.g.
99+
100+
rosetta-cli check:construction --configuration-file devnet-construction.json \
101+
--online-url=http://localhost:7091 --offline-url=http://localhost:7091
102+
"""
103+
104+
command = [
105+
"rosetta-cli",
106+
"check:construction",
107+
f"--configuration-file={configuration.check_construction_configuration_file}",
108+
f"--online-url=http://localhost:{constants.PORT_ROSETTA}",
109+
f"--offline-url=http://localhost:{constants.PORT_ROSETTA}",
110+
]
111+
112+
return subprocess.Popen(command)
113+
114+
115+
def run_mesh_cli_with_check_data(configuration: Configuration):
116+
"""
117+
E.g.
118+
119+
rosetta-cli check:data --configuration-file devnet-data.json \
120+
--online-url=http://localhost:7091 --data-dir=devnet-data
121+
"""
122+
123+
shutil.rmtree(configuration.check_data_directory, ignore_errors=True)
124+
125+
current_epoch = get_current_epoch(configuration)
126+
first_historical_epoch = current_epoch - configuration.num_historical_epochs + 1
127+
start_block = get_start_of_epoch(configuration, first_historical_epoch)
128+
129+
command = [
130+
"rosetta-cli",
131+
"check:data",
132+
f"--configuration-file={configuration.check_data_configuration_file}",
133+
f"--online-url=http://localhost:{constants.PORT_ROSETTA}",
134+
f"--data-dir={configuration.check_data_directory}",
135+
f"--start-block={start_block}"
136+
]
137+
138+
return subprocess.Popen(command)
139+
140+
141+
def get_current_epoch(configuration: Configuration) -> int:
142+
response = requests.get(f"{configuration.proxy_url}/network/status/{configuration.network_shard}")
143+
response.raise_for_status()
144+
return response.json()["data"]["status"]["erd_epoch_number"]
145+
146+
147+
def get_start_of_epoch(configuration: Configuration, epoch: int) -> int:
148+
response = requests.get(f"{configuration.proxy_url}/network/epoch-start/{configuration.network_shard}/by-epoch/{epoch}")
149+
response.raise_for_status()
150+
return response.json()["data"]["epochStart"]["nonce"]
151+
152+
153+
if __name__ == "__main__":
154+
exit_code = main()
155+
sys.exit(exit_code)

systemtests/config.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass
5+
class Configuration:
6+
network_shard: int
7+
network_id: str
8+
network_name: str
9+
native_currency: str
10+
num_historical_epochs: int
11+
proxy_url: str
12+
check_construction_configuration_file: str
13+
check_data_configuration_file: str
14+
check_data_directory: str
15+
16+
17+
CONFIGURATIONS = {
18+
"devnet": Configuration(
19+
network_shard=0,
20+
network_id="D",
21+
network_name="untitled",
22+
native_currency="EGLD",
23+
num_historical_epochs=2,
24+
proxy_url="https://devnet-gateway.multiversx.com",
25+
check_construction_configuration_file="systemtests/mesh_cli_config/devnet-construction.json",
26+
check_data_configuration_file="systemtests/mesh_cli_config/check-data.json",
27+
check_data_directory="systemtests/devnet-data",
28+
),
29+
"testnet": Configuration(
30+
network_shard=0,
31+
network_id="T",
32+
network_name="untitled",
33+
native_currency="EGLD",
34+
num_historical_epochs=2,
35+
proxy_url="https://testnet-gateway.multiversx.com",
36+
check_construction_configuration_file="systemtests/mesh_cli_config/testnet-construction.json",
37+
check_data_configuration_file="systemtests/mesh_cli_config/check-data.json",
38+
check_data_directory="systemtests/testnet-data",
39+
),
40+
}

systemtests/constants.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from pathlib import Path
2+
3+
PATH_REPOSITORY = Path(__file__).parent.parent
4+
PATH_ROSETTA = PATH_REPOSITORY / "cmd" / "rosetta" / "rosetta"
5+
PATH_PROXY_TO_OBSERVER_ADAPTER = PATH_REPOSITORY / "systemtests" / "proxyToObserverAdapter"
6+
PORT_ROSETTA = 7091
7+
PORT_OBSERVER_SURROGATE = 8080
8+
ADAPTER_DELAY_IN_MILLISECONDS = 100
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"network": {
3+
"blockchain": "MultiversX",
4+
"network": "untitled"
5+
},
6+
"http_timeout": 30,
7+
"max_retries": 10,
8+
"retry_elapsed_time": 20,
9+
"max_online_connections": 8,
10+
"max_sync_concurrency": 4,
11+
"tip_delay": 10,
12+
"max_reorg_depth": 100,
13+
"log_configuration": false,
14+
"compression_disabled": false,
15+
"l0_in_memory_enabled": false,
16+
"error_stack_trace_disabled": false,
17+
"coin_supported": false,
18+
"construction": null,
19+
"data": {
20+
"active_reconciliation_concurrency": 8,
21+
"inactive_reconciliation_concurrency": 1,
22+
"inactive_reconciliation_frequency": 8,
23+
"log_blocks": false,
24+
"log_transactions": true,
25+
"log_balance_changes": true,
26+
"log_reconciliations": true,
27+
"ignore_reconciliation_error": false,
28+
"reconciliation_disabled": false,
29+
"reconciliation_drain_disabled": false,
30+
"inactive_discrepancy_search_disabled": false,
31+
"balance_tracking_disabled": false,
32+
"coin_tracking_disabled": false,
33+
"status_port": 9091,
34+
"results_output_file": "",
35+
"pruning_block_disabled": false,
36+
"pruning_balance_disabled": false,
37+
"initial_balance_fetch_disabled": false,
38+
"historical_balance_disabled": false,
39+
"end_conditions": {
40+
"reconciliation_coverage": {
41+
"coverage": 1,
42+
"from_tip": true
43+
}
44+
}
45+
}
46+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"network": {
3+
"blockchain": "MultiversX",
4+
"network": "untitled"
5+
},
6+
"data_directory": "",
7+
"http_timeout": 200,
8+
"max_retries": 1,
9+
"max_online_connections": 120,
10+
"max_sync_concurrency": 1,
11+
"construction": {
12+
"end_conditions": {
13+
"transfer": 1
14+
},
15+
"stale_depth": 10,
16+
"broadcast_limit": 5,
17+
"constructor_dsl_file": "devnet-construction.ros",
18+
"prefunded_accounts": [
19+
{
20+
"account_identifier": {
21+
"address": "erd1ldjsdetjvegjdnda0qw2h62kq6rpvrklkc5pw9zxm0nwulfhtyqqtyc4vq"
22+
},
23+
"privkey": "3e4e89e501eb542c12403fb15c52479e8721f2f4dedc3b3ef0f3b47b37de006c",
24+
"curve_type": "edwards25519",
25+
"currency": {
26+
"symbol": "EGLD",
27+
"decimals": 18
28+
}
29+
}
30+
]
31+
},
32+
"data": {
33+
"inactive_discrepancy_search_disabled": true
34+
}
35+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
transfer(1){
2+
transfer{
3+
transfer.network = {"network":"untitled", "blockchain":"MultiversX"};
4+
native_currency = {"symbol":"EGLD", "decimals":18};
5+
sender = {
6+
"account_identifier": {
7+
"address": "erd1ldjsdetjvegjdnda0qw2h62kq6rpvrklkc5pw9zxm0nwulfhtyqqtyc4vq"
8+
},
9+
"currency": {
10+
"symbol": "EGLD",
11+
"decimals": 18
12+
}
13+
};
14+
15+
max_fee = "50000000000000";
16+
max_transfer_amount = "10000000000000000";
17+
recipient_amount = random_number({"minimum": "1", "maximum": {{max_transfer_amount}}});
18+
19+
print_message({"recipient_amount":{{recipient_amount}}});
20+
21+
sender_amount = 0-{{recipient_amount}};
22+
recipient = {
23+
"account_identifier": {
24+
"address": "erd1xtslmt67utuewwv8jsx729mxjxaa8dvyyzp7492hy99dl7hvcuqq30l98v"
25+
},
26+
"currency": {
27+
"symbol": "EGLD",
28+
"decimals": 18
29+
}
30+
};
31+
transfer.confirmation_depth = "10";
32+
transfer.operations = [
33+
{
34+
"operation_identifier":{"index":0},
35+
"type":"Transfer",
36+
"account":{{sender.account_identifier}},
37+
"amount":{
38+
"value":{{sender_amount}},
39+
"currency":{{native_currency}}
40+
}
41+
},
42+
{
43+
"operation_identifier":{"index":1},
44+
"related_operations": [{"index": 0}],
45+
"type":"Transfer",
46+
"account":{{recipient.account_identifier}},
47+
"amount":{
48+
"value":{{recipient_amount}},
49+
"currency":{{native_currency}}
50+
}
51+
}
52+
];
53+
}
54+
}

0 commit comments

Comments
 (0)