diff --git a/substreams/rustfmt.toml b/substreams/rustfmt.toml index 42cdd4f..5cd8eb3 100644 --- a/substreams/rustfmt.toml +++ b/substreams/rustfmt.toml @@ -19,4 +19,4 @@ ignore = [ "ethereum-uniswap-v3/src/abi", "ethereum-uniswap-v3-logs-only/src/abi", "ethereum-uniswap-v4/src/abi", -] +] \ No newline at end of file diff --git a/testing/requirements.txt b/testing/requirements.txt index 32a4fac..f348f15 100644 --- a/testing/requirements.txt +++ b/testing/requirements.txt @@ -3,4 +3,4 @@ PyYAML==6.0.1 Requests==2.32.2 web3==5.31.3 git+https://github.com/propeller-heads/tycho-indexer.git@0.49.0#subdirectory=tycho-client-py -git+https://github.com/propeller-heads/tycho-simulation.git@0.69.0#subdirectory=tycho_simulation_py \ No newline at end of file +git+https://github.com/propeller-heads/tycho-simulation.git@348cf5114f7f6bebd2cb56732c0a81e27736fd12#subdirectory=tycho_simulation_py \ No newline at end of file diff --git a/testing/src/runner/cli.py b/testing/src/runner/cli.py index 66f08d0..27da17a 100644 --- a/testing/src/runner/cli.py +++ b/testing/src/runner/cli.py @@ -7,9 +7,7 @@ def main() -> None: description="Run indexer within a specified range of blocks" ) parser.add_argument("--package", type=str, help="Name of the package to test.") - parser.add_argument( - "--tycho-logs", action="store_true", help="Enable Tycho logs." - ) + parser.add_argument("--tycho-logs", action="store_true", help="Enable Tycho logs.") parser.add_argument( "--db-url", default="postgres://postgres:mypassword@localhost:5431/tycho_indexer_0", diff --git a/testing/src/runner/models.py b/testing/src/runner/models.py index a4711a8..f4f464c 100644 --- a/testing/src/runner/models.py +++ b/testing/src/runner/models.py @@ -31,7 +31,10 @@ class ProtocolComponentExpectation(BaseModel): @validator("static_attributes", pre=True, always=True) def convert_static_attributes_to_hexbytes(cls, v): if v: - return {k: v[k] if isinstance(v[k], HexBytes) else HexBytes(v[k].lower()) for k in v} + return { + k: v[k] if isinstance(v[k], HexBytes) else HexBytes(v[k].lower()) + for k in v + } return {} @validator("creation_tx", pre=True, always=True) diff --git a/testing/src/runner/runner.py b/testing/src/runner/runner.py index b9dff48..9c8218a 100644 --- a/testing/src/runner/runner.py +++ b/testing/src/runner/runner.py @@ -24,7 +24,6 @@ from tycho_indexer_client.dto import ( HexBytes, ResponseAccount, Snapshot, - ContractId, ) from tycho_indexer_client.rpc_client import TychoRPCClient @@ -40,8 +39,9 @@ from utils import build_snapshot_message, token_factory class TestResult: - def __init__(self, success: bool, message: str = None): + def __init__(self, success: bool, step: str = None, message: str = None): self.success = success + self.step = step self.message = message @classmethod @@ -49,8 +49,8 @@ class TestResult: return cls(success=True) @classmethod - def Failed(cls, message: str): - return cls(success=False, message=message) + def Failed(cls, step: str, message: str): + return cls(success=False, step=step, message=message) def parse_config(yaml_path: str) -> IntegrationTestsConfig: @@ -109,14 +109,17 @@ class TestRunner: test.initialized_accounts or [], ) - result = self.tycho_runner.run_with_rpc_server( - self.validate_state, test.expected_components, test.stop_block + result: TestResult = self.tycho_runner.run_with_rpc_server( + self.validate_state, + test.expected_components, + test.stop_block, + test.initialized_accounts or [], ) if result.success: print(f"\n✅ {test.name} passed.\n") else: - print(f"\n❗️ {test.name} failed: {result.message}\n") + print(f"\n❗️ {test.name} failed on {result.step}: {result.message}\n") print( "\nTest finished! \n" @@ -131,6 +134,7 @@ class TestRunner: self, expected_components: List[ProtocolComponentWithTestConfig], stop_block: int, + initialized_accounts: List[str], ) -> TestResult: """Validate the current protocol state against the expected state.""" protocol_components = self.tycho_rpc_client.get_protocol_components( @@ -143,21 +147,27 @@ class TestRunner: component.id: component for component in protocol_components } + step = "Protocol component validation" try: + # Step 1: Validate the protocol components for expected_component in expected_components: comp_id = expected_component.id.lower() if comp_id not in components_by_id: return TestResult.Failed( - f"'{comp_id}' not found in protocol components. " - f"Available components: {set(components_by_id.keys())}" + step=step, + message=f"'{comp_id}' not found in protocol components. " + f"Available components: {set(components_by_id.keys())}", ) diff = ProtocolComponentExpectation( **components_by_id[comp_id].dict() ).compare(ProtocolComponentExpectation(**expected_component.dict())) if diff is not None: - return TestResult.Failed(diff) + return TestResult.Failed(step=step, message=diff) + print(f"\n✅ {step} passed.\n") + + step = "Token balance validation" token_balances: dict[str, dict[HexBytes, int]] = defaultdict(dict) for component in protocol_components: comp_id = component.id.lower() @@ -181,42 +191,74 @@ class TestRunner: node_balance = get_token_balance(token, comp_id, stop_block) if node_balance != tycho_balance: return TestResult.Failed( - f"Balance mismatch for {comp_id}:{token} at block {stop_block}: got {node_balance} " - f"from rpc call and {tycho_balance} from Substreams" + step=step, + message=f"Balance mismatch for {comp_id}:{token} at block {stop_block}: got {node_balance} " + f"from rpc call and {tycho_balance} from Substreams", ) - contract_states = self.tycho_rpc_client.get_contract_state( - ContractStateParams( - contract_ids=[ - ContractId(chain=self._chain, address=a) - for component in protocol_components - for a in component.contract_ids - ] - ) - ) - filtered_components = [ - pc - for pc in protocol_components - if pc.id - in [c.id for c in expected_components if c.skip_simulation is False] - ] - simulation_failures = self.simulate_get_amount_out( - stop_block, protocol_states, filtered_components, contract_states - ) - if len(simulation_failures): - error_msgs = [] - for pool_id, failures in simulation_failures.items(): - failures_ = [ - f"{f.sell_token} -> {f.buy_token}: {f.error}" for f in failures - ] - error_msgs.append( - f"Pool {pool_id} failed simulations: {', '.join(failures_)}" - ) - raise ValueError(". ".join(error_msgs)) + if not self.config.skip_balance_check: + print(f"\n✅ {step} passed.\n") + else: + print(f"\nℹ️ {step} skipped") + + step = "Simulation validation" + + # Loads from Tycho-Indexer the state of all the contracts that are related to the protocol components. + filtered_components = [] + simulation_components = [ + c.id for c in expected_components if c.skip_simulation is False + ] + + related_contracts = set() + for account in self.config.initialized_accounts: + related_contracts.add(HexBytes(account)) + for account in initialized_accounts: + related_contracts.add(HexBytes(account)) + + # Filter out components that are not set to be used for the simulation + component_related_contracts = set() + for component in protocol_components: + if component.id in simulation_components: + for a in component.contract_ids: + component_related_contracts.add(a) + filtered_components.append(component) + + # Check if any of the initialized contracts are not listed as component contract dependencies + unspecified_contracts = related_contracts - component_related_contracts + if len(unspecified_contracts): + print( + f"⚠️ The following initialized contracts are not listed as component contract dependencies: {unspecified_contracts}. " + f"Please ensure that, if they are required for this component's simulation, they are specified under the Protocol Component's contract field." + ) + + related_contracts.update(component_related_contracts) + related_contracts = [a.hex() for a in related_contracts] + + contract_states = self.tycho_rpc_client.get_contract_state( + ContractStateParams(contract_ids=related_contracts) + ) + if len(filtered_components): + simulation_failures = self.simulate_get_amount_out( + stop_block, protocol_states, filtered_components, contract_states + ) + if len(simulation_failures): + error_msgs = [] + for pool_id, failures in simulation_failures.items(): + failures_ = [ + f"{f.sell_token} -> {f.buy_token}: {f.error}" + for f in failures + ] + error_msgs.append( + f"Pool {pool_id} failed simulations: {', '.join(failures_)}" + ) + return TestResult.Failed(step=step, message="/n".join(error_msgs)) + print(f"\n✅ {step} passed.\n") + else: + print(f"\nℹ️ {step} skipped") return TestResult.Passed() except Exception as e: error_message = f"An error occurred: {str(e)}\n" + traceback.format_exc() - return TestResult.Failed(error_message) + return TestResult.Failed(step=step, message=error_message) def simulate_get_amount_out( self, @@ -263,6 +305,17 @@ class TestRunner: decoded = decoder.decode_snapshot(snapshot_message, block) + for component in protocol_components: + if component.id not in decoded: + failed_simulations[component.id] = [ + SimulationFailure( + pool_id=component.id, + sell_token=component.tokens[0].hex(), + buy_token=component.tokens[1].hex(), + error="Pool not found in decoded state.", + ) + ] + for pool_state in decoded.values(): pool_id = pool_state.id_ if not pool_state.balances: