diff --git a/tycho_simulation_py/Readme.md b/tycho_simulation_py/Readme.md index f19d6613..9a9db2f5 100644 --- a/tycho_simulation_py/Readme.md +++ b/tycho_simulation_py/Readme.md @@ -1,19 +1,10 @@ # Python bindings for Tycho Simulation -`tycho_simulation_py` - a Python module, implemented as a Rust crate, that allows using Rust EVM simulation module from -Python. +`tycho_simulation_py` - is a Python module that allows using Tycho's Simulation module, originally written in Rust. -## Install +This library is intended for anyone who wants to leverage Tycho's ecosystem and performance, while integrating with their Python applications. -We regularly push wheel to codeartifact. You should be able to install them from there. If there is no wheel for you -arch available, please consider building it (see below) and pushing it. - -``` -aws --region eu-central-1 codeartifact pip --tool twine --domain propeller --domain-owner 827659017777 --repository protosim -pip install tycho-simulation-py -``` - -## Summary +### How does it work? `evm` module from `tycho-simulation` crate implements simulating on-chain transactions. This crate - `tycho_simulation_py` - @@ -23,7 +14,7 @@ wraps `evm` in order to allow using it in Python. Rust Python ┌────────────────────────────────────────────────────────────────┐ ┌────────────────────────────┐ │ │ │ │ -│ tycho_simulation::evm_simulation tycho_simulation_py │ │ tycho_simulation_py │ +│ tycho_simulation::evm::simulation tycho_simulation_py │ │ tycho_simulation_py │ │ ┌────────────────────────┐ ┌──────────────────────┐ │ │ ┌──────────────────────┐ │ │ │ │ │ │ │ │ │ │ │ │ │ │ wrap │ │ │ │ │ │ │ @@ -44,10 +35,24 @@ wraps `evm` in order to allow using it in Python. └───────┘ ``` -_Editable -chart [here](https://asciiflow.com/#/share/eJyrVspLzE1VslIqKMovyS%2FOzI0vqFTSUcpJrEwtAopWxyhVxChZWZpY6sQoVQJZRuamQFZJakUJkBOjpBBUWlyiQDkIqCzJyM%2BLicl7NKXn0ZSGIY4mgLxEM59MAAdTE6VBDjWCgElAaZh1sBRiZZValhsPZJTmJJZk5uehqEdKRnisw6cKah1Vg28CihVUMXgCqpeoaio8CHBGDaoUToVQpzWhs3GrJNrq8qLEAlpaHQxPX6556Zl5qQpIobSHiJCEqZm2C9MoHI7DVEdGuGDlUTFc8FhNvygJSi0uzSmhSpRAjSIYJTB1o1GC02o9PT0KogSkmxjHYaobXFEyhXrVxgwUe4gxeA0RRqJ6iwhTp20izlRy2wXomnC1DLCoA1tJxRCnLiImByDFNIk%2BIcF0YDCRHi2YEYBdDSWGJ5Vm5qRAuLjaL6C2U2ZecUliTg5F1hGT0HeBXBWekZqaA9Qwh6aBS6TrZsQo1SrVAgD%2BnnnV)_ +## Installation + +### Local Installation + +1. Install Rust and Python. +2. Install tycho-client-py: [Installation guide](https://github.com/propeller-heads/tycho-indexer/tree/main/tycho-client-py) +3. Install tycho-simulation-py: + `pip install -e .` -## Building and installation +### Using PyPI + +On every release, we automatically push the package to PyPI. You can install it using pip: + +```shell +pip install tycho-simulation-py +``` + +## Building ### Build in `manylinux` docker image @@ -61,9 +66,6 @@ sudo ./tycho_simulation_py/build_tycho_simulation_wheel.sh A wheel file will be created in `tycho_simulation_py/target/wheels`. -In the script file, there's a commented out line for pushing the wheel to S3. Execute it if you want to publish the -wheel for defibot to use it. Be careful - this file will be immediately used by defibot CI! - ### Build locally The crate should be built using [maturin](https://www.maturin.rs/) tool. @@ -108,22 +110,6 @@ wheel in a different target environment. - Documentation on using this module to implement a `PoolState` in defibot: https://github.com/propeller-heads/defibot/blob/master/defibot/swaps/protosim/Readme.md -### Publish to code artifacts - -If you have a Mac Silicon or old Mac please build the wheels and publish them. This will help your colleagues as sooner -or later everyone will have to upgrade. - -Usually you should have a tycho-simulation-build environment where maturin is already installed. - -- Make sure the verison was bumped according to semver. -- If you don't have twine installed yet add it: - `pip install twine` -- Build the wheel in release mode: - `maturin build --release` -- Activate your credentials for aws code-artifact - `aws --region eu-central-1 codeartifact login --tool twine --domain propeller --domain-owner 827659017777 --repository protosim` -- Upload the wheel: - `twine upload --repository codeartifact target/wheels/tycho_simulation_py-[VERSION]*.whl` ### Troubleshooting @@ -143,3 +129,20 @@ Alternatively, you can control log level per module e.g. like this: `RUST_LOG=ty 3. On macOS, Try building with MACOSX_DEPLOYMENT_TARGET environment variable set. See [here](https://www.maturin.rs/environment-variables.html#other-environment-variables) and [here](https://www.maturin.rs/migration.html?highlight=MACOSX_DEPLOYMENT_TARGET#macos-deployment-target-version-defaults-what-rustc-supports). + +## Usage + +First, add `tycho-simulation-py` as a dependency on your project: + +``` +# requirements.txt + +tycho-simulation-py==0.32.0 (please update the version accordingly) +``` + + + +### What is this used for? + +As part of Tycho Ecosystem, this library is intented for users that want to quickly integrate Tycho simulations in their system. If you want to know more about Tycho, please [read Tycho docs](https://docs.propellerheads.xyz/tycho) + diff --git a/tycho_simulation_py/build_tycho_simulation_wheel.sh b/tycho_simulation_py/build_tycho_simulation_wheel.sh index 5e145231..bc4f9388 100755 --- a/tycho_simulation_py/build_tycho_simulation_wheel.sh +++ b/tycho_simulation_py/build_tycho_simulation_wheel.sh @@ -10,6 +10,3 @@ mkdir -p ./tycho_simulation_py/target/wheels/ docker build -t tycho_simulation_py_build -f tycho_simulation_py/tycho_simulation_py.Dockerfile . docker run -v $(pwd):/prop-builder tycho_simulation_py_build chmod -R 777 ./tycho_simulation_py/target/wheels/ - -# Do this if you want to publish the wheel. Note that CI uses this file! -# aws s3 cp ./tycho_simulation_py/target/wheels/tycho_simulation_py-0.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl s3://defibot-data/tycho_simulation_py-0.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl \ No newline at end of file diff --git a/tycho_simulation_py/protosim_evm_demo.py b/tycho_simulation_py/protosim_evm_demo.py index fe9a00ae..5e5b5e76 100644 --- a/tycho_simulation_py/protosim_evm_demo.py +++ b/tycho_simulation_py/protosim_evm_demo.py @@ -30,9 +30,7 @@ def test_simulation_db(): if eth_rpc_url is None: raise Exception("ETH_RPC_URL environment variable not set") - db = SimulationDB( - rpc_url=eth_rpc_url, block=None - ) + db = SimulationDB(rpc_url=eth_rpc_url, block=None) engine = SimulationEngine.new_with_simulation_db(db=db, trace=False) params = SimulationParameters( diff --git a/tycho_simulation_py/python/test/evm/utils.py b/tycho_simulation_py/python/test/evm/utils.py index 20961169..f6390df7 100644 --- a/tycho_simulation_py/python/test/evm/utils.py +++ b/tycho_simulation_py/python/test/evm/utils.py @@ -1,11 +1,16 @@ -from tycho_simulation_py.evm import AccountInfo, StateUpdate, BlockHeader, SimulationEngine +from tycho_simulation_py.evm import ( + AccountInfo, + StateUpdate, + BlockHeader, + SimulationEngine, +) from tycho_simulation_py.evm.constants import MAX_BALANCE from tycho_simulation_py.evm.utils import exec_rpc_method, get_code_for_address from tycho_simulation_py.models import Address, EVMBlock def read_account_storage_from_rpc( - address: Address, block_hash: str, connection_string: str = None + address: Address, block_hash: str, connection_string: str = None ) -> dict[str, str]: """Reads complete storage of a contract from a Geth instance. @@ -51,10 +56,10 @@ def read_account_storage_from_rpc( def init_contract_via_rpc( - block: EVMBlock, - contract_address: Address, - engine: SimulationEngine, - connection_string: str, + block: EVMBlock, + contract_address: Address, + engine: SimulationEngine, + connection_string: str, ): """Initializes a contract in the simulation engine using data fetched via RPC. @@ -97,11 +102,7 @@ def init_contract_via_rpc( ) engine.init_account( address=contract_address, - account=AccountInfo( - balance=MAX_BALANCE, - nonce=0, - code=bytecode, - ), + account=AccountInfo(balance=MAX_BALANCE, nonce=0, code=bytecode), mocked=False, permanent_storage=None, ) diff --git a/tycho_simulation_py/python/test/test_evm_token.py b/tycho_simulation_py/python/test/test_evm_token.py index 2419af9c..86711f66 100644 --- a/tycho_simulation_py/python/test/test_evm_token.py +++ b/tycho_simulation_py/python/test/test_evm_token.py @@ -4,9 +4,7 @@ from tycho_simulation_py.evm.storage import TychoDBSingleton from tycho_simulation_py.evm.token import brute_force_slots -from tycho_simulation_py.evm.utils import ( - create_engine, -) +from tycho_simulation_py.evm.utils import create_engine from test.evm.utils import init_contract_via_rpc from tycho_simulation_py.models import EthereumToken, EVMBlock diff --git a/tycho_simulation_py/python/tycho_simulation_py/assets/BalancerV2SwapAdapter.evm.runtime b/tycho_simulation_py/python/tycho_simulation_py/assets/BalancerV2SwapAdapter.evm.runtime new file mode 100644 index 00000000..24c0db98 Binary files /dev/null and b/tycho_simulation_py/python/tycho_simulation_py/assets/BalancerV2SwapAdapter.evm.runtime differ diff --git a/tycho_simulation_py/python/tycho_simulation_py/evm/adapter_contract.py b/tycho_simulation_py/python/tycho_simulation_py/evm/adapter_contract.py index 96f01745..0b0da622 100644 --- a/tycho_simulation_py/python/tycho_simulation_py/evm/adapter_contract.py +++ b/tycho_simulation_py/python/tycho_simulation_py/evm/adapter_contract.py @@ -68,14 +68,14 @@ def _decode_output(self, fname: str, encoded: list[int]) -> Any: return eth_abi.decode(types, bytearray(encoded)) def call( - self, - fname: str, - *args: list[Union[int, str, bool, bytes]], - block_number, - timestamp: int = None, - overrides: TStateOverwrites = None, - caller: Address = EXTERNAL_ACCOUNT, - value: int = 0, + self, + fname: str, + *args: list[Union[int, str, bool, bytes]], + block_number, + timestamp: int = None, + overrides: TStateOverwrites = None, + caller: Address = EXTERNAL_ACCOUNT, + value: int = 0, ) -> TychoSimulationResponse: call_data = self._encode_input(fname, *args) params = SimulationParameters( @@ -131,13 +131,13 @@ def __init__(self, address: Address, engine: SimulationEngine): super().__init__(address, "ISwapAdapter", engine) def price( - self, - pair_id: HexStr, - sell_token: EthereumToken, - buy_token: EthereumToken, - amounts: list[int], - block: EVMBlock, - overwrites: TStateOverwrites = None, + self, + pair_id: HexStr, + sell_token: EthereumToken, + buy_token: EthereumToken, + amounts: list[int], + block: EVMBlock, + overwrites: TStateOverwrites = None, ) -> list[Fraction]: args = [HexBytes(pair_id), sell_token.address, buy_token.address, amounts] res = self.call( @@ -150,14 +150,14 @@ def price( return list(map(lambda x: Fraction(*x), res.return_value[0])) def swap( - self, - pair_id: HexStr, - sell_token: EthereumToken, - buy_token: EthereumToken, - is_buy: bool, - amount: Decimal, - block: EVMBlock, - overwrites: TStateOverwrites = None, + self, + pair_id: HexStr, + sell_token: EthereumToken, + buy_token: EthereumToken, + is_buy: bool, + amount: Decimal, + block: EVMBlock, + overwrites: TStateOverwrites = None, ) -> tuple[Trade, dict[str, StateUpdate]]: args = [ HexBytes(pair_id), @@ -177,12 +177,12 @@ def swap( return Trade(amount, gas, Fraction(*price)), res.simulation_result.state_updates def get_limits( - self, - pair_id: HexStr, - sell_token: EthereumToken, - buy_token: EthereumToken, - block: EVMBlock, - overwrites: TStateOverwrites = None, + self, + pair_id: HexStr, + sell_token: EthereumToken, + buy_token: EthereumToken, + block: EVMBlock, + overwrites: TStateOverwrites = None, ) -> tuple[int, int]: args = [HexBytes(pair_id), sell_token.address, buy_token.address] res = self.call( @@ -195,7 +195,7 @@ def get_limits( return res.return_value[0] def get_capabilities( - self, pair_id: HexStr, sell_token: EthereumToken, buy_token: EthereumToken + self, pair_id: HexStr, sell_token: EthereumToken, buy_token: EthereumToken ) -> set[Capability]: args = [HexBytes(pair_id), sell_token.address, buy_token.address] res = self.call("getCapabilities", args, block_number=1) diff --git a/tycho_simulation_py/python/tycho_simulation_py/evm/constants.py b/tycho_simulation_py/python/tycho_simulation_py/evm/constants.py index 9dfc6ebb..d6d21f7d 100644 --- a/tycho_simulation_py/python/tycho_simulation_py/evm/constants.py +++ b/tycho_simulation_py/python/tycho_simulation_py/evm/constants.py @@ -5,6 +5,6 @@ EXTERNAL_ACCOUNT: Final[str] = "0xf847a638E44186F3287ee9F8cAF73FF4d4B80784" """This is a dummy address used as a transaction sender""" -UINT256_MAX: Final[int] = 2**256 - 1 +UINT256_MAX: Final[int] = 2 ** 256 - 1 MAX_BALANCE: Final[int] = UINT256_MAX // 2 """0.5 of the maximal possible balance to avoid overflow errors""" diff --git a/tycho_simulation_py/python/tycho_simulation_py/evm/decoders.py b/tycho_simulation_py/python/tycho_simulation_py/evm/decoders.py index 1660ef9f..2acee185 100644 --- a/tycho_simulation_py/python/tycho_simulation_py/evm/decoders.py +++ b/tycho_simulation_py/python/tycho_simulation_py/evm/decoders.py @@ -165,7 +165,9 @@ def decode_pool_state( adapter_contract_path=self.adapter_contract, trace=self.trace, manual_updates=manual_updates, - involved_contracts=set(to_checksum_address(b.hex()) for b in component.contract_ids), + involved_contracts=set( + to_checksum_address(b.hex()) for b in component.contract_ids + ), **optional_attributes, ) @@ -204,8 +206,8 @@ def decode_balances( ): balances = {} for addr, balance in balances_msg.items(): - checksum_addr = to_checksum_address(addr) - token = next(t for t in tokens if t.address == checksum_addr) + checksum_addr = addr.hex().lower() + token = next(t for t in tokens if t.address.lower() == checksum_addr) balances[token.address] = token.from_onchain_amount( int(balance) # balances are big endian encoded ) @@ -237,8 +239,10 @@ def apply_deltas( pool_id = self.component_pool_id.get(component_id, component_id) pool = pools[pool_id] for addr, token_balance in balance_update.items(): - checksum_addr = to_checksum_address(addr) - token = next(t for t in pool.tokens if t.address == checksum_addr) + checksum_addr = addr.lower() + token = next( + t for t in pool.tokens if t.address.lower() == checksum_addr + ) balance = token.from_onchain_amount( int.from_bytes(token_balance.balance, "big", signed=False) ) # balances are big endian encoded diff --git a/tycho_simulation_py/python/tycho_simulation_py/evm/pool_state.py b/tycho_simulation_py/python/tycho_simulation_py/evm/pool_state.py index 264a15cd..19e40939 100644 --- a/tycho_simulation_py/python/tycho_simulation_py/evm/pool_state.py +++ b/tycho_simulation_py/python/tycho_simulation_py/evm/pool_state.py @@ -82,9 +82,9 @@ def __init__( contract). If given, balances will be overwritten here instead of on the pool contract during simulations.""" - self.block_lasting_overwrites: defaultdict[Address, dict[int, int]] = ( - block_lasting_overwrites or defaultdict(dict) - ) + self.block_lasting_overwrites: defaultdict[ + Address, dict[int, int] + ] = block_lasting_overwrites or defaultdict(dict) """Storage overwrites that will be applied to all simulations. They will be cleared when ``clear_all_cache`` is called, i.e. usually at each block. Hence the name.""" @@ -100,7 +100,7 @@ def __init__( """A set of all contract addresses involved in the simulation of this pool.""" self.token_storage_slots: dict[Address, tuple[ERC20Slots, ContractCompiler]] = ( - token_storage_slots or {} + token_storage_slots or {} ) """Allows the specification of custom storage slots for token allowances and balances. This is particularly useful for token contracts involved in protocol @@ -181,12 +181,12 @@ def _set_marginal_prices(self): t1, [sell_amount], block=self.block, - overwrites=self._get_overwrites(t0,t1), + overwrites=self._get_overwrites(t0, t1), )[0] if Capability.ScaledPrices in self.capabilities: self.marginal_prices[(t0, t1)] = frac_to_decimal(frac) else: - scaled = frac * Fraction(10**t0.decimals, 10**t1.decimals) + scaled = frac * Fraction(10 ** t0.decimals, 10 ** t1.decimals) self.marginal_prices[(t0, t1)] = frac_to_decimal(scaled) def _ensure_capability(self, capability: Capability): @@ -302,11 +302,11 @@ def _get_token_overwrites( max_amount = sell_token.to_onchain_amount( self.get_sell_amount_limit(sell_token, buy_token) ) - slots, compiler = self.token_storage_slots.get(sell_token.address, (ERC20Slots(0, 1), ContractCompiler.Solidity)) + slots, compiler = self.token_storage_slots.get( + sell_token.address, (ERC20Slots(0, 1), ContractCompiler.Solidity) + ) overwrites = ERC20OverwriteFactory( - sell_token, - token_slots=slots, - compiler=compiler + sell_token, token_slots=slots, compiler=compiler ) overwrites.set_balance(max_amount, EXTERNAL_ACCOUNT) overwrites.set_allowance( diff --git a/tycho_simulation_py/python/tycho_simulation_py/evm/token.py b/tycho_simulation_py/python/tycho_simulation_py/evm/token.py index 42d99393..6b40f1cc 100644 --- a/tycho_simulation_py/python/tycho_simulation_py/evm/token.py +++ b/tycho_simulation_py/python/tycho_simulation_py/evm/token.py @@ -13,7 +13,7 @@ class SlotDetectionFailure(Exception): def brute_force_slots( - t: EthereumToken, block: EVMBlock, engine: SimulationEngine + t: EthereumToken, block: EVMBlock, engine: SimulationEngine ) -> tuple[ERC20Slots, ContractCompiler]: """Brute-force detection of storage slots for token allowances and balances. @@ -52,7 +52,9 @@ def brute_force_slots( compiler = ContractCompiler.Solidity for i in range(100): for compiler_flag in [ContractCompiler.Solidity, ContractCompiler.Vyper]: - overwrite_factory = ERC20OverwriteFactory(t, ERC20Slots(i, 1), compiler=compiler_flag) + overwrite_factory = ERC20OverwriteFactory( + t, ERC20Slots(i, 1), compiler=compiler_flag + ) overwrite_factory.set_balance(_MARKER_VALUE, EXTERNAL_ACCOUNT) res = token_contract.call( "balanceOf", @@ -76,23 +78,24 @@ def brute_force_slots( allowance_slot = None for i in range(100): - overwrite_factory = ERC20OverwriteFactory(t, ERC20Slots(0, i), compiler=compiler) - overwrite_factory.set_allowance(_MARKER_VALUE, _SPENDER, EXTERNAL_ACCOUNT) - res = token_contract.call( - "allowance", - [EXTERNAL_ACCOUNT, _SPENDER], - block_number=block.id, - timestamp=int(block.ts.timestamp()), - overrides=overwrite_factory.get_tycho_overwrites(), - caller=EXTERNAL_ACCOUNT, - value=0, - ) - if res.return_value is None: - continue - if res.return_value[0] == _MARKER_VALUE: - allowance_slot = i - break - + overwrite_factory = ERC20OverwriteFactory( + t, ERC20Slots(0, i), compiler=compiler + ) + overwrite_factory.set_allowance(_MARKER_VALUE, _SPENDER, EXTERNAL_ACCOUNT) + res = token_contract.call( + "allowance", + [EXTERNAL_ACCOUNT, _SPENDER], + block_number=block.id, + timestamp=int(block.ts.timestamp()), + overrides=overwrite_factory.get_tycho_overwrites(), + caller=EXTERNAL_ACCOUNT, + value=0, + ) + if res.return_value is None: + continue + if res.return_value[0] == _MARKER_VALUE: + allowance_slot = i + break if allowance_slot is None: raise SlotDetectionFailure(f"Failed to infer allowance slot for {t.address}") diff --git a/tycho_simulation_py/python/tycho_simulation_py/evm/utils.py b/tycho_simulation_py/python/tycho_simulation_py/evm/utils.py index 78480399..e423ef9c 100644 --- a/tycho_simulation_py/python/tycho_simulation_py/evm/utils.py +++ b/tycho_simulation_py/python/tycho_simulation_py/evm/utils.py @@ -29,7 +29,7 @@ def decode_tycho_exchange(exchange: str) -> str: def create_engine( - mocked_tokens: list[Address], trace: bool = False + mocked_tokens: list[Address], trace: bool = False ) -> SimulationEngine: """Create a simulation engine with a mocked ERC20 contract at given addresses. @@ -62,26 +62,34 @@ def create_engine( return engine + class ContractCompiler(enum.Enum): Solidity = enum.auto() Vyper = enum.auto() - + def compute_map_slot(self, map_base_slot: bytes, key: bytes) -> bytes: if self == ContractCompiler.Solidity: return eth_utils.keccak(key + map_base_slot) elif self == ContractCompiler.Vyper: return eth_utils.keccak(map_base_slot + key) else: - raise NotImplementedError(f"compute_map_slot not implemented for {self.name}") - - + raise NotImplementedError( + f"compute_map_slot not implemented for {self.name}" + ) + + class ERC20Slots(NamedTuple): balance_map: int allowance_map: int class ERC20OverwriteFactory: - def __init__(self, token: EthereumToken, token_slots: ERC20Slots = ERC20Slots(0, 1), compiler: ContractCompiler = ContractCompiler.Solidity): + def __init__( + self, + token: EthereumToken, + token_slots: ERC20Slots = ERC20Slots(0, 1), + compiler: ContractCompiler = ContractCompiler.Solidity, + ): """ Initialize the ERC20OverwriteFactory. @@ -103,7 +111,9 @@ def set_balance(self, balance: int, owner: Address): balance: The balance value. owner: The owner's address. """ - storage_index = get_storage_slot_at_key(HexStr(owner), self._balance_slot, self._contract_compiler) + storage_index = get_storage_slot_at_key( + HexStr(owner), self._balance_slot, self._contract_compiler + ) self._overwrites[storage_index] = balance log.log( 5, @@ -122,8 +132,11 @@ def set_allowance(self, allowance: int, spender: Address, owner: Address): """ storage_index = get_storage_slot_at_key( HexStr(spender), - get_storage_slot_at_key(HexStr(owner), self._allowance_slot, self._contract_compiler), - self._contract_compiler) + get_storage_slot_at_key( + HexStr(owner), self._allowance_slot, self._contract_compiler + ), + self._contract_compiler, + ) self._overwrites[storage_index] = allowance log.log( 5, @@ -172,7 +185,9 @@ def get_geth_overwrites(self) -> dict[Address, dict[int, int]]: return {self._token.address: {"stateDiff": formatted_overwrites, "code": code}} -def get_storage_slot_at_key(key: Address, mapping_slot: int, compiler = ContractCompiler.Solidity) -> int: +def get_storage_slot_at_key( + key: Address, mapping_slot: int, compiler=ContractCompiler.Solidity +) -> int: """Get storage slot index of a value stored at a certain key in a mapping Parameters @@ -183,10 +198,10 @@ def get_storage_slot_at_key(key: Address, mapping_slot: int, compiler = Contract mapping_slot Storage slot at which the mapping itself is stored. See the examples for more explanation. - + compiler - The compiler with which the target contract was compiled. Solidity and Vyper handle - maps differently. This defaults to Solidity because it's the most used. + The compiler with which the target contract was compiled. Solidity and Vyper handle + maps differently. This defaults to Solidity because it's the most used. Returns ------- @@ -306,7 +321,7 @@ def parse_solidity_error_message(data) -> str: def maybe_coerce_error( - err: RuntimeError, pool_state: Any, gas_limit: int = None + err: RuntimeError, pool_state: Any, gas_limit: int = None ) -> Exception: details = err.args[0] # we got bytes as data, so this was a revert diff --git a/tycho_simulation_py/python/tycho_simulation_py/models.py b/tycho_simulation_py/python/tycho_simulation_py/models.py index 09a619c8..08c05e61 100644 --- a/tycho_simulation_py/python/tycho_simulation_py/models.py +++ b/tycho_simulation_py/python/tycho_simulation_py/models.py @@ -49,7 +49,7 @@ def to_onchain_amount(self, amount: Union[float, Decimal, str]) -> int: log.warning(f"Expected variable of type Decimal. Got {type(amount)}.") with localcontext(Context(rounding=ROUND_FLOOR, prec=256)): - amount = Decimal(str(amount)) * (10**self.decimals) + amount = Decimal(str(amount)) * (10 ** self.decimals) try: amount = amount.quantize(Decimal("1.0")) except InvalidOperation: @@ -76,17 +76,17 @@ def from_onchain_amount( return ( Decimal(onchain_amount.numerator) / Decimal(onchain_amount.denominator) - / Decimal(10**self.decimals) + / Decimal(10 ** self.decimals) ).quantize(Decimal(f"{1 / 10 ** self.decimals}")) if quantize is True: try: amount = ( - Decimal(str(onchain_amount)) / 10**self.decimals + Decimal(str(onchain_amount)) / 10 ** self.decimals ).quantize(Decimal(f"{1 / 10 ** self.decimals}")) except InvalidOperation: - amount = Decimal(str(onchain_amount)) / Decimal(10**self.decimals) + amount = Decimal(str(onchain_amount)) / Decimal(10 ** self.decimals) else: - amount = Decimal(str(onchain_amount)) / Decimal(10**self.decimals) + amount = Decimal(str(onchain_amount)) / Decimal(10 ** self.decimals) return amount def __repr__(self): diff --git a/tycho_simulation_py/python/tycho_simulation_py/quickstart.py b/tycho_simulation_py/python/tycho_simulation_py/quickstart.py new file mode 100644 index 00000000..4566469b --- /dev/null +++ b/tycho_simulation_py/python/tycho_simulation_py/quickstart.py @@ -0,0 +1,125 @@ +import asyncio +from decimal import Decimal +from logging import getLogger +from typing import Optional + +from tycho_indexer_client import TychoStream, TychoRPCClient +import tycho_indexer_client.dto as tycho_models +import os + +from tycho_simulation_py.evm.decoders import ThirdPartyPoolTychoDecoder +from tycho_simulation_py.evm.pool_state import ThirdPartyPool +from tycho_simulation_py.models import EthereumToken, EVMBlock + +log = getLogger(__name__) + + +class TokenFactory: + def __init__(self, tokens: Optional[dict[str, EthereumToken]] = None): + self.tokens = tokens + + def get_token(self, address: str) -> Optional[EthereumToken]: + if address not in self.tokens: + return None + return self.tokens[address] + + def get_tokens(self, addresses: list[str]) -> list[EthereumToken]: + return [self.get_token(addr) for addr in addresses] + + +def load_all_tokens(rpc_client: TychoRPCClient) -> dict[str, EthereumToken]: + page = 0 + tokens = dict() + log.info("Loading all tokens from Tycho.") + while True: + params = tycho_models.TokensParams( + pagination=tycho_models.PaginationParams(page=page, page_size=1000), + min_quality=50, + traded_n_days_ago=30, + ) + + res = rpc_client.get_tokens(params) + if not res: + break + + for token in res: + address = token.address.hex().lower() + tokens[address] = EthereumToken(token.symbol, address, token.decimals) + + page += 1 + return tokens + + +async def run_example(): + curdir = os.path.dirname(os.path.abspath(__file__)) + "/" + auth_token = os.getenv("TYCHO_AUTH_KEY", "sampletoken") + + tycho_stream = TychoStream( + "tycho-beta.propellerheads.xyz", + ["vm:balancer"], + blockchain=tycho_models.Chain.ethereum, + auth_token=auth_token, + include_state=True, + logs_directory=curdir + "logs", + min_tvl=Decimal("1000"), + ) + rpc_client = TychoRPCClient( + rpc_url="https://tycho-beta.propellerheads.xyz", auth_token=auth_token + ) + + all_tokens = load_all_tokens(rpc_client) + + token_factory = TokenFactory(all_tokens) + + decoder = ThirdPartyPoolTychoDecoder( + token_factory.get_tokens, + adapter_contract=curdir + "assets/BalancerV2SwapAdapter.evm.runtime", + minimum_gas=72300, + trace=False, + ) + + # Starts Tycho Stream. This will run a subprocess that connects to the Tycho Indexer and streams data. + await tycho_stream.start() + + pools: dict[str, ThirdPartyPool] = dict() + + removed_pools = set() + decoded_count = 0 + total_count = 0 + n_new_tokens = 0 + + async for msg in tycho_stream: + for exchange, sync_msg in msg.sync_states.items(): + if sync_msg.status.lower() != "ready": + log.warning( + f"Exchange {exchange} is not ready! Current status is: {sync_msg.status}" + ) + block: EVMBlock = EVMBlock( + msg.sync_states["vm:balancer"].header.number, + msg.sync_states["vm:balancer"].header.hash.hex(), + ) + + for exchange, state_msg in msg.state_msgs.items(): + new_pools = decoder.decode_snapshot(state_msg.snapshots, block) + updates = decoder.apply_deltas(pools, state_msg.deltas, block) + + log.info(f"Found {len(new_pools)} new pools for {exchange}.") + log.info(f"Updated {len(updates)} pools for {exchange}.") + + pools.update(new_pools) + pools.update(updates) + for pool_id in removed_pools: + pools.pop(pool_id, None) + + for pool_id, pool in pools.items(): + print(f"Block: {block.id}") + print( + f"Pool {pool_id}. " + f"Token0: {pool.tokens[0].symbol} ({pool.tokens[0].address}). " + f"Token1: {pool.tokens[1].symbol} ({pool.tokens[1].address}). " + f"Spot prices: {pool.marginal_prices}. " + ) + + +if __name__ == "__main__": + asyncio.run(run_example())