From 8cc526527e8993a18c1acc012e04107af247ef8a Mon Sep 17 00:00:00 2001 From: Florian Pellissier <111426680+flopell@users.noreply.github.com> Date: Thu, 16 May 2024 09:42:28 +0200 Subject: [PATCH 01/12] feat(testing): add a script for Tycho integration testing --- .gitignore | 2 + docker-compose.yaml | 15 ++ substreams/ethereum-curve/substreams.yaml | 7 +- substreams/ethereum-curve/test_assets.yaml | 108 +++++++++++++ testing/cli.py | 24 +++ testing/evm.py | 24 +++ testing/runner.py | 137 ++++++++++++++++ testing/tycho.py | 175 +++++++++++++++++++++ 8 files changed, 490 insertions(+), 2 deletions(-) create mode 100644 docker-compose.yaml create mode 100644 substreams/ethereum-curve/test_assets.yaml create mode 100644 testing/cli.py create mode 100644 testing/evm.py create mode 100644 testing/runner.py create mode 100644 testing/tycho.py diff --git a/.gitignore b/.gitignore index 8a9b8e6..c043c7d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ target/ .idea *.log +__pycache__ + substreams/ethereum-template/Cargo.lock .DS_Store diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..904d6f4 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,15 @@ +version: '3.1' +services: + db: + image: ghcr.io/dbsystel/postgresql-partman:15-5 + restart: "always" + environment: + POSTGRESQL_PASSWORD: mypassword + POSTGRESQL_DATABASE: tycho_indexer_0 + POSTGRESQL_USERNAME: postgres + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data +volumes: + postgres_data: \ No newline at end of file diff --git a/substreams/ethereum-curve/substreams.yaml b/substreams/ethereum-curve/substreams.yaml index 141bbfa..39ebd11 100644 --- a/substreams/ethereum-curve/substreams.yaml +++ b/substreams/ethereum-curve/substreams.yaml @@ -37,7 +37,7 @@ modules: - name: map_relative_balances kind: map - initialBlock: 9906598 # An arbitrary block that should change based on your requirements + initialBlock: 9906598 # An arbitrary block that should change based on your requirements inputs: - source: sf.ethereum.type.v2.Block - store: store_component_tokens @@ -61,6 +61,9 @@ modules: - map: map_relative_balances - store: store_component_tokens - store: store_balances - mode: deltas # This is the key property that simplifies `BalanceChange` handling + mode: deltas # This is the key property that simplifies `BalanceChange` handling output: type: proto:tycho.evm.v1.BlockContractChanges + +params: + map_components: "address=bebc44782c7db0a1a60cb6fe97d0b483032ff1c7&tx_hash=20793bbf260912aae189d5d261ff003c9b9166da8191d8f9d63ff1c7722f3ac6&tokens[]=6b175474e89094c44da98b954eedeac495271d0f&tokens[]=a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&tokens[]=dac17f958d2ee523a2206206994597c13d831ec7,address=dc24316b9ae028f1497c275eb9192a3ea0f67022&tx_hash=fac67ecbd423a5b915deff06045ec9343568edaec34ae95c43d35f2c018afdaa&tokens[]=EeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokens[]=ae7ab96520DE3A18E5e111B5EaAb095312D7fE84,address=d51a44d3fae010294c616388b506acda1bfaae46&tx_hash=dafb6385ed988ce8aacecfe1d97b38ea5e60b1ebce74d2423f71ddd621680138&tokens[]=dAC17F958D2ee523a2206206994597C13D831ec7&tokens[]=2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599&tokens[]=C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" diff --git a/substreams/ethereum-curve/test_assets.yaml b/substreams/ethereum-curve/test_assets.yaml new file mode 100644 index 0000000..bb69fa7 --- /dev/null +++ b/substreams/ethereum-curve/test_assets.yaml @@ -0,0 +1,108 @@ +substreams_yaml_path: ./substreams.yaml +protocol_type_names: + - "curve_pool" + - "crypto_swap_ng" + - "tricrypto" +tests: + - name: test_3pool_creation + start_block: 10809470 + stop_block: 10809480 + expected_state: + protocol_components: + - id: "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7" + tokens: + - "0xdac17f958d2ee523a2206206994597c13d831ec7" + - "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + - "0x6b175474e89094c44da98b954eedeac495271d0f" + static_attributes: + creation_tx: "0x20793bbf260912aae189d5d261ff003c9b9166da8191d8f9d63ff1c7722f3ac6" + - name: test_steth_creation + start_block: 11592550 + stop_block: 11592553 + expected_state: + protocol_components: + - id: "0xdc24316b9ae028f1497c275eb9192a3ea0f67022" + tokens: + - "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + - "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" + static_attributes: + creation_tx: "0xfac67ecbd423a5b915deff06045ec9343568edaec34ae95c43d35f2c018afdaa" + - name: test_crypto_swap_ng_factory_plain_pool_creation + start_block: 19355220 + stop_block: 19355225 + expected_state: + protocol_components: + - id: "0xeeda34a377dd0ca676b9511ee1324974fa8d980d" + tokens: + - "0xd9a442856c234a39a81a089c06451ebaa4306a72" + - "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0" + static_attributes: + creation_tx: "0x0e2bad5695d4ff8ebbaf668674a24bdcc4843da44933d947f2a454fd731da3c1" + - name: test_crypto_swap_ng_factory_meta_pool_creation + start_block: 19216042 + stop_block: 19216045 + expected_state: + protocol_components: + - id: "0xef484de8C07B6e2d732A92B5F78e81B38f99f95E" + tokens: + - "0x865377367054516e17014CcdED1e7d814EDC9ce4" + - "0xA5588F7cdf560811710A2D82D3C9c99769DB1Dcb" + static_attributes: + creation_tx: "0x3cfeecae1b43086ee5705f89b803e21eb0492d7d5db06c229586db8fc72f5665" + - name: test_metapool_factory_metapool_creation + start_block: 18028600 + stop_block: 18028610 + expected_state: + protocol_components: + - id: "0x61fA2c947e523F9ABfb8d7e2903A5D5218C119a7" + tokens: + - "0x6c3ea9036406852006290770BEdFcAbA0e23A0e8" + - "0x3175Df0976dFA876431C2E9eE6Bc45b65d3473CC" + static_attributes: + creation_tx: "0x78454fd29fad021842288092aaeee5eff9ab877e006ac372d084e5e95ca2bd2c" + - name: test_metapool_factory_plainpool_creation + start_block: 18808555 + stop_block: 18808577 + expected_state: + protocol_components: + - id: "0xf2DCf6336D8250754B4527f57b275b19c8D5CF88" + tokens: + - "0xe9633C52f4c8B7BDeb08c4A7fE8a5c1B84AFCf67" + - "0x77E06c9eCCf2E797fd462A92B6D7642EF85b0A44" + static_attributes: + creation_tx: "0xeb34c90d352f18ffcfe78b7e393e155f0314acf06c54d1ac9996e4ee5a9b4742" + - id: "0x3f67dc2AdBA4B1beB6A48c30AB3AFb1c1440d35B" + tokens: + - "0xe9633C52f4c8B7BDeb08c4A7fE8a5c1B84AFCf67" + - "0x77E06c9eCCf2E797fd462A92B6D7642EF85b0A44" + static_attributes: + creation_tx: "0x455559b43afaf429c15c1d807fd7f5dd47be30f6411a854499f719b944f4c024" + - name: test_cryptopool_factory_creation + start_block: 19162590 + stop_block: 19162633 + expected_state: + protocol_components: + - id: "0x71db3764d6841d8b01dc27c0fd4a66a8a34b2be0" + tokens: + - "0x04c154b66cb340f3ae24111cc767e0184ed00cc6" + - "0x4591dbff62656e7859afe5e45f6f47d3669fbb28" + static_attributes: + creation_tx: "0xa89c09a7e0dfd84f3a294b8df4f33cc4a623e6d52deee357457afe2591ea596f" + - id: "0x6c9Fe53cC13b125d6476E5Ce2b76983bd5b7A112" + tokens: + - "0x35fA164735182de50811E8e2E824cFb9B6118ac2" + - "0xf951E335afb289353dc249e82926178EaC7DEd78" + static_attributes: + creation_tx: "0xa5b13d50c56242f7994b8e1339032bb4c6f9ac3af3054d4eae3ce9e32e3c1a50" + - name: test_tricrypto_factory_creation + start_block: 17371455 + stop_block: 17371457 + expected_state: + protocol_components: + - id: "0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B" + tokens: + - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + - "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" #TODO: NEEDS TO BE NATIVE + static_attributes: + creation_tx: "0x2bd59c19f993b83729fb23498f897a58567c6f0b3ee2f00613ba515a7b19fe23" diff --git a/testing/cli.py b/testing/cli.py new file mode 100644 index 0000000..745b59d --- /dev/null +++ b/testing/cli.py @@ -0,0 +1,24 @@ +import argparse +from runner import TestRunner + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Run indexer within a specified range of blocks" + ) + parser.add_argument( + "--test_yaml_path", type=str, help="Path to the test configuration YAML file." + ) + parser.add_argument( + "--with_binary_logs", + action="store_true", + help="Flag to activate logs from Tycho.", + ) + args = parser.parse_args() + + test_runner = TestRunner(args.test_yaml_path, args.with_binary_logs) + test_runner.run_tests() + + +if __name__ == "__main__": + main() diff --git a/testing/evm.py b/testing/evm.py new file mode 100644 index 0000000..4408140 --- /dev/null +++ b/testing/evm.py @@ -0,0 +1,24 @@ +from web3 import Web3 + + +def get_token_balance(rpc_url, token_address, wallet_address, block_number): + web3 = Web3(Web3.HTTPProvider(rpc_url)) + + if not web3.isConnected(): + raise ConnectionError("Failed to connect to the Ethereum node") + + erc20_abi = [ + { + "constant": True, + "inputs": [{"name": "_owner", "type": "address"}], + "name": "balanceOf", + "outputs": [{"name": "balance", "type": "uint256"}], + "type": "function", + } + ] + + contract = web3.eth.contract(address=token_address, abi=erc20_abi) + balance = contract.functions.balanceOf(wallet_address).call( + block_identifier=block_number + ) + return balance diff --git a/testing/runner.py b/testing/runner.py new file mode 100644 index 0000000..2189570 --- /dev/null +++ b/testing/runner.py @@ -0,0 +1,137 @@ +import os +from pathlib import Path +import shutil +import subprocess + +import yaml + +from tycho import TychoRunner + + +class TestResult: + def __init__(self, success: bool, message: str = None): + self.success = success + self.message = message + + @classmethod + def Passed(cls): + return cls(success=True) + + @classmethod + def Failed(cls, message: str): + return cls(success=False, message=message) + + +def load_config(yaml_path: str) -> dict: + """Load YAML configuration from a specified file path.""" + with open(yaml_path, "r") as file: + return yaml.safe_load(file) + + +class TestRunner: + def __init__(self, config_path: str, with_binary_logs: bool): + self.config = load_config(config_path) + self.base_dir = os.path.dirname(config_path) + self.tycho_runner = TychoRunner(with_binary_logs) + + def run_tests(self) -> None: + """Run all tests specified in the configuration.""" + print(f"Running tests ...") + for test in self.config["tests"]: + + spkg_path = self.build_spkg( + os.path.join(self.base_dir, self.config["substreams_yaml_path"]), + lambda data: self.update_initial_block(data, test["start_block"]), + ) + self.tycho_runner.run_tycho( + spkg_path, + test["start_block"], + test["stop_block"], + self.config["protocol_type_names"], + ) + + result = self.tycho_runner.run_with_rpc_server( + self.validate_state, test["expected_state"] + ) + + if result.success: + print(f"✅ {test['name']} passed.") + else: + print(f"❗️ {test['name']} failed: {result.message}") + + self.tycho_runner.empty_database( + "postgres://postgres:mypassword@localhost:5432" + ) + + def validate_state(self, expected_state: dict) -> TestResult: + """Validate the current protocol state against the expected state.""" + protocol_components = self.tycho_runner.get_protocol_components() + components = { + component["id"]: component + for component in protocol_components["protocol_components"] + } + + try: + for expected_component in expected_state.get("protocol_components", []): + comp_id = expected_component["id"].lower() + if comp_id not in components: + return TestResult.Failed( + f"'{comp_id}' not found in protocol components." + ) + + component = components[comp_id] + for key, value in expected_component.items(): + if key not in component: + return TestResult.Failed( + f"Missing '{key}' in component '{comp_id}'." + ) + if isinstance(value, list): + if set(map(str.lower, value)) != set( + map(str.lower, component[key]) + ): + return TestResult.Failed( + f"List mismatch for key '{key}': {value} != {component[key]}" + ) + elif value is not None and value.lower() != component[key]: + return TestResult.Failed( + f"Value mismatch for key '{key}': {value} != {component[key]}" + ) + return TestResult.Passed() + + except Exception as e: + return TestResult.Failed(str(e)) + + @staticmethod + def build_spkg(yaml_file_path: str, modify_func: callable) -> str: + """Build a Substreams package with modifications to the YAML file.""" + backup_file_path = f"{yaml_file_path}.backup" + shutil.copy(yaml_file_path, backup_file_path) + + with open(yaml_file_path, "r") as file: + data = yaml.safe_load(file) + + modify_func(data) + spkg_name = f"{yaml_file_path.rsplit('/', 1)[0]}/{data['package']['name'].replace('_', '-', 1)}-{data['package']['version']}.spkg" + + with open(yaml_file_path, "w") as file: + yaml.dump(data, file, default_flow_style=False) + + try: + result = subprocess.run( + ["substreams", "pack", yaml_file_path], capture_output=True, text=True + ) + if result.returncode != 0: + print("Substreams pack command failed:", result.stderr) + except Exception as e: + print(f"Error running substreams pack command: {e}") + + shutil.copy(backup_file_path, yaml_file_path) + Path(backup_file_path).unlink() + + return spkg_name + + @staticmethod + def update_initial_block(data: dict, start_block: int) -> None: + """Update the initial block for all modules in the configuration data.""" + for module in data["modules"]: + module["initialBlock"] = start_block diff --git a/testing/tycho.py b/testing/tycho.py new file mode 100644 index 0000000..988dc70 --- /dev/null +++ b/testing/tycho.py @@ -0,0 +1,175 @@ +import signal +import threading +import time +import requests +import subprocess +import os +import psycopg2 +from psycopg2 import sql + +binary_path = "./testing/tycho-indexer" + + +class TychoRunner: + def __init__(self, with_binary_logs: bool = False): + self.with_binary_logs = with_binary_logs + + def run_tycho( + self, + spkg_path: str, + start_block: int, + end_block: int, + protocol_type_names: list, + ) -> None: + """Run the Tycho indexer with the specified SPKG and block range.""" + + env = os.environ.copy() + env["RUST_LOG"] = "info" + + try: + process = subprocess.Popen( + [ + binary_path, + "run", + "--spkg", + spkg_path, + "--module", + "map_protocol_changes", + "--protocol-type-names", + ",".join(protocol_type_names), + "--start-block", + str(start_block), + "--stop-block", + str(end_block + 2), + ], # TODO: +2 is a hack to make up for the cache in the index side. + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1, + env=env, + ) + if self.with_binary_logs: + with process.stdout: + for line in iter(process.stdout.readline, ""): + if line: + print(line.strip()) + + with process.stderr: + for line in iter(process.stderr.readline, ""): + if line: + print(line.strip()) + + process.wait() + + except Exception as e: + print(f"Error running Tycho indexer: {e}") + + def run_with_rpc_server(self, func: callable, *args, **kwargs): + """ + Run a function with Tycho RPC running in background. + + This function is a wrapper around a target function. It starts Tycho RPC as a background task, executes the target function and stops Tycho RPC. + """ + stop_event = threading.Event() + process = None + + def run_rpc_server(): + nonlocal process + try: + env = os.environ.copy() + env["RUST_LOG"] = "info" + + process = subprocess.Popen( + [binary_path, "rpc"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1, + env=env, + ) + # Read remaining stdout and stderr + if self.with_binary_logs: + for output in process.stdout: + if output: + print(output.strip()) + + for error_output in process.stderr: + if error_output: + print(error_output.strip()) + + process.wait() + + if process.returncode != 0: + print("Command failed with return code:", process.returncode) + + except Exception as e: + print(f"An error occurred while running the command: {e}") + finally: + if process and process.poll() is None: + process.terminate() + process.wait() + + # Start the RPC server in a separate thread + rpc_thread = threading.Thread(target=run_rpc_server) + rpc_thread.start() + time.sleep(3) # Wait for the RPC server to start + + try: + # Run the provided function + result = func(*args, **kwargs) + return result + + finally: + stop_event.set() + if process and process.poll() is None: + process.send_signal(signal.SIGINT) + if rpc_thread.is_alive(): + rpc_thread.join() + + @staticmethod + def get_protocol_components() -> dict: + """Retrieve protocol components from the RPC server.""" + url = "http://0.0.0.0:4242/v1/ethereum/protocol_components" + headers = {"accept": "application/json", "Content-Type": "application/json"} + data = {"protocol_system": "test_protocol"} + + response = requests.post(url, headers=headers, json=data) + return response.json() + + @staticmethod + def get_protocol_state() -> dict: + """Retrieve protocol state from the RPC server.""" + url = "http://0.0.0.0:4242/v1/ethereum/protocol_state" + headers = {"accept": "application/json", "Content-Type": "application/json"} + data = { + "protocolSystem": "string", + "version": {"block": {"chain": "ethereum", "number": 0}}, + } + + response = requests.post(url, headers=headers, json=data) + return response.json() + + @staticmethod + def empty_database(db_url: str) -> None: + """Drop and recreate the Tycho indexer database.""" + try: + conn = psycopg2.connect(db_url) + conn.autocommit = True + cursor = conn.cursor() + + cursor.execute( + sql.SQL("DROP DATABASE IF EXISTS {}").format( + sql.Identifier("tycho_indexer_0") + ) + ) + cursor.execute( + sql.SQL("CREATE DATABASE {}").format(sql.Identifier("tycho_indexer_0")) + ) + + except psycopg2.Error as e: + print(f"Database error: {e}") + finally: + if cursor: + cursor.close() + if conn: + conn.close() From 9a647b76ce3a7a25c12d911e0776365e12912b87 Mon Sep 17 00:00:00 2001 From: Florian Pellissier <111426680+flopell@users.noreply.github.com> Date: Tue, 21 May 2024 15:16:46 +0200 Subject: [PATCH 02/12] feat(testing): add balances check --- testing/evm.py | 44 ++++++++++++++++++++++++++++---------------- testing/runner.py | 12 ++++++++++-- testing/tycho.py | 5 +---- 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/testing/evm.py b/testing/evm.py index 4408140..1d90989 100644 --- a/testing/evm.py +++ b/testing/evm.py @@ -1,24 +1,36 @@ +import os from web3 import Web3 +native_aliases = ["0x0000000000000000000000000000000000000000","0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"] + +erc20_abi = [ + { + "constant": True, + "inputs": [{"name": "_owner", "type": "address"}], + "name": "balanceOf", + "outputs": [{"name": "balance", "type": "uint256"}], + "type": "function", + } +] + +def get_token_balance(token_address, wallet_address, block_number): + rpc_url = os.getenv("RPC_URL") + + if rpc_url is None: + raise EnvironmentError("RPC_URL environment variable not set") -def get_token_balance(rpc_url, token_address, wallet_address, block_number): web3 = Web3(Web3.HTTPProvider(rpc_url)) if not web3.isConnected(): raise ConnectionError("Failed to connect to the Ethereum node") - - erc20_abi = [ - { - "constant": True, - "inputs": [{"name": "_owner", "type": "address"}], - "name": "balanceOf", - "outputs": [{"name": "balance", "type": "uint256"}], - "type": "function", - } - ] - - contract = web3.eth.contract(address=token_address, abi=erc20_abi) - balance = contract.functions.balanceOf(wallet_address).call( - block_identifier=block_number - ) + + # Check if the token_address is a native token alias + if token_address.lower() in native_aliases: + balance = web3.eth.get_balance(Web3.toChecksumAddress(wallet_address), block_identifier=block_number) + else: + contract = web3.eth.contract(address=Web3.toChecksumAddress(token_address), abi=erc20_abi) + balance = contract.functions.balanceOf(Web3.toChecksumAddress(wallet_address)).call( + block_identifier=block_number + ) + return balance diff --git a/testing/runner.py b/testing/runner.py index 2189570..deab2e3 100644 --- a/testing/runner.py +++ b/testing/runner.py @@ -5,6 +5,7 @@ import subprocess import yaml +from evm import get_token_balance from tycho import TychoRunner @@ -51,7 +52,7 @@ class TestRunner: ) result = self.tycho_runner.run_with_rpc_server( - self.validate_state, test["expected_state"] + self.validate_state, test["expected_state"], test["stop_block"] ) if result.success: @@ -63,9 +64,10 @@ class TestRunner: "postgres://postgres:mypassword@localhost:5432" ) - def validate_state(self, expected_state: dict) -> TestResult: + def validate_state(self, expected_state: dict, stop_block: int) -> TestResult: """Validate the current protocol state against the expected state.""" protocol_components = self.tycho_runner.get_protocol_components() + protocol_states = self.tycho_runner.get_protocol_state() components = { component["id"]: component for component in protocol_components["protocol_components"] @@ -96,6 +98,12 @@ class TestRunner: return TestResult.Failed( f"Value mismatch for key '{key}': {value} != {component[key]}" ) + for state in protocol_states["states"]: + for token, balance in state["balances"].items(): + node_balance = get_token_balance(token,comp_id,stop_block) + tycho_balance = int(balance,16) + if node_balance != tycho_balance: + return TestResult.Failed(f"Balance mismatch for {comp_id}:{token} at block {stop_block}: got {node_balance} from rpc call and {tycho_balance} from Substreams") return TestResult.Passed() except Exception as e: diff --git a/testing/tycho.py b/testing/tycho.py index 988dc70..1806b0a 100644 --- a/testing/tycho.py +++ b/testing/tycho.py @@ -141,10 +141,7 @@ class TychoRunner: """Retrieve protocol state from the RPC server.""" url = "http://0.0.0.0:4242/v1/ethereum/protocol_state" headers = {"accept": "application/json", "Content-Type": "application/json"} - data = { - "protocolSystem": "string", - "version": {"block": {"chain": "ethereum", "number": 0}}, - } + data = {} response = requests.post(url, headers=headers, json=data) return response.json() From 30eb0d5adde1411bd639731349cb89345a870ab6 Mon Sep 17 00:00:00 2001 From: Florian Pellissier <111426680+flopell@users.noreply.github.com> Date: Thu, 23 May 2024 16:20:01 +0200 Subject: [PATCH 03/12] feat(testing): add readme for testing --- substreams/ethereum-template/test_assets.yaml | 28 ++++++++++ testing/README.md | 53 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 substreams/ethereum-template/test_assets.yaml create mode 100644 testing/README.md diff --git a/substreams/ethereum-template/test_assets.yaml b/substreams/ethereum-template/test_assets.yaml new file mode 100644 index 0000000..31edb21 --- /dev/null +++ b/substreams/ethereum-template/test_assets.yaml @@ -0,0 +1,28 @@ +substreams_yaml_path: ./substreams.yaml +protocol_type_names: + - "type_name_1" + - "type_name_2" +tests: + - name: test_pool_creation + start_block: 123 + stop_block: 456 + expected_state: + protocol_components: + - id: "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7" + tokens: + - "0xdac17f958d2ee523a2206206994597c13d831ec7" + - "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + - "0x6b175474e89094c44da98b954eedeac495271d0f" + static_attributes: + creation_tx: "0x20793bbf260912aae189d5d261ff003c9b9166da8191d8f9d63ff1c7722f3ac6" + - name: test_something_else + start_block: 123 + stop_block: 456 + expected_state: + protocol_components: + - id: "0xdc24316b9ae028f1497c275eb9192a3ea0f67022" + tokens: + - "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + - "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" + static_attributes: + creation_tx: "0xfac67ecbd423a5b915deff06045ec9343568edaec34ae95c43d35f2c018afdaa" diff --git a/testing/README.md b/testing/README.md new file mode 100644 index 0000000..7f64c80 --- /dev/null +++ b/testing/README.md @@ -0,0 +1,53 @@ +# Substreams Testing + +We know testing your Substreams modules can be a pain. So, our goal here is to provide a quick and easy way to run end-to-end tests. + +Our script will build the `.spkg` for your Substreams module, index the specified block range, and check if the expected state has been correctly indexed in PostgreSQL. + +## How to Test + +Here's what you need to do: + +1. Get the latest version of our indexer. +2. Define your tests and expected state. +3. Run the testing script. + +### Get the Latest Version of Tycho (Indexer) + +We don't have a direct download link yet. Please reach out to us to get the latest version. Once you have it, place it in the `/testing/` folder. + +### Define Your Tests + +Define all your tests in a `yaml` file. You can find a template in `substreams/ethereum-template/test_assets.yaml`. + +You'll need to: + +- Point to the target substreams config file. +- Specify the expected protocol types. +- Define your tests. + +For each test, the script will index all blocks between `start-block` and `stop-block` and check that the indexed state matches the expected state. + +### Run the Script + +Once everything is set up, run our script using the CLI. + +First, you need a local postgres database + +```bash +docker-compose up -d db +``` + +Then export an environment variable for RPC connection + +```bash +export RPC_URL=your-chain-rpc +``` + +And finally run the testing script + +```bash +python testing/cli.py --test_yaml_path "./substreams/your-substreams-folder/test_assets.yaml" +``` + +You can get the available CLI flags using `--help` From bcf11876d24d844d4ad174fe08ec41a7b81f1706 Mon Sep 17 00:00:00 2001 From: Florian Pellissier <111426680+flopell@users.noreply.github.com> Date: Fri, 24 May 2024 11:14:52 +0200 Subject: [PATCH 04/12] fix: update docker-compose port for db --- docker-compose.yaml | 2 +- testing/runner.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 904d6f4..7c93ea8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,7 +8,7 @@ services: POSTGRESQL_DATABASE: tycho_indexer_0 POSTGRESQL_USERNAME: postgres ports: - - "5432:5432" + - "5431:5432" volumes: - postgres_data:/var/lib/postgresql/data volumes: diff --git a/testing/runner.py b/testing/runner.py index deab2e3..195c322 100644 --- a/testing/runner.py +++ b/testing/runner.py @@ -61,7 +61,7 @@ class TestRunner: print(f"❗️ {test['name']} failed: {result.message}") self.tycho_runner.empty_database( - "postgres://postgres:mypassword@localhost:5432" + "postgres://postgres:mypassword@localhost:5431" ) def validate_state(self, expected_state: dict, stop_block: int) -> TestResult: From c8f4fd611c723f69a7f5e9b8c02317dc75147d93 Mon Sep 17 00:00:00 2001 From: Florian Pellissier <111426680+flopell@users.noreply.github.com> Date: Fri, 24 May 2024 11:15:14 +0200 Subject: [PATCH 05/12] chore: add requirements.txt for testing script --- testing/requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 testing/requirements.txt diff --git a/testing/requirements.txt b/testing/requirements.txt new file mode 100644 index 0000000..d141296 --- /dev/null +++ b/testing/requirements.txt @@ -0,0 +1,4 @@ +psycopg2==2.9.9 +PyYAML==6.0.1 +Requests==2.32.2 +web3==5.31.3 From a96d246e11a046bd58fe44392a3cce7522c741f4 Mon Sep 17 00:00:00 2001 From: Florian Pellissier <111426680+flopell@users.noreply.github.com> Date: Fri, 14 Jun 2024 17:33:29 +0200 Subject: [PATCH 06/12] fix(test runner): fix how we check balances and stout full buffer --- testing/runner.py | 22 +++++++++++++++------- testing/tycho.py | 20 ++++++++++---------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/testing/runner.py b/testing/runner.py index 195c322..a9136ed 100644 --- a/testing/runner.py +++ b/testing/runner.py @@ -98,14 +98,22 @@ class TestRunner: return TestResult.Failed( f"Value mismatch for key '{key}': {value} != {component[key]}" ) - for state in protocol_states["states"]: - for token, balance in state["balances"].items(): - node_balance = get_token_balance(token,comp_id,stop_block) - tycho_balance = int(balance,16) - if node_balance != tycho_balance: - return TestResult.Failed(f"Balance mismatch for {comp_id}:{token} at block {stop_block}: got {node_balance} from rpc call and {tycho_balance} from Substreams") - return TestResult.Passed() + for component in protocol_components["protocol_components"]: + comp_id = component["id"].lower() + for token in component["tokens"]: + token_lower = token.lower() + state = next((s for s in protocol_states["states"] if s["component_id"].lower() == comp_id), None) + if state: + balance_hex = state["balances"].get(token_lower, "0x0") + else: + balance_hex = "0x0" + + node_balance = get_token_balance(token, comp_id, stop_block) + tycho_balance = int(balance_hex, 16) + if node_balance != tycho_balance: + return TestResult.Failed(f"Balance mismatch for {comp_id}:{token} at block {stop_block}: got {node_balance} from rpc call and {tycho_balance} from Substreams") + return TestResult.Passed() except Exception as e: return TestResult.Failed(str(e)) diff --git a/testing/tycho.py b/testing/tycho.py index 1806b0a..8408cc7 100644 --- a/testing/tycho.py +++ b/testing/tycho.py @@ -41,23 +41,23 @@ class TychoRunner: str(start_block), "--stop-block", str(end_block + 2), - ], # TODO: +2 is a hack to make up for the cache in the index side. + ], # +2 is to make up for the cache in the index side. stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, env=env, ) - if self.with_binary_logs: - with process.stdout: - for line in iter(process.stdout.readline, ""): - if line: - print(line.strip()) + + with process.stdout: + for line in iter(process.stdout.readline, ""): + if line and self.with_binary_logs: + print(line.strip()) - with process.stderr: - for line in iter(process.stderr.readline, ""): - if line: - print(line.strip()) + with process.stderr: + for line in iter(process.stderr.readline, ""): + if line and self.with_binary_logs: + print(line.strip()) process.wait() From c4176cf9bba98a3a538382327685eafac78d2c2f Mon Sep 17 00:00:00 2001 From: Florian Pellissier <111426680+flopell@users.noreply.github.com> Date: Fri, 14 Jun 2024 21:19:19 +0200 Subject: [PATCH 07/12] chore: add tycho-indexer to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index c043c7d..9c89335 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ __pycache__ substreams/ethereum-template/Cargo.lock .DS_Store + +tycho-indexer \ No newline at end of file From 329375fa0e18c7b418c1805b3c6e3bb466d9aafd Mon Sep 17 00:00:00 2001 From: Florian Pellissier <111426680+flopell@users.noreply.github.com> Date: Fri, 14 Jun 2024 21:29:08 +0200 Subject: [PATCH 08/12] refactor: update Curve test assets --- substreams/ethereum-curve/test_assets.yaml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/substreams/ethereum-curve/test_assets.yaml b/substreams/ethereum-curve/test_assets.yaml index bb69fa7..6a9088d 100644 --- a/substreams/ethereum-curve/test_assets.yaml +++ b/substreams/ethereum-curve/test_assets.yaml @@ -6,7 +6,7 @@ protocol_type_names: tests: - name: test_3pool_creation start_block: 10809470 - stop_block: 10809480 + stop_block: 10810226 expected_state: protocol_components: - id: "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7" @@ -18,18 +18,18 @@ tests: creation_tx: "0x20793bbf260912aae189d5d261ff003c9b9166da8191d8f9d63ff1c7722f3ac6" - name: test_steth_creation start_block: 11592550 - stop_block: 11592553 + stop_block: 11595553 expected_state: protocol_components: - id: "0xdc24316b9ae028f1497c275eb9192a3ea0f67022" tokens: - - "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + - "0x0000000000000000000000000000000000000000" - "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" static_attributes: creation_tx: "0xfac67ecbd423a5b915deff06045ec9343568edaec34ae95c43d35f2c018afdaa" - name: test_crypto_swap_ng_factory_plain_pool_creation start_block: 19355220 - stop_block: 19355225 + stop_block: 19356225 expected_state: protocol_components: - id: "0xeeda34a377dd0ca676b9511ee1324974fa8d980d" @@ -40,7 +40,7 @@ tests: creation_tx: "0x0e2bad5695d4ff8ebbaf668674a24bdcc4843da44933d947f2a454fd731da3c1" - name: test_crypto_swap_ng_factory_meta_pool_creation start_block: 19216042 - stop_block: 19216045 + stop_block: 19217045 expected_state: protocol_components: - id: "0xef484de8C07B6e2d732A92B5F78e81B38f99f95E" @@ -51,7 +51,7 @@ tests: creation_tx: "0x3cfeecae1b43086ee5705f89b803e21eb0492d7d5db06c229586db8fc72f5665" - name: test_metapool_factory_metapool_creation start_block: 18028600 - stop_block: 18028610 + stop_block: 18029610 expected_state: protocol_components: - id: "0x61fA2c947e523F9ABfb8d7e2903A5D5218C119a7" @@ -59,10 +59,10 @@ tests: - "0x6c3ea9036406852006290770BEdFcAbA0e23A0e8" - "0x3175Df0976dFA876431C2E9eE6Bc45b65d3473CC" static_attributes: - creation_tx: "0x78454fd29fad021842288092aaeee5eff9ab877e006ac372d084e5e95ca2bd2c" + creation_tx: "0xc9c6b879cbb19f7f26405335c3879c350592d530956878ff172e9efad786c63f" - name: test_metapool_factory_plainpool_creation start_block: 18808555 - stop_block: 18808577 + stop_block: 18818577 expected_state: protocol_components: - id: "0xf2DCf6336D8250754B4527f57b275b19c8D5CF88" @@ -79,7 +79,7 @@ tests: creation_tx: "0x455559b43afaf429c15c1d807fd7f5dd47be30f6411a854499f719b944f4c024" - name: test_cryptopool_factory_creation start_block: 19162590 - stop_block: 19162633 + stop_block: 19163633 expected_state: protocol_components: - id: "0x71db3764d6841d8b01dc27c0fd4a66a8a34b2be0" @@ -96,13 +96,13 @@ tests: creation_tx: "0xa5b13d50c56242f7994b8e1339032bb4c6f9ac3af3054d4eae3ce9e32e3c1a50" - name: test_tricrypto_factory_creation start_block: 17371455 - stop_block: 17371457 + stop_block: 17374457 expected_state: protocol_components: - id: "0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B" tokens: - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" - - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" #TODO: NEEDS TO BE NATIVE + - "0x0000000000000000000000000000000000000000" static_attributes: creation_tx: "0x2bd59c19f993b83729fb23498f897a58567c6f0b3ee2f00613ba515a7b19fe23" From b432e2827faec3c3657745bb9bc9ac8f641c9eb0 Mon Sep 17 00:00:00 2001 From: Florian Pellissier <111426680+flopell@users.noreply.github.com> Date: Fri, 14 Jun 2024 21:51:34 +0200 Subject: [PATCH 09/12] fix(curve): wrong static attibutes --- .../ethereum-curve/src/pool_factories.rs | 32 +++++++++---------- substreams/ethereum-curve/test_assets.yaml | 2 -- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/substreams/ethereum-curve/src/pool_factories.rs b/substreams/ethereum-curve/src/pool_factories.rs index 499f557..d58dc54 100644 --- a/substreams/ethereum-curve/src/pool_factories.rs +++ b/substreams/ethereum-curve/src/pool_factories.rs @@ -107,7 +107,7 @@ pub fn address_map( }, Attribute { name: "factory_name".into(), - value: "crypto_pool".into(), + value: "crypto_pool_factory".into(), change: ChangeType::Creation.into(), }, Attribute { @@ -182,7 +182,7 @@ pub fn address_map( static_att: vec![ Attribute { name: "pool_type".into(), - value: "PlainPool".into(), + value: "plain_pool".into(), change: ChangeType::Creation.into(), }, Attribute { @@ -192,7 +192,7 @@ pub fn address_map( }, Attribute { name: "factory_name".into(), - value: "meta_pool".into(), + value: "meta_pool_factory".into(), change: ChangeType::Creation.into(), }, Attribute { @@ -261,7 +261,7 @@ pub fn address_map( }, Attribute { name: "factory_name".into(), - value: "meta_pool".into(), + value: "meta_pool_factory".into(), change: ChangeType::Creation.into(), }, Attribute { @@ -336,12 +336,12 @@ pub fn address_map( }, Attribute { name: "factory_name".into(), - value: "meta_pool".into(), + value: "meta_pool_factory".into(), change: ChangeType::Creation.into(), }, Attribute { name: "factory".into(), - value: address_to_bytes_with_0x(&META_POOL_FACTORY), + value: address_to_bytes_with_0x(&META_POOL_FACTORY_OLD), change: ChangeType::Creation.into(), }, Attribute { @@ -396,7 +396,7 @@ pub fn address_map( }, Attribute { name: "factory_name".into(), - value: "crypto_swap_ng".into(), + value: "crypto_swap_ng_factory".into(), change: ChangeType::Creation.into(), }, Attribute { @@ -444,7 +444,7 @@ pub fn address_map( }, Attribute { name: "factory_name".into(), - value: "crypto_swap_ng".into(), + value: "crypto_swap_ng_factory".into(), change: ChangeType::Creation.into(), }, Attribute { @@ -491,7 +491,7 @@ pub fn address_map( static_att: vec![ Attribute { name: "pool_type".into(), - value: "trycrypto".into(), + value: "tricrypto".into(), change: ChangeType::Creation.into(), }, Attribute { @@ -501,7 +501,7 @@ pub fn address_map( }, Attribute { name: "factory_name".into(), - value: "tricrypto".into(), + value: "tricrypto_factory".into(), change: ChangeType::Creation.into(), }, Attribute { @@ -571,7 +571,7 @@ pub fn address_map( static_att: vec![ Attribute { name: "pool_type".into(), - value: "plain".into(), + value: "plain_pool".into(), change: ChangeType::Creation.into(), }, Attribute { @@ -581,18 +581,18 @@ pub fn address_map( }, Attribute { name: "factory_name".into(), - value: "stable_swap".into(), + value: "stable_swap_factory".into(), change: ChangeType::Creation.into(), }, Attribute { name: "factory".into(), - value: address_to_bytes_with_0x(&CRYPTO_SWAP_NG_FACTORY), + value: address_to_bytes_with_0x(&STABLESWAP_FACTORY), change: ChangeType::Creation.into(), }, ], change: ChangeType::Creation.into(), protocol_type: Some(ProtocolType { - name: "curve".into(), + name: "curve_pool".into(), financial_type: FinancialType::Swap.into(), attribute_schema: Vec::new(), implementation_type: ImplementationType::Vm.into(), @@ -649,7 +649,7 @@ pub fn address_map( }, Attribute { name: "factory_name".into(), - value: "stable_swap".into(), + value: "stable_swap_factory".into(), change: ChangeType::Creation.into(), }, Attribute { @@ -667,7 +667,7 @@ pub fn address_map( ], change: ChangeType::Creation.into(), protocol_type: Some(ProtocolType { - name: "curve".into(), + name: "curve_pool".into(), financial_type: FinancialType::Swap.into(), attribute_schema: Vec::new(), implementation_type: ImplementationType::Vm.into(), diff --git a/substreams/ethereum-curve/test_assets.yaml b/substreams/ethereum-curve/test_assets.yaml index 6a9088d..008d8ae 100644 --- a/substreams/ethereum-curve/test_assets.yaml +++ b/substreams/ethereum-curve/test_assets.yaml @@ -1,8 +1,6 @@ substreams_yaml_path: ./substreams.yaml protocol_type_names: - "curve_pool" - - "crypto_swap_ng" - - "tricrypto" tests: - name: test_3pool_creation start_block: 10809470 From b598a12592cda9889d0e3eb46fb2cb352d6252e2 Mon Sep 17 00:00:00 2001 From: Florian Pellissier <111426680+flopell@users.noreply.github.com> Date: Fri, 14 Jun 2024 23:32:28 +0200 Subject: [PATCH 10/12] chore: update params --- substreams/ethereum-curve/params.json | 26 +++++++++++------------ substreams/ethereum-curve/substreams.yaml | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/substreams/ethereum-curve/params.json b/substreams/ethereum-curve/params.json index 7366b44..23c01aa 100644 --- a/substreams/ethereum-curve/params.json +++ b/substreams/ethereum-curve/params.json @@ -14,8 +14,8 @@ "address": "dc24316b9ae028f1497c275eb9192a3ea0f67022", "tx_hash": "fac67ecbd423a5b915deff06045ec9343568edaec34ae95c43d35f2c018afdaa", "tokens": [ - "EeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", - "ae7ab96520DE3A18E5e111B5EaAb095312D7fE84" + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "ae7ab96520de3a18e5e111b5eaab095312d7fe84" ] }, { @@ -23,29 +23,29 @@ "address": "d51a44d3fae010294c616388b506acda1bfaae46", "tx_hash": "dafb6385ed988ce8aacecfe1d97b38ea5e60b1ebce74d2423f71ddd621680138", "tokens": [ - "dAC17F958D2ee523a2206206994597C13D831ec7", - "2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", - "C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + "dac17f958d2ee523a2206206994597c13d831ec7", + "2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" ] }, { "name": "susd", - "address": "A5407eAE9Ba41422680e2e00537571bcC53efBfD", + "address": "a5407eae9ba41422680e2e00537571bcc53efbfd", "tx_hash": "51aca4a03a395de8855fa2ca59b7febe520c2a223e69c502066162f7c1a95ec2", "tokens": [ - "6B175474E89094C44Da98b954EedeAC495271d0F", - "A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - "dAC17F958D2ee523a2206206994597C13D831ec7", - "57Ab1ec28D129707052df4dF418D58a2D46d5f51" + "6b175474e89094c44da98b954eedeac495271d0f", + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "dac17f958d2ee523a2206206994597c13d831ec7", + "57ab1ec28d129707052df4df418d58a2d46d5f51" ] }, { "name": "fraxusdc", - "address": "DcEF968d416a41Cdac0ED8702fAC8128A64241A2", + "address": "dcef968d416a41cdac0ed8702fac8128a64241a2", "tx_hash": "1f4254004ce9e19d4eb742ee5a69d30f29085902d976f73e97c44150225ef775", "tokens": [ - "853d955aCEf822Db058eb8505911ED77F175b99e", - "A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + "853d955acef822db058eb8505911ed77f175b99e", + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" ] } ] \ No newline at end of file diff --git a/substreams/ethereum-curve/substreams.yaml b/substreams/ethereum-curve/substreams.yaml index 39ebd11..18a7a24 100644 --- a/substreams/ethereum-curve/substreams.yaml +++ b/substreams/ethereum-curve/substreams.yaml @@ -66,4 +66,4 @@ modules: type: proto:tycho.evm.v1.BlockContractChanges params: - map_components: "address=bebc44782c7db0a1a60cb6fe97d0b483032ff1c7&tx_hash=20793bbf260912aae189d5d261ff003c9b9166da8191d8f9d63ff1c7722f3ac6&tokens[]=6b175474e89094c44da98b954eedeac495271d0f&tokens[]=a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&tokens[]=dac17f958d2ee523a2206206994597c13d831ec7,address=dc24316b9ae028f1497c275eb9192a3ea0f67022&tx_hash=fac67ecbd423a5b915deff06045ec9343568edaec34ae95c43d35f2c018afdaa&tokens[]=EeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokens[]=ae7ab96520DE3A18E5e111B5EaAb095312D7fE84,address=d51a44d3fae010294c616388b506acda1bfaae46&tx_hash=dafb6385ed988ce8aacecfe1d97b38ea5e60b1ebce74d2423f71ddd621680138&tokens[]=dAC17F958D2ee523a2206206994597C13D831ec7&tokens[]=2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599&tokens[]=C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + map_components: "address=bebc44782c7db0a1a60cb6fe97d0b483032ff1c7&tx_hash=20793bbf260912aae189d5d261ff003c9b9166da8191d8f9d63ff1c7722f3ac6&tokens[]=6b175474e89094c44da98b954eedeac495271d0f&tokens[]=a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&tokens[]=dac17f958d2ee523a2206206994597c13d831ec7&attribute_keys[]=name&attribute_vals[]=3pool&attribute_keys[]=factory_name&attribute_vals[]=NA&attribute_keys[]=factory&attribute_vals[]=0x0000000000000000000000000000000000000000,address=dc24316b9ae028f1497c275eb9192a3ea0f67022&tx_hash=fac67ecbd423a5b915deff06045ec9343568edaec34ae95c43d35f2c018afdaa&tokens[]=eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee&tokens[]=ae7ab96520de3a18e5e111b5eaab095312d7fe84&attribute_keys[]=name&attribute_vals[]=steth&attribute_keys[]=factory_name&attribute_vals[]=NA&attribute_keys[]=factory&attribute_vals[]=0x0000000000000000000000000000000000000000,address=d51a44d3fae010294c616388b506acda1bfaae46&tx_hash=dafb6385ed988ce8aacecfe1d97b38ea5e60b1ebce74d2423f71ddd621680138&tokens[]=dac17f958d2ee523a2206206994597c13d831ec7&tokens[]=2260fac5e5542a773aa44fbcfedf7c193bc2c599&tokens[]=c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2&attribute_keys[]=name&attribute_vals[]=tricrypto2&attribute_keys[]=factory_name&attribute_vals[]=NA&attribute_keys[]=factory&attribute_vals[]=0x0000000000000000000000000000000000000000,address=a5407eae9ba41422680e2e00537571bcc53efbfd&tx_hash=51aca4a03a395de8855fa2ca59b7febe520c2a223e69c502066162f7c1a95ec2&tokens[]=6b175474e89094c44da98b954eedeac495271d0f&tokens[]=a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&tokens[]=dac17f958d2ee523a2206206994597c13d831ec7&tokens[]=57ab1ec28d129707052df4df418d58a2d46d5f51&attribute_keys[]=name&attribute_vals[]=susd&attribute_keys[]=factory_name&attribute_vals[]=NA&attribute_keys[]=factory&attribute_vals[]=0x0000000000000000000000000000000000000000,address=dcef968d416a41cdac0ed8702fac8128a64241a2&tx_hash=1f4254004ce9e19d4eb742ee5a69d30f29085902d976f73e97c44150225ef775&tokens[]=853d955acef822db058eb8505911ed77f175b99e&tokens[]=a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&attribute_keys[]=name&attribute_vals[]=fraxusdc&attribute_keys[]=factory_name&attribute_vals[]=NA&attribute_keys[]=factory&attribute_vals[]=0x0000000000000000000000000000000000000000" From a9954873d9f49cb274c8db5d117fd83a5265e6b8 Mon Sep 17 00:00:00 2001 From: Thales Lima Date: Thu, 27 Jun 2024 16:02:19 +0200 Subject: [PATCH 11/12] Wrap testing module inside a docker-compose --- .gitignore | 5 ++-- docker-compose.yaml | 15 ---------- testing/.env.default | 4 +++ testing/Dockerfile | 33 +++++++++++++++++++++ testing/README.md | 57 ++++++++++++++----------------------- testing/docker-compose.yaml | 31 ++++++++++++++++++++ testing/runner.py | 2 +- 7 files changed, 94 insertions(+), 53 deletions(-) delete mode 100644 docker-compose.yaml create mode 100644 testing/.env.default create mode 100644 testing/Dockerfile create mode 100644 testing/docker-compose.yaml diff --git a/.gitignore b/.gitignore index 9c89335..3b908fb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ target/ # Substreams spkg files are build artifacts *.spkg -.env +*/.env .vscode .idea *.log @@ -18,4 +18,5 @@ substreams/ethereum-template/Cargo.lock .DS_Store -tycho-indexer \ No newline at end of file +tycho-indexer +substreams/my_substream \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index 7c93ea8..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,15 +0,0 @@ -version: '3.1' -services: - db: - image: ghcr.io/dbsystel/postgresql-partman:15-5 - restart: "always" - environment: - POSTGRESQL_PASSWORD: mypassword - POSTGRESQL_DATABASE: tycho_indexer_0 - POSTGRESQL_USERNAME: postgres - ports: - - "5431:5432" - volumes: - - postgres_data:/var/lib/postgresql/data -volumes: - postgres_data: \ No newline at end of file diff --git a/testing/.env.default b/testing/.env.default new file mode 100644 index 0000000..21eb434 --- /dev/null +++ b/testing/.env.default @@ -0,0 +1,4 @@ +SUBSTREAMS_PATH=../substreams/ethereum-curve +RPC_URL=https://mainnet.infura.io/v3/your-infura-key +DATABASE_URL: "postgres://postgres:mypassword@db:5432/tycho_indexer_0" +SUBSTREAMS_API_TOKEN="changeme" \ No newline at end of file diff --git a/testing/Dockerfile b/testing/Dockerfile new file mode 100644 index 0000000..ff7c6cc --- /dev/null +++ b/testing/Dockerfile @@ -0,0 +1,33 @@ +# Use an official Python runtime as a parent image +FROM --platform=linux/amd64 continuumio/miniconda3:24.4.0-0 + +# Set the working directory in the container to /app +WORKDIR /app + +# Add current directory code to /app in container +ADD . /app/testing +RUN chmod +x /app/testing/tycho-indexer + +# Create a new conda environment and install pip +RUN conda create -n myenv pip python=3.9 + +# Install any needed packages specified in requirements.txt +RUN echo "source activate myenv" >~/.bashrc +ENV PATH /opt/conda/envs/myenv/bin:$PATH + +RUN apt-get update \ + && apt-get -y install libpq-dev gcc \ + && pip install psycopg2 \ + && apt-get clean + +RUN /bin/bash -c "source activate myenv && pip install --no-cache-dir -r testing/requirements.txt" + +# Make port 80 available to the world outside this container +EXPOSE 80 + +# Install the substreams cli +RUN wget -c https://github.com/streamingfast/substreams/releases/download/v1.8.0/substreams_linux_x86_64.tar.gz -O - | tar xzf - substreams +RUN mv substreams /usr/local/bin/substreams && chmod +x /usr/local/bin/substreams + +# Run the command to start your application +CMD ["python", "testing/cli.py", "--test_yaml_path", "/app/substreams/my_substream/test_assets.yaml", "--with_binary_logs"] \ No newline at end of file diff --git a/testing/README.md b/testing/README.md index 7f64c80..7355128 100644 --- a/testing/README.md +++ b/testing/README.md @@ -1,53 +1,40 @@ # Substreams Testing -We know testing your Substreams modules can be a pain. So, our goal here is to provide a quick and easy way to run end-to-end tests. +This package provides a comprehensive testing suite for Substreams modules. The testing suite is designed to facilitate end-to-end testing, ensuring that your Substreams modules function as expected. -Our script will build the `.spkg` for your Substreams module, index the specified block range, and check if the expected state has been correctly indexed in PostgreSQL. +## Overview -## How to Test +The testing suite builds the `.spkg` for your Substreams module, indexes a specified block range, and verifies that the expected state has been correctly indexed in PostgreSQL. -Here's what you need to do: +## Prerequisites -1. Get the latest version of our indexer. -2. Define your tests and expected state. -3. Run the testing script. +- Latest version of our indexer, Tycho. Please contact us to obtain the latest version. Once acquired, place it in the `/testing/` directory. +- Docker installed on your machine. -### Get the Latest Version of Tycho (Indexer) +## Test Configuration -We don't have a direct download link yet. Please reach out to us to get the latest version. Once you have it, place it in the `/testing/` folder. +Tests are defined in a `yaml` file. A template can be found at `substreams/ethereum-template/test_assets.yaml`. The configuration file should include: -### Define Your Tests +- The target Substreams config file. +- The expected protocol types. +- The tests to be run. -Define all your tests in a `yaml` file. You can find a template in `substreams/ethereum-template/test_assets.yaml`. +Each test will index all blocks between `start-block` and `stop-block` and verify that the indexed state matches the expected state. -You'll need to: +## Running Tests -- Point to the target substreams config file. -- Specify the expected protocol types. -- Define your tests. +### Step 1: Export Environment Variables -For each test, the script will index all blocks between `start-block` and `stop-block` and check that the indexed state matches the expected state. +Export the required environment variables for the execution. You can find the available environment variables in the `.env.default` file. +Please create a `.env` file in the `testing` directory and set the required environment variables. -### Run the Script +The variable SUBSTREAMS_PATH should be a relative reference to the directory containing the Substreams module that you want to test. -Once everything is set up, run our script using the CLI. +Example: `SUBSTREAMS_PATH=../substreams/ethereum-curve` -First, you need a local postgres database +### Step 2: Build and the Testing Script +Run the testing script using Docker Compose: ```bash -docker-compose up -d db -``` - -Then export an environment variable for RPC connection - -```bash -export RPC_URL=your-chain-rpc -``` - -And finally run the testing script - -```bash -python testing/cli.py --test_yaml_path "./substreams/your-substreams-folder/test_assets.yaml" -``` - -You can get the available CLI flags using `--help` +docker compose run app +``` \ No newline at end of file diff --git a/testing/docker-compose.yaml b/testing/docker-compose.yaml new file mode 100644 index 0000000..c618ad4 --- /dev/null +++ b/testing/docker-compose.yaml @@ -0,0 +1,31 @@ +version: '3.1' +services: + db: + image: ghcr.io/dbsystel/postgresql-partman:15-5 + restart: "always" + environment: + POSTGRESQL_PASSWORD: mypassword + POSTGRESQL_DATABASE: tycho_indexer_0 + POSTGRESQL_USERNAME: postgres + ports: + - "5431:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + app: + build: + context: . + dockerfile: Dockerfile + volumes: + - ${SUBSTREAMS_PATH}:/app/substreams/my_substream + - ../substreams:/app/substreams + - ../proto:/app/proto + - ./tycho-indexer:/app/testing/tycho-indexer + - ./runner.py:/app/testing/runner.py + ports: + - "80:80" + depends_on: + - db + env_file: + - ".env" +volumes: + postgres_data: \ No newline at end of file diff --git a/testing/runner.py b/testing/runner.py index a9136ed..6c767d3 100644 --- a/testing/runner.py +++ b/testing/runner.py @@ -61,7 +61,7 @@ class TestRunner: print(f"❗️ {test['name']} failed: {result.message}") self.tycho_runner.empty_database( - "postgres://postgres:mypassword@localhost:5431" + "postgres://postgres:mypassword@db:5432" ) def validate_state(self, expected_state: dict, stop_block: int) -> TestResult: From ac863a495d929daeddec71fb3fcaca2bff9284fb Mon Sep 17 00:00:00 2001 From: Thales Lima Date: Thu, 27 Jun 2024 21:07:19 +0200 Subject: [PATCH 12/12] Make db url configurable --- testing/Dockerfile | 2 +- testing/cli.py | 7 ++++++- testing/runner.py | 10 ++++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/testing/Dockerfile b/testing/Dockerfile index ff7c6cc..8019ebd 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -30,4 +30,4 @@ RUN wget -c https://github.com/streamingfast/substreams/releases/download/v1.8.0 RUN mv substreams /usr/local/bin/substreams && chmod +x /usr/local/bin/substreams # Run the command to start your application -CMD ["python", "testing/cli.py", "--test_yaml_path", "/app/substreams/my_substream/test_assets.yaml", "--with_binary_logs"] \ No newline at end of file +CMD ["python", "testing/cli.py", "--test_yaml_path", "/app/substreams/my_substream/test_assets.yaml", "--with_binary_logs", "--db_url", "postgres://postgres:mypassword@db:5432"] \ No newline at end of file diff --git a/testing/cli.py b/testing/cli.py index 745b59d..0243c8f 100644 --- a/testing/cli.py +++ b/testing/cli.py @@ -14,9 +14,14 @@ def main() -> None: action="store_true", help="Flag to activate logs from Tycho.", ) + parser.add_argument( + "--db_url", + type=str, + help="Postgres database URL for the Tycho indexer.", + ) args = parser.parse_args() - test_runner = TestRunner(args.test_yaml_path, args.with_binary_logs) + test_runner = TestRunner(args.test_yaml_path, args.with_binary_logs, db_url=args.db_url) test_runner.run_tests() diff --git a/testing/runner.py b/testing/runner.py index 6c767d3..bbf8933 100644 --- a/testing/runner.py +++ b/testing/runner.py @@ -30,10 +30,11 @@ def load_config(yaml_path: str) -> dict: class TestRunner: - def __init__(self, config_path: str, with_binary_logs: bool): + def __init__(self, config_path: str, with_binary_logs: bool, db_url: str): self.config = load_config(config_path) self.base_dir = os.path.dirname(config_path) self.tycho_runner = TychoRunner(with_binary_logs) + self.db_url = db_url def run_tests(self) -> None: """Run all tests specified in the configuration.""" @@ -61,7 +62,7 @@ class TestRunner: print(f"❗️ {test['name']} failed: {result.message}") self.tycho_runner.empty_database( - "postgres://postgres:mypassword@db:5432" + self.db_url ) def validate_state(self, expected_state: dict, stop_block: int) -> TestResult: @@ -89,7 +90,7 @@ class TestRunner: ) if isinstance(value, list): if set(map(str.lower, value)) != set( - map(str.lower, component[key]) + map(str.lower, component[key]) ): return TestResult.Failed( f"List mismatch for key '{key}': {value} != {component[key]}" @@ -112,7 +113,8 @@ class TestRunner: node_balance = get_token_balance(token, comp_id, stop_block) tycho_balance = int(balance_hex, 16) if node_balance != tycho_balance: - return TestResult.Failed(f"Balance mismatch for {comp_id}:{token} at block {stop_block}: got {node_balance} from rpc call and {tycho_balance} from Substreams") + return TestResult.Failed( + f"Balance mismatch for {comp_id}:{token} at block {stop_block}: got {node_balance} from rpc call and {tycho_balance} from Substreams") return TestResult.Passed() except Exception as e: return TestResult.Failed(str(e))