Merge pull request #39 from propeller-heads/zz/sdk-integration-scripts

Substreams integration script
This commit is contained in:
tvinagre
2024-06-27 21:07:37 +02:00
committed by GitHub
15 changed files with 678 additions and 32 deletions

7
.gitignore vendored
View File

@@ -7,11 +7,16 @@ target/
# Substreams spkg files are build artifacts
*.spkg
.env
*/.env
.vscode
.idea
*.log
__pycache__
substreams/ethereum-template/Cargo.lock
.DS_Store
tycho-indexer
substreams/my_substream

View File

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

View File

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

View File

@@ -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&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"

View File

@@ -0,0 +1,106 @@
substreams_yaml_path: ./substreams.yaml
protocol_type_names:
- "curve_pool"
tests:
- name: test_3pool_creation
start_block: 10809470
stop_block: 10810226
expected_state:
protocol_components:
- id: "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7"
tokens:
- "0xdac17f958d2ee523a2206206994597c13d831ec7"
- "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
- "0x6b175474e89094c44da98b954eedeac495271d0f"
static_attributes:
creation_tx: "0x20793bbf260912aae189d5d261ff003c9b9166da8191d8f9d63ff1c7722f3ac6"
- name: test_steth_creation
start_block: 11592550
stop_block: 11595553
expected_state:
protocol_components:
- id: "0xdc24316b9ae028f1497c275eb9192a3ea0f67022"
tokens:
- "0x0000000000000000000000000000000000000000"
- "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"
static_attributes:
creation_tx: "0xfac67ecbd423a5b915deff06045ec9343568edaec34ae95c43d35f2c018afdaa"
- name: test_crypto_swap_ng_factory_plain_pool_creation
start_block: 19355220
stop_block: 19356225
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: 19217045
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: 18029610
expected_state:
protocol_components:
- id: "0x61fA2c947e523F9ABfb8d7e2903A5D5218C119a7"
tokens:
- "0x6c3ea9036406852006290770BEdFcAbA0e23A0e8"
- "0x3175Df0976dFA876431C2E9eE6Bc45b65d3473CC"
static_attributes:
creation_tx: "0xc9c6b879cbb19f7f26405335c3879c350592d530956878ff172e9efad786c63f"
- name: test_metapool_factory_plainpool_creation
start_block: 18808555
stop_block: 18818577
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: 19163633
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: 17374457
expected_state:
protocol_components:
- id: "0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B"
tokens:
- "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
- "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
- "0x0000000000000000000000000000000000000000"
static_attributes:
creation_tx: "0x2bd59c19f993b83729fb23498f897a58567c6f0b3ee2f00613ba515a7b19fe23"

View File

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

4
testing/.env.default Normal file
View File

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

33
testing/Dockerfile Normal file
View File

@@ -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", "--db_url", "postgres://postgres:mypassword@db:5432"]

40
testing/README.md Normal file
View File

@@ -0,0 +1,40 @@
# Substreams Testing
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.
## Overview
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.
## Prerequisites
- 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.
## Test Configuration
Tests are defined in a `yaml` file. A template can be found at `substreams/ethereum-template/test_assets.yaml`. The configuration file should include:
- The target Substreams config file.
- The expected protocol types.
- The tests to be run.
Each test will index all blocks between `start-block` and `stop-block` and verify that the indexed state matches the expected state.
## Running Tests
### Step 1: Export Environment Variables
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.
Example: `SUBSTREAMS_PATH=../substreams/ethereum-curve`
### Step 2: Build and the Testing Script
Run the testing script using Docker Compose:
```bash
docker compose run app
```

29
testing/cli.py Normal file
View File

@@ -0,0 +1,29 @@
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.",
)
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, db_url=args.db_url)
test_runner.run_tests()
if __name__ == "__main__":
main()

View File

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

36
testing/evm.py Normal file
View File

@@ -0,0 +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")
web3 = Web3(Web3.HTTPProvider(rpc_url))
if not web3.isConnected():
raise ConnectionError("Failed to connect to the Ethereum node")
# 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

4
testing/requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
psycopg2==2.9.9
PyYAML==6.0.1
Requests==2.32.2
web3==5.31.3

155
testing/runner.py Normal file
View File

@@ -0,0 +1,155 @@
import os
from pathlib import Path
import shutil
import subprocess
import yaml
from evm import get_token_balance
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, 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."""
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"], test["stop_block"]
)
if result.success:
print(f"{test['name']} passed.")
else:
print(f"❗️ {test['name']} failed: {result.message}")
self.tycho_runner.empty_database(
self.db_url
)
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"]
}
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]}"
)
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))
@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

172
testing/tycho.py Normal file
View File

@@ -0,0 +1,172 @@
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),
], # +2 is to make up for the cache in the index side.
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
env=env,
)
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 and self.with_binary_logs:
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 = {}
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()