From 5e6c7d4647a6f9df98999d087997611033a0caaf Mon Sep 17 00:00:00 2001 From: Thales Lima Date: Wed, 17 Jul 2024 23:27:36 +0200 Subject: [PATCH] Add relative imports, small bugfixes --- testing/docker-compose.yaml | 4 +- testing/postgres.Dockerfile | 16 + testing/tycho.py | 6 +- tycho/tycho/models.py | 68 ---- {tycho => tycho_client}/README.md | 0 {tycho/tycho => tycho_client}/__init__.py | 0 {tycho => tycho_client}/requirements.txt | 0 tycho_client/tycho/__init__.py | 0 .../tycho/adapter_contract.py | 6 +- .../tycho/assets/CurveSwapAdapter.evm.runtime | Bin 0 -> 9565 bytes tycho_client/tycho/assets/ERC20.bin | Bin 0 -> 1918 bytes tycho_client/tycho/assets/IERC20.sol | 78 ++++ tycho_client/tycho/assets/mocked_ERC20.sol | 363 ++++++++++++++++++ {tycho => tycho_client}/tycho/constants.py | 2 +- {tycho => tycho_client}/tycho/decoders.py | 10 +- {tycho => tycho_client}/tycho/exceptions.py | 0 tycho_client/tycho/models.py | 108 ++++++ {tycho => tycho_client}/tycho/pool_state.py | 20 +- .../tycho/tycho_adapter.py | 71 +++- {tycho => tycho_client}/tycho/tycho_db.py | 0 {tycho => tycho_client}/tycho/utils.py | 10 +- 21 files changed, 651 insertions(+), 111 deletions(-) create mode 100644 testing/postgres.Dockerfile delete mode 100644 tycho/tycho/models.py rename {tycho => tycho_client}/README.md (100%) rename {tycho/tycho => tycho_client}/__init__.py (100%) rename {tycho => tycho_client}/requirements.txt (100%) create mode 100644 tycho_client/tycho/__init__.py rename {tycho => tycho_client}/tycho/adapter_contract.py (97%) create mode 100644 tycho_client/tycho/assets/CurveSwapAdapter.evm.runtime create mode 100644 tycho_client/tycho/assets/ERC20.bin create mode 100644 tycho_client/tycho/assets/IERC20.sol create mode 100644 tycho_client/tycho/assets/mocked_ERC20.sol rename {tycho => tycho_client}/tycho/constants.py (88%) rename {tycho => tycho_client}/tycho/decoders.py (94%) rename {tycho => tycho_client}/tycho/exceptions.py (100%) create mode 100644 tycho_client/tycho/models.py rename {tycho => tycho_client}/tycho/pool_state.py (96%) rename {tycho => tycho_client}/tycho/tycho_adapter.py (83%) rename {tycho => tycho_client}/tycho/tycho_db.py (100%) rename {tycho => tycho_client}/tycho/utils.py (97%) diff --git a/testing/docker-compose.yaml b/testing/docker-compose.yaml index c618ad4..a8a3b91 100644 --- a/testing/docker-compose.yaml +++ b/testing/docker-compose.yaml @@ -1,12 +1,14 @@ version: '3.1' services: db: - image: ghcr.io/dbsystel/postgresql-partman:15-5 + build: + dockerfile: postgres.Dockerfile restart: "always" environment: POSTGRESQL_PASSWORD: mypassword POSTGRESQL_DATABASE: tycho_indexer_0 POSTGRESQL_USERNAME: postgres + POSTGRESQL_SHARED_PRELOAD_LIBRARIES: pg_cron ports: - "5431:5432" volumes: diff --git a/testing/postgres.Dockerfile b/testing/postgres.Dockerfile new file mode 100644 index 0000000..1e01ea8 --- /dev/null +++ b/testing/postgres.Dockerfile @@ -0,0 +1,16 @@ +# This Dockerfile creates a custom postgres image used for CI and local deployment. +# This is required because we use some postgres extensions that aren't in the generic Postgres image such as pg_partman or pg_cron. + +# As an image with pg_partman already exist, we start from this one an add pg_cron and possibly other extensions on top of that. +FROM ghcr.io/dbsystel/postgresql-partman:15-5 +ARG PGCRON_VERSION="1.6.2" +USER root +RUN cd /tmp \ + && wget "https://github.com/citusdata/pg_cron/archive/refs/tags/v${PGCRON_VERSION}.tar.gz" \ + && tar zxf v${PGCRON_VERSION}.tar.gz \ + && cd pg_cron-${PGCRON_VERSION} \ + && make \ + && make install \ + && cd .. && rm -r pg_cron-${PGCRON_VERSION} v${PGCRON_VERSION}.tar.gz +RUN echo "cron.database_name = 'tycho_indexer_0'" >> /opt/bitnami/postgresql/conf/postgresql.conf +USER 1001 \ No newline at end of file diff --git a/testing/tycho.py b/testing/tycho.py index 8408cc7..f5c7b76 100644 --- a/testing/tycho.py +++ b/testing/tycho.py @@ -7,7 +7,9 @@ import os import psycopg2 from psycopg2 import sql -binary_path = "./testing/tycho-indexer" +from pathlib import Path + +binary_path = Path(__file__).parent / "tycho-indexer" class TychoRunner: @@ -48,7 +50,7 @@ class TychoRunner: bufsize=1, env=env, ) - + with process.stdout: for line in iter(process.stdout.readline, ""): if line and self.with_binary_logs: diff --git a/tycho/tycho/models.py b/tycho/tycho/models.py deleted file mode 100644 index 1362d57..0000000 --- a/tycho/tycho/models.py +++ /dev/null @@ -1,68 +0,0 @@ -import datetime -from dataclasses import dataclass -from enum import Enum, IntEnum, auto -from typing import Union - -from pydantic import BaseModel, Field - -from tycho.tycho.pool_state import ThirdPartyPool - -Address = str - - -class Blockchain(Enum): - ethereum = "ethereum" - arbitrum = "arbitrum" - polygon = "polygon" - zksync = "zksync" - - -class EVMBlock(BaseModel): - id: int - ts: datetime.datetime = Field(default_factory=datetime.datetime.utcnow) - hash_: str - - -class EthereumToken(BaseModel): - symbol: str - address: str - decimals: int - gas: Union[int, list[int]] = 29000 - - -class DatabaseType(Enum): - # Make call to the node each time it needs a storage (unless cached from a previous call). - rpc_reader = "rpc_reader" - # Connect to Tycho and cache the whole state of a target contract, the state is continuously updated by Tycho. - # To use this we need Tycho to be configured to index the target contract state. - tycho = "tycho" - - -class Capability(IntEnum): - SellSide = auto() - BuySide = auto() - PriceFunction = auto() - FeeOnTransfer = auto() - ConstantPrice = auto() - TokenBalanceIndependent = auto() - ScaledPrice = auto() - - -class SynchronizerState(Enum): - started = "started" - ready = "ready" - stale = "stale" - delayed = "delayed" - advanced = "advanced" - ended = "ended" - - -@dataclass(repr=False) -class BlockProtocolChanges: - block: EVMBlock - pool_states: dict[Address, ThirdPartyPool] - """All updated pools""" - removed_pools: set[Address] - sync_states: dict[str, SynchronizerState] - deserialization_time: float - """The time it took to deserialize the pool states from the tycho feed message""" diff --git a/tycho/README.md b/tycho_client/README.md similarity index 100% rename from tycho/README.md rename to tycho_client/README.md diff --git a/tycho/tycho/__init__.py b/tycho_client/__init__.py similarity index 100% rename from tycho/tycho/__init__.py rename to tycho_client/__init__.py diff --git a/tycho/requirements.txt b/tycho_client/requirements.txt similarity index 100% rename from tycho/requirements.txt rename to tycho_client/requirements.txt diff --git a/tycho_client/tycho/__init__.py b/tycho_client/tycho/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tycho/tycho/adapter_contract.py b/tycho_client/tycho/adapter_contract.py similarity index 97% rename from tycho/tycho/adapter_contract.py rename to tycho_client/tycho/adapter_contract.py index bcf2d74..9015cb8 100644 --- a/tycho/tycho/adapter_contract.py +++ b/tycho_client/tycho/adapter_contract.py @@ -17,9 +17,9 @@ from protosim_py import ( StateUpdate, ) -from tycho.tycho.constants import EXTERNAL_ACCOUNT -from tycho.tycho.models import Address, EthereumToken, EVMBlock, Capability -from tycho.tycho.utils import load_abi, maybe_coerce_error +from .constants import EXTERNAL_ACCOUNT +from .models import Address, EthereumToken, EVMBlock, Capability +from .utils import load_abi, maybe_coerce_error log = logging.getLogger(__name__) diff --git a/tycho_client/tycho/assets/CurveSwapAdapter.evm.runtime b/tycho_client/tycho/assets/CurveSwapAdapter.evm.runtime new file mode 100644 index 0000000000000000000000000000000000000000..517f6387728ab2ea8f2bd6443cd199a896c00930 GIT binary patch literal 9565 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 bUs!c{sqp^r|GYV~XV>=WOX`~Y>Q?_h$O)Rl literal 0 HcmV?d00001 diff --git a/tycho_client/tycho/assets/ERC20.bin b/tycho_client/tycho/assets/ERC20.bin new file mode 100644 index 0000000000000000000000000000000000000000..d791f43ea680b19a978df56cc36ae5553fff5b8d GIT binary patch literal 1918 zcmah~U1%It6rMXXyS0kA37f%~&>b5ELzG(E)TBsQZTvw|C&Ok(JHfd(8;k|1sa1-U zn3+2>YabNvY*IzyL)2n1AlO92>Vvf{Cio(KFe)1Rqp@Nu#g?KEMm_h=Y&P+y&cMt! z_kQQx^PPLn9qi(~xzTMP(p8q@F4_iGH)V+QrscHb6U}a^(-`fkBhq#&hFOAg0EZ^% zdVse8-23JD?Etp|+-DvB0$@MDw^FZu2=Hlu^Zh?}0o)_pyQTEvw@Q{slQ6UTk^R#E zX8~TVT{#c%48WH6pX6{6;QHGZ2EjCA**r8NO%XI+v~j+NNFPOvqz;M?H&WOInEjea zbDOYA(z&8dj(_sTP|>8RvY0)=n7$UH#h1QM!5cOjGJn12Z1J?BPcJ1 zT;L>p;ylv}jPs_5yobMGk)fK4my;pCHgrDm`jQx$fG_z^aE-Cz76_VT zBz>%CyEuoi?nbH%f`krunrslkEien2*gR&M?{7MREZRby9M#yZ2AB` z`JY7J(;U8q%hn!yPICiu43?)%4_Pnk2|RP;dHdwTnX|+8u?KhDF}x|08kVo{DO9xVP+A}= z?|)u9Eb28#YBPfpt1(x$q7a;F7K?UfH-5LGcEU>uu6hhsHaCOjV7*K0fuvN?!m?Ws z<0SP|EtaIJ^yF9o3Ia>7>R9ua(8&CG^X;Znv*X(izw_Adx4tmBw{8C~rK7KDH(cC$ z|HYQ+b3RlvQ9g^HP;|t*5zYeN^m|+yBGa*BmET*ThkOLh2xkK_DI%#^NTh@Fw2Yud z@;KLsb3W2Sges8VxPRUDY8ajdMUuWooYfIesssNAKUC@)Ncv_mO46M^NvlPB3`2*E zqITZkoabO1lm+qOgxDVlV#OfoA*_c;d_@M%2dJGH6@CjDG!SMiF?)py;wX=AMF0m9 z!Uu5x5%`_OdJy{+4&yTLyk<2d7Nea#CGF`s4M1Z9|Xld}wXGe*EK>&%+xlcm1Tjcjxr3ceiX`w=3vq d?E7TyX#0;doAuKJJD+&Gv_-lst4RNM{RKA5iOc{1 literal 0 HcmV?d00001 diff --git a/tycho_client/tycho/assets/IERC20.sol b/tycho_client/tycho/assets/IERC20.sol new file mode 100644 index 0000000..a19535a --- /dev/null +++ b/tycho_client/tycho/assets/IERC20.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol) + +pragma solidity ^0.8.19; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 amount) external returns (bool); +} diff --git a/tycho_client/tycho/assets/mocked_ERC20.sol b/tycho_client/tycho/assets/mocked_ERC20.sol new file mode 100644 index 0000000..1c0d7f3 --- /dev/null +++ b/tycho_client/tycho/assets/mocked_ERC20.sol @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol) + +pragma solidity ^0.8.19; + +import "./IERC20.sol"; + + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} + +/** + * @dev Interface for the optional metadata functions from the ERC20 standard. + * + * _Available since v4.1._ + */ +interface IERC20Metadata is IERC20 { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + */ + function decimals() external view returns (uint8); +} + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * + * TIP: For a detailed writeup see our guide + * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * The default value of {decimals} is 18. To change this, you should override + * this function so it returns a different value. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead returning `false` on failure. This behavior is nonetheless + * conventional and does not conflict with the expectations of ERC20 + * applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract ERC20 is Context, IERC20, IERC20Metadata { + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + uint8 private _decimals; + + /** + * @dev Sets the values for {name}, {symbol} and {decimals}. + * + * All three of these values are immutable: they can only be set once during + * construction. + */ + constructor(string memory name_, string memory symbol_, uint8 decimals_) { + _name = name_; + _symbol = symbol_; + _decimals = decimals_; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual returns (uint8) { + return _decimals; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address to, uint256 amount) public virtual returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + * - the caller must have allowance for ``from``'s tokens of at least + * `amount`. + */ + function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, allowance(owner, spender) + addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + address owner = _msgSender(); + uint256 currentAllowance = allowance(owner, spender); + require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); + unchecked { + _approve(owner, spender, currentAllowance - subtractedValue); + } + + return true; + } + + /** + * @dev Moves `amount` of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _transfer(address from, address to, uint256 amount) internal { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + _update(from, to, amount); + } + + /** + * @dev Transfers `amount` of tokens from `from` to `to`, or alternatively mints (or burns) if `from` (or `to`) is + * the zero address. All customizations to transfers, mints, and burns should be done by overriding this function. + * + * Emits a {Transfer} event. + */ + function _update(address from, address to, uint256 amount) internal virtual { + if (from == address(0)) { + _totalSupply += amount; + } else { + uint256 fromBalance = _balances[from]; + require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); + unchecked { + // Overflow not possible: amount <= fromBalance <= totalSupply. + _balances[from] = fromBalance - amount; + } + } + + if (to == address(0)) { + unchecked { + // Overflow not possible: amount <= totalSupply or amount <= fromBalance <= totalSupply. + _totalSupply -= amount; + } + } else { + unchecked { + // Overflow not possible: balance + amount is at most totalSupply, which we know fits into a uint256. + _balances[to] += amount; + } + } + + emit Transfer(from, to, amount); + } + + /** + * @dev Creates `amount` tokens and assigns them to `account`, by transferring it from address(0). + * Relies on the `_update` mechanism + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _mint(address account, uint256 amount) internal { + require(account != address(0), "ERC20: mint to the zero address"); + _update(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, by transferring it to address(0). + * Relies on the `_update` mechanism. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead + */ + function _burn(address account, uint256 amount) internal { + require(account != address(0), "ERC20: burn from the zero address"); + _update(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `amount`. + * + * Does not update the allowance amount in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Might emit an {Approval} event. + */ + function _spendAllowance(address owner, address spender, uint256 amount) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + require(currentAllowance >= amount, "ERC20: insufficient allowance"); + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } +} diff --git a/tycho/tycho/constants.py b/tycho_client/tycho/constants.py similarity index 88% rename from tycho/tycho/constants.py rename to tycho_client/tycho/constants.py index 1dc7c1a..5ec8c6d 100644 --- a/tycho/tycho/constants.py +++ b/tycho_client/tycho/constants.py @@ -1,7 +1,7 @@ from pathlib import Path from typing import Final -TYCHO_CLIENT_FOLDER = Path(__file__) / "bins" +TYCHO_CLIENT_FOLDER = Path(__file__).parent / "bins" TYCHO_CLIENT_LOG_FOLDER = TYCHO_CLIENT_FOLDER / "logs" EXTERNAL_ACCOUNT: Final[str] = "0xf847a638E44186F3287ee9F8cAF73FF4d4B80784" diff --git a/tycho/tycho/decoders.py b/tycho_client/tycho/decoders.py similarity index 94% rename from tycho/tycho/decoders.py rename to tycho_client/tycho/decoders.py index 160243f..b2ba9dc 100644 --- a/tycho/tycho/decoders.py +++ b/tycho_client/tycho/decoders.py @@ -2,10 +2,10 @@ from decimal import Decimal from logging import getLogger from typing import Any -from tycho.tycho.exceptions import TychoDecodeError -from tycho.tycho.models import EVMBlock, EthereumToken -from tycho.tycho.pool_state import ThirdPartyPool -from tycho.tycho.utils import decode_tycho_exchange +from .exceptions import TychoDecodeError +from .models import EVMBlock, EthereumToken +from .pool_state import ThirdPartyPool +from .utils import decode_tycho_exchange log = getLogger(__name__) @@ -98,7 +98,7 @@ class ThirdPartyPoolTychoDecoder: @staticmethod def apply_update( - pool: ThirdPartyPool, + pool: ThirdPartyPool, pool_update: dict[str, Any], balance_updates: dict[str, Any], block: EVMBlock, diff --git a/tycho/tycho/exceptions.py b/tycho_client/tycho/exceptions.py similarity index 100% rename from tycho/tycho/exceptions.py rename to tycho_client/tycho/exceptions.py diff --git a/tycho_client/tycho/models.py b/tycho_client/tycho/models.py new file mode 100644 index 0000000..ddf7967 --- /dev/null +++ b/tycho_client/tycho/models.py @@ -0,0 +1,108 @@ +import datetime +from decimal import Decimal, localcontext, Context, ROUND_FLOOR, InvalidOperation +from enum import Enum, IntEnum, auto +from fractions import Fraction +from logging import getLogger +from typing import Union + +from pydantic import BaseModel, Field + +Address = str + +log = getLogger(__name__) + + +class Blockchain(Enum): + ethereum = "ethereum" + arbitrum = "arbitrum" + polygon = "polygon" + zksync = "zksync" + + +class EVMBlock(BaseModel): + id: int + ts: datetime.datetime = Field(default_factory=datetime.datetime.utcnow) + hash_: str + + +class EthereumToken(BaseModel): + symbol: str + address: str + decimals: int + gas: Union[int, list[int]] = 29000 + + def to_onchain_amount(self, amount: Union[float, Decimal, str]) -> int: + """Converts floating-point numerals to an integer, by shifting right by the + token's maximum amount of decimals (e.g.: 1.000000 becomes 1000000). + For the reverse operation please see self.from_onchain_amount + """ + if not isinstance(amount, Decimal): + 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) + try: + amount = amount.quantize(Decimal("1.0")) + except InvalidOperation: + log.error( + f"Quantize failed for {self.symbol}, {amount}, {self.decimals}" + ) + return int(amount) + + def from_onchain_amount( + self, onchain_amount: Union[int, Fraction], quantize: bool = True + ) -> Decimal: + """Converts an Integer to a quantized decimal, by shifting left by the token's + maximum amount of decimals (e.g.: 1000000 becomes 1.000000 for a 6-decimal token + For the reverse operation please see self.to_onchain_amount + + If the onchain_amount is too low, then using quantize can underflow without + raising and the offchain amount returned is 0. + See _decimal.Decimal.quantize docstrings for details. + + Quantize is needed for UniswapV2. + """ + with localcontext(self._dec_context): + if isinstance(onchain_amount, Fraction): + return ( + Decimal(onchain_amount.numerator) + / Decimal(onchain_amount.denominator) + / Decimal(10 ** self.decimals) + ).quantize(Decimal(f"{1 / 10 ** self.decimals}")) + if quantize is True: + try: + amount = ( + 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) + else: + amount = Decimal(str(onchain_amount)) / Decimal(10 ** self.decimals) + return amount + + +class DatabaseType(Enum): + # Make call to the node each time it needs a storage (unless cached from a previous call). + rpc_reader = "rpc_reader" + # Connect to Tycho and cache the whole state of a target contract, the state is continuously updated by Tycho. + # To use this we need Tycho to be configured to index the target contract state. + tycho = "tycho" + + +class Capability(IntEnum): + SellSide = auto() + BuySide = auto() + PriceFunction = auto() + FeeOnTransfer = auto() + ConstantPrice = auto() + TokenBalanceIndependent = auto() + ScaledPrice = auto() + + +class SynchronizerState(Enum): + started = "started" + ready = "ready" + stale = "stale" + delayed = "delayed" + advanced = "advanced" + ended = "ended" diff --git a/tycho/tycho/pool_state.py b/tycho_client/tycho/pool_state.py similarity index 96% rename from tycho/tycho/pool_state.py rename to tycho_client/tycho/pool_state.py index d31f914..ea08ea2 100644 --- a/tycho/tycho/pool_state.py +++ b/tycho_client/tycho/pool_state.py @@ -5,17 +5,17 @@ from copy import deepcopy from decimal import Decimal from fractions import Fraction from logging import getLogger -from typing import Optional, cast, TypeVar +from typing import Optional, cast, TypeVar, Annotated, DefaultDict from eth_typing import HexStr from protosim_py import SimulationEngine, AccountInfo from pydantic import BaseModel, PrivateAttr, Field -from tycho.tycho.adapter_contract import AdapterContract -from tycho.tycho.constants import MAX_BALANCE, EXTERNAL_ACCOUNT -from tycho.tycho.exceptions import RecoverableSimulationException -from tycho.tycho.models import EVMBlock, Capability, Address, EthereumToken -from tycho.tycho.utils import ( +from .adapter_contract import AdapterContract +from .constants import MAX_BALANCE, EXTERNAL_ACCOUNT +from .exceptions import RecoverableSimulationException +from .models import EVMBlock, Capability, Address, EthereumToken +from .utils import ( create_engine, get_contract_bytecode, frac_to_decimal, @@ -54,9 +54,11 @@ class ThirdPartyPool(BaseModel): """The contract address for where protocol balances are stored (i.e. a vault contract). If given, balances will be overwritten here instead of on the pool contract during simulations.""" - block_lasting_overwrites: defaultdict[Address, dict[int, int]] = Field( - default_factory=lambda: defaultdict(dict) - ) + block_lasting_overwrites: DefaultDict[ + Address, + Annotated[dict[int, int], Field(default_factory=lambda: 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.""" diff --git a/tycho/tycho/tycho_adapter.py b/tycho_client/tycho/tycho_adapter.py similarity index 83% rename from tycho/tycho/tycho_adapter.py rename to tycho_client/tycho/tycho_adapter.py index 1f8bea4..cfe102c 100644 --- a/tycho/tycho/tycho_adapter.py +++ b/tycho_client/tycho/tycho_adapter.py @@ -4,6 +4,7 @@ import platform import time from asyncio.subprocess import STDOUT, PIPE from collections import defaultdict +from dataclasses import dataclass from datetime import datetime from decimal import Decimal from http.client import HTTPException @@ -13,18 +14,13 @@ from typing import Any, Optional, Dict import requests from protosim_py import AccountUpdate, AccountInfo, BlockHeader -from tycho.tycho.constants import TYCHO_CLIENT_LOG_FOLDER, TYCHO_CLIENT_FOLDER -from tycho.tycho.decoders import ThirdPartyPoolTychoDecoder -from tycho.tycho.exceptions import APIRequestError, TychoClientException -from tycho.tycho.models import ( - Blockchain, - EVMBlock, - EthereumToken, - BlockProtocolChanges, - SynchronizerState, -) -from tycho.tycho.tycho_db import TychoDBSingleton -from tycho.tycho.utils import create_engine +from .pool_state import ThirdPartyPool +from .constants import TYCHO_CLIENT_LOG_FOLDER, TYCHO_CLIENT_FOLDER +from .decoders import ThirdPartyPoolTychoDecoder +from .exceptions import APIRequestError, TychoClientException +from .models import Blockchain, EVMBlock, EthereumToken, SynchronizerState, Address +from .tycho_db import TychoDBSingleton +from .utils import create_engine log = getLogger(__name__) @@ -34,7 +30,7 @@ class TokenLoader: self, tycho_url: str, blockchain: Blockchain, - min_token_quality: Optional[int] = 51, + min_token_quality: Optional[int] = 0, ): self.tycho_url = tycho_url self.blockchain = blockchain @@ -70,6 +66,34 @@ class TokenLoader: return formatted_tokens + def get_token_subset(self, addresses: list[str]) -> dict[str, EthereumToken]: + """Loads a subset of tokens from Tycho RPC""" + url = self.tycho_url + self.endpoint.format(self.blockchain.value) + page = 0 + + start = time.monotonic() + all_tokens = [] + while data := self._get_all_with_pagination( + url=url, + page=page, + limit=self._token_limit, + params={"min_quality": self.min_token_quality, "addresses": addresses}, + ): + all_tokens.extend(data) + page += 1 + if len(data) < self._token_limit: + break + + log.info(f"Loaded {len(all_tokens)} tokens in {time.monotonic() - start:.2f}s") + + formatted_tokens = dict() + + for token in all_tokens: + formatted = EthereumToken(**token) + formatted_tokens[formatted.address] = formatted + + return formatted_tokens + @staticmethod def _get_all_with_pagination( url: str, params: Optional[Dict] = None, page: int = 0, limit: int = 50 @@ -87,6 +111,17 @@ class TokenLoader: return r.json()["tokens"] +@dataclass(repr=False) +class BlockProtocolChanges: + block: EVMBlock + pool_states: dict[Address, ThirdPartyPool] + """All updated pools""" + removed_pools: set[Address] + sync_states: dict[str, SynchronizerState] + deserialization_time: float + """The time it took to deserialize the pool states from the tycho feed message""" + + class TychoPoolStateStreamAdapter: def __init__( self, @@ -95,7 +130,7 @@ class TychoPoolStateStreamAdapter: decoder: ThirdPartyPoolTychoDecoder, blockchain: Blockchain, min_tvl: Optional[Decimal] = 10, - min_token_quality: Optional[int] = 51, + min_token_quality: Optional[int] = 0, include_state=True, ): """ @@ -122,7 +157,7 @@ class TychoPoolStateStreamAdapter: # Loads tokens from Tycho self._tokens: dict[str, EthereumToken] = TokenLoader( - tycho_url=self.tycho_url, + tycho_url=f"http://{self.tycho_url}", blockchain=self._blockchain, min_token_quality=self.min_token_quality, ).get_tokens() @@ -139,11 +174,11 @@ class TychoPoolStateStreamAdapter: cmd = [ "--log-folder", - TYCHO_CLIENT_LOG_FOLDER, + str(TYCHO_CLIENT_LOG_FOLDER), "--tycho-url", self.tycho_url, "--min-tvl", - self.min_tvl, + str(self.min_tvl), ] if not self._include_state: cmd.append("--no-state") @@ -152,7 +187,7 @@ class TychoPoolStateStreamAdapter: log.debug(f"Starting tycho-client binary at {bin_path}. CMD: {cmd}") self.tycho_client = await asyncio.create_subprocess_exec( - bin_path, *cmd, stdout=PIPE, stderr=STDOUT, limit=2 ** 64 + str(bin_path), *cmd, stdout=PIPE, stderr=STDOUT, limit=2 ** 64 ) @staticmethod diff --git a/tycho/tycho/tycho_db.py b/tycho_client/tycho/tycho_db.py similarity index 100% rename from tycho/tycho/tycho_db.py rename to tycho_client/tycho/tycho_db.py diff --git a/tycho/tycho/utils.py b/tycho_client/tycho/utils.py similarity index 97% rename from tycho/tycho/utils.py rename to tycho_client/tycho/utils.py index ac30a9b..e1ecab2 100644 --- a/tycho/tycho/utils.py +++ b/tycho_client/tycho/utils.py @@ -13,10 +13,10 @@ from hexbytes import HexBytes from protosim_py import SimulationEngine, AccountInfo from web3 import Web3 -from tycho.tycho.constants import EXTERNAL_ACCOUNT, MAX_BALANCE -from tycho.tycho.exceptions import OutOfGas -from tycho.tycho.models import Address, EthereumToken -from tycho.tycho.tycho_db import TychoDBSingleton +from .constants import EXTERNAL_ACCOUNT, MAX_BALANCE +from .exceptions import OutOfGas +from .models import Address, EthereumToken +from .tycho_db import TychoDBSingleton log = getLogger(__name__) @@ -182,6 +182,8 @@ def get_storage_slot_at_key(key: Address, mapping_slot: int) -> int: @lru_cache def get_contract_bytecode(name: str) -> bytes: + """Load contract bytecode from a file in the assets directory""" + # TODO: Check if this locaation is correct with open(Path(__file__).parent / "assets" / name, "rb") as fh: code = fh.read() return code