diff --git a/testing/.env.default b/testing/.env.default index 91db7db..a192fe5 100644 --- a/testing/.env.default +++ b/testing/.env.default @@ -1,5 +1,5 @@ -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" -DOMAIN_OWNER="AWSAccountId" \ No newline at end of file +export SUBSTREAMS_PACKAGE=ethereum-curve +export RPC_URL=https://mainnet.infura.io/v3/your-infura-key +export DATABASE_URL: "postgres://postgres:mypassword@db:5432/tycho_indexer_0" +export SUBSTREAMS_API_TOKEN="changeme" +export DOMAIN_OWNER="AWSAccountId" \ No newline at end of file diff --git a/testing/Dockerfile b/testing/Dockerfile index 66f1c57..d6003b0 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -4,6 +4,14 @@ FROM --platform=linux/amd64 continuumio/miniconda3:24.4.0-0 # Set the working directory in the container to /app WORKDIR /app +# Install Foundry +RUN apt-get update && apt-get install -y curl +RUN curl -L https://foundry.paradigm.xyz | bash +RUN /bin/bash -c "source $HOME/.bashrc && $HOME/.foundry/bin/foundryup" +# +# Add Foundry to PATH +ENV PATH /root/.foundry/bin:$PATH + # Add current directory code to /app in container ADD . /app/testing @@ -22,7 +30,7 @@ RUN apt-get update \ && apt-get clean ARG PIP_INDEX_URL -RUN /bin/bash -c "source activate myenv && cd testing && pip install --no-cache-dir -r src/requirements.txt && cd -" +RUN /bin/bash -c "source activate myenv && cd testing && pip install --no-cache-dir -r requirements.txt && cd -" # Make port 80 available to the world outside this container EXPOSE 80 diff --git a/testing/README.md b/testing/README.md index 8796f45..bef2a35 100644 --- a/testing/README.md +++ b/testing/README.md @@ -33,9 +33,27 @@ Please place this Runtime file under the respective `substream` directory inside 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. -The variable SUBSTREAMS_PATH should be a relative reference to the directory containing the Substreams module that you want to test. +#### Environment Variables -Example: `SUBSTREAMS_PATH=../substreams/ethereum-curve` +**SUBSTREAMS_PACKAGE** +- **Description**: Specifies the Substreams module that you want to test +- **Example**: `export SUBSTREAMS_PACKAGE=ethereum-balancer` + +**DATABASE_URL** +- **Description**: The connection string for the PostgreSQL database. It includes the username, password, host, port, and database name. It's already set to the default for the Docker container. +- **Example**: `export DATABASE_URL="postgres://postgres:mypassword@localhost:5431/tycho_indexer_0` + +**RPC_URL** +- **Description**: The URL for the Ethereum RPC endpoint. This is used to fetch the storage data. The node needs to be an archive node, and support [debug_storageRangeAt](https://www.quicknode.com/docs/ethereum/debug_storageRangeAt). +- **Example**: `export RPC_URL="https://ethereum-mainnet.core.chainstack.com/123123123123"` + +**SUBSTREAMS_API_TOKEN** +- **Description**: The API token for accessing Substreams services. This token is required for authentication. +- **Example**: `export SUBSTREAMS_API_TOKEN=eyJhbGci...` + +**DOMAIN_OWNER** +- **Description**: The domain owner identifier for Propellerhead's AWS account, used for authenticating on the private PyPI repository. +- **Example**: `export DOMAIN_OWNER=123456789` ### Step 2: Build and the Testing Script diff --git a/testing/__init__.py b/testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testing/docker-compose.yaml b/testing/docker-compose.yaml index 2e787f1..7f74724 100644 --- a/testing/docker-compose.yaml +++ b/testing/docker-compose.yaml @@ -20,7 +20,6 @@ services: args: PIP_INDEX_URL: ${PIP_INDEX_URL} volumes: -# - ${SUBSTREAMS_PATH}:/app/substreams/my_substream - ../substreams:/app/substreams - ../proto:/app/proto - ./tycho-indexer:/bin/tycho-indexer @@ -33,13 +32,15 @@ services: - db env_file: - ".env" + environment: + - DATABASE_URL=postgres://postgres:mypassword@db:5432/tycho_indexer_0 command: - "python" - "testing/src/runner/cli.py" - "--package" - - "ethereum-balancer" - - "--with_binary_logs" - - "--db_url" + - ${SUBSTREAMS_PACKAGE} + - "--tycho-logs" + - "--db-url" - "postgres://postgres:mypassword@db:5432/tycho_indexer_0" volumes: postgres_data: \ No newline at end of file diff --git a/testing/pre_build.sh b/testing/pre_build.sh index 06fd22b..342aed4 100755 --- a/testing/pre_build.sh +++ b/testing/pre_build.sh @@ -12,7 +12,7 @@ set +a # Check if DOMAIN_OWNER is set if [ -z "$DOMAIN_OWNER" ]; then echo "DOMAIN_OWNER environment variable is not set." - exit 1 + return 1 fi # Fetch the CODEARTIFACT_AUTH_TOKEN diff --git a/testing/src/runner/adapter_handler.py b/testing/src/runner/adapter_handler.py new file mode 100644 index 0000000..31b3816 --- /dev/null +++ b/testing/src/runner/adapter_handler.py @@ -0,0 +1,44 @@ +import os +import subprocess +from typing import Optional + + +class AdapterContractHandler: + @staticmethod + def build_target( + src_path: str, + adapter_contract: str, + signature: Optional[str], + args: Optional[str], + ): + """ + Runs the buildRuntime Bash script in a subprocess with the provided arguments. + + :param src_path: Path to the script to be executed. + :param adapter_contract: The contract name to be passed to the script. + :param signature: The constructor signature to be passed to the script. + :param args: The constructor arguments to be passed to the script. + """ + + script_path = "scripts/buildRuntime.sh" + cmd = [script_path, "-c", adapter_contract] + if signature: + cmd.extend(["-s", signature, "-a", args]) + try: + # Running the bash script with the provided arguments + result = subprocess.run( + [script_path, "-c", adapter_contract, "-s", signature, "-a", args], + cwd=src_path, + capture_output=True, + text=True, + check=True, + ) + + # Print standard output and error for debugging + print("Output:\n", result.stdout) + if result.stderr: + print("Errors:\n", result.stderr) + + except subprocess.CalledProcessError as e: + print(f"An error occurred: {e}") + print("Error Output:\n", e.stderr) diff --git a/testing/src/runner/runner.py b/testing/src/runner/runner.py index 93ebf26..51c51f6 100644 --- a/testing/src/runner/runner.py +++ b/testing/src/runner/runner.py @@ -10,6 +10,8 @@ import traceback import yaml from pydantic import BaseModel + +from adapter_handler import AdapterContractHandler from tycho_client.decoders import ThirdPartyPoolTychoDecoder from tycho_client.models import Blockchain, EVMBlock from tycho_client.tycho_adapter import TychoPoolStateStreamAdapter @@ -46,13 +48,19 @@ class SimulationFailure(BaseModel): class TestRunner: - def __init__(self, package: str, with_binary_logs: bool, db_url: str, vm_traces: bool): + def __init__( + self, package: str, with_binary_logs: bool, db_url: str, vm_traces: bool + ): self.repo_root = os.getcwd() - config_path = os.path.join(self.repo_root, "substreams", package, "test_assets.yaml") + config_path = os.path.join( + self.repo_root, "substreams", package, "test_assets.yaml" + ) self.config = load_config(config_path) self.spkg_src = os.path.join(self.repo_root, "substreams", package) self.adapters_src = os.path.join(self.repo_root, "evm") - self.tycho_runner = TychoRunner(db_url, with_binary_logs, self.config["initialized_accounts"]) + self.tycho_runner = TychoRunner( + db_url, with_binary_logs, self.config["initialized_accounts"] + ) self.tycho_rpc_client = TychoRPCClient() self.db_url = db_url self._vm_traces = vm_traces @@ -113,7 +121,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]}" @@ -151,15 +159,20 @@ class TestRunner: f"from rpc call and {tycho_balance} from Substreams" ) contract_states = self.tycho_rpc_client.get_contract_state() - filtered_components = {'protocol_components': [pc for pc in protocol_components["protocol_components"] if - pc["id"] in [c["id"].lower() for c in - expected_state["protocol_components"] if - c.get("skip_simulation", False) is False]]} + filtered_components = { + "protocol_components": [ + pc + for pc in protocol_components["protocol_components"] + if pc["id"] + in [ + c["id"].lower() + for c in expected_state["protocol_components"] + if c.get("skip_simulation", False) is False + ] + ] + } simulation_failures = self.simulate_get_amount_out( - stop_block, - protocol_states, - filtered_components, - contract_states, + stop_block, protocol_states, filtered_components, contract_states ) if len(simulation_failures): error_msgs = [] @@ -178,11 +191,11 @@ class TestRunner: return TestResult.Failed(error_message) def simulate_get_amount_out( - self, - block_number: int, - protocol_states: dict, - protocol_components: dict, - contract_state: dict, + self, + block_number: int, + protocol_states: dict, + protocol_components: dict, + contract_state: dict, ) -> dict[str, list[SimulationFailure]]: protocol_type_names = self.config["protocol_type_names"] @@ -196,10 +209,24 @@ class TestRunner: failed_simulations: dict[str, list[SimulationFailure]] = dict() for protocol in protocol_type_names: adapter_contract = os.path.join( - self.adapters_src, "out", f"{self.config['adapter_contract']}.sol", - f"{self.config['adapter_contract']}.evm.runtime" + self.adapters_src, + "out", + f"{self.config['adapter_contract']}.sol", + f"{self.config['adapter_contract']}.evm.runtime", + ) + if not os.path.exists(adapter_contract): + print("Adapter contract not found. Building it ...") + + AdapterContractHandler.build_target( + self.adapters_src, + self.config["adapter_contract"], + self.config["adapter_build_signature"], + self.config["adapter_build_args"], + ) + + decoder = ThirdPartyPoolTychoDecoder( + adapter_contract, 0, trace=self._vm_traces ) - decoder = ThirdPartyPoolTychoDecoder(adapter_contract, 0, trace=self._vm_traces) stream_adapter = TychoPoolStateStreamAdapter( tycho_url="0.0.0.0:4242", protocol=protocol, @@ -216,10 +243,12 @@ class TestRunner: if not pool_state.balances: raise ValueError(f"Missing balances for pool {pool_id}") for sell_token, buy_token in itertools.permutations( - pool_state.tokens, 2 + pool_state.tokens, 2 ): # Try to sell 0.1% of the protocol balance - sell_amount = Decimal("0.001") * pool_state.balances[sell_token.address] + sell_amount = ( + Decimal("0.001") * pool_state.balances[sell_token.address] + ) try: amount_out, gas_used, _ = pool_state.get_amount_out( sell_token, sell_amount, buy_token