From 73bb0fd6351d3866a0e038d46a5eadb7ce6a3d32 Mon Sep 17 00:00:00 2001 From: Tim Olson <> Date: Sun, 5 Nov 2023 16:49:42 -0400 Subject: [PATCH] price constraints working --- src/dexorder/base/chain.py | 2 +- src/dexorder/base/token.py | 1 - src/dexorder/bin/main.py | 3 +- src/dexorder/blockchain/uniswap.py | 40 ---------- src/dexorder/contract/__init__.py | 7 +- src/dexorder/contract/contract_proxy.py | 4 +- src/dexorder/contract/decimals.py | 16 ++++ src/dexorder/contract/pool_contract.py | 10 --- src/dexorder/contract/uniswap_contracts.py | 23 ------ src/dexorder/data/__init__.py | 15 +++- src/dexorder/database/model/series.py | 3 - src/dexorder/database/model/transaction.py | 2 - src/dexorder/event_handler.py | 61 ++++++++++----- src/dexorder/order/orderlib.py | 77 ++++++++----------- src/dexorder/order/triggers.py | 49 ++++++------ src/dexorder/transaction.py | 4 +- src/dexorder/uniswap.py | 87 ++++++++++++++++++++++ 17 files changed, 224 insertions(+), 180 deletions(-) delete mode 100644 src/dexorder/blockchain/uniswap.py create mode 100644 src/dexorder/contract/decimals.py delete mode 100644 src/dexorder/contract/pool_contract.py delete mode 100644 src/dexorder/contract/uniswap_contracts.py create mode 100644 src/dexorder/uniswap.py diff --git a/src/dexorder/base/chain.py b/src/dexorder/base/chain.py index 86a4118..c9fb8df 100644 --- a/src/dexorder/base/chain.py +++ b/src/dexorder/base/chain.py @@ -44,6 +44,6 @@ Polygon = Blockchain(137, 'Polygon') # POS not zkEVM Mumbai = Blockchain(80001, 'Mumbai') BSC = Blockchain(56, 'BSC') Arbitrum = Blockchain(42161, 'Arbitrum', 3, batch_size=1000) # todo configure batch size... does it depend on log count? :( -Mock = Blockchain(31337, 'Mock', 3) +Mock = Blockchain(31337, 'Mock', 3, batch_size=10000) current_chain = ContextVar[Blockchain]('current_chain') diff --git a/src/dexorder/base/token.py b/src/dexorder/base/token.py index 55b06e4..4b71340 100644 --- a/src/dexorder/base/token.py +++ b/src/dexorder/base/token.py @@ -119,4 +119,3 @@ for _chain_id, _native in _native_tokens.items(): _tokens_by_chain[_chain_id][_native.symbol] = _native tokens = ByBlockchainDict[Token](_tokens_by_chain) - diff --git a/src/dexorder/bin/main.py b/src/dexorder/bin/main.py index 65cdc51..c27f7fe 100644 --- a/src/dexorder/bin/main.py +++ b/src/dexorder/bin/main.py @@ -2,8 +2,7 @@ import logging import sys from asyncio import CancelledError -from dexorder import db, config, Blockchain, blockchain -from dexorder.base.chain import current_chain +from dexorder import db, blockchain from dexorder.bin.executable import execute from dexorder.blockstate.blockdata import BlockData from dexorder.blockstate.db_state import DbState diff --git a/src/dexorder/blockchain/uniswap.py b/src/dexorder/blockchain/uniswap.py deleted file mode 100644 index 83c24a5..0000000 --- a/src/dexorder/blockchain/uniswap.py +++ /dev/null @@ -1,40 +0,0 @@ -from charset_normalizer.md import getLogger -from eth_abi.packed import encode_packed -from eth_utils import keccak, to_bytes, to_checksum_address - -from dexorder import dec -from dexorder.contract import abi_encoder -from dexorder.util import hexbytes - -UNISWAPV3_POOL_INIT_CODE_HASH = hexbytes('0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54') - -log = getLogger(__name__) - -class Fee: - LOWEST = 100 - LOW = 500 - MEDIUM = 3000 - HIGH = 10000 - - -def ordered_addresses(addr_a:str, addr_b:str): - return (addr_a, addr_b) if addr_a.lower() <= addr_b.lower() else (addr_b, addr_a) - - -def uniswap_pool_address(factory_addr: str, addr_a: str, addr_b: str, fee: int) -> str: - token0, token1 = ordered_addresses(addr_a, addr_b) - salt = keccak(abi_encoder.encode(['address','address','uint24'],[token0, token1, fee])) - contract_address = keccak( - b"\xff" - + to_bytes(hexstr=factory_addr) - + salt - + UNISWAPV3_POOL_INIT_CODE_HASH - ).hex()[-40:] - result = to_checksum_address(contract_address) - # log.debug(f'uniswap pool address {factory_addr} {addr_a} {addr_b} {fee} => {result}') - return result - -def uniswap_price(sqrt_price): - d = dec(sqrt_price) - price = d * d / dec(2 ** (96 * 2)) - return price diff --git a/src/dexorder/contract/__init__.py b/src/dexorder/contract/__init__.py index c59f9a6..62c0e23 100644 --- a/src/dexorder/contract/__init__.py +++ b/src/dexorder/contract/__init__.py @@ -4,15 +4,12 @@ from eth_abi.codec import ABIDecoder, ABIEncoder from eth_abi.registry import registry as default_registry from .. import current_w3 as _current_w3 +from .abi import abis +from .contract_proxy import ContractProxy abi_decoder = ABIDecoder(default_registry) abi_encoder = ABIEncoder(default_registry) -from .abi import abis -from .contract_proxy import ContractProxy -from .pool_contract import UniswapV3Pool -from .uniswap_contracts import uniswapV3 - def get_contract_data(name): with open(f'../contract/out/{name}.sol/{name}.json', 'rt') as file: diff --git a/src/dexorder/contract/contract_proxy.py b/src/dexorder/contract/contract_proxy.py index 3d2f099..faf7845 100644 --- a/src/dexorder/contract/contract_proxy.py +++ b/src/dexorder/contract/contract_proxy.py @@ -2,13 +2,11 @@ import json from typing import Optional import eth_account -from eth_utils import keccak from web3.types import TxReceipt -from dexorder import current_w3, Account +from dexorder import current_w3 from dexorder.base.account import current_account from dexorder.database.model.block import current_block -from dexorder.base import TransactionDict from dexorder.util import hexstr diff --git a/src/dexorder/contract/decimals.py b/src/dexorder/contract/decimals.py new file mode 100644 index 0000000..cc16240 --- /dev/null +++ b/src/dexorder/contract/decimals.py @@ -0,0 +1,16 @@ +import logging + +from dexorder import db +from dexorder.contract import ContractProxy + +log = logging.getLogger(__name__) + + +async def token_decimals(addr): + key = f'td|{addr}' + try: + return db.kv[key] + except KeyError: + decimals = await ContractProxy(addr, 'ERC20').decimals() + db.kv[key] = decimals + return decimals diff --git a/src/dexorder/contract/pool_contract.py b/src/dexorder/contract/pool_contract.py deleted file mode 100644 index eff7f0a..0000000 --- a/src/dexorder/contract/pool_contract.py +++ /dev/null @@ -1,10 +0,0 @@ -from .contract_proxy import ContractProxy -from ..blockchain.uniswap import uniswap_price - - -class UniswapV3Pool (ContractProxy): - def __init__(self, address: str = None): - super().__init__(address, 'IUniswapV3Pool') - - async def price(self): - return uniswap_price((await self.slot0())[0]) diff --git a/src/dexorder/contract/uniswap_contracts.py b/src/dexorder/contract/uniswap_contracts.py deleted file mode 100644 index 3d9a163..0000000 --- a/src/dexorder/contract/uniswap_contracts.py +++ /dev/null @@ -1,23 +0,0 @@ -from dexorder.base.chain import Ethereum, Goerli, Polygon, Mumbai, Arbitrum, Mock -from dexorder.blockchain.uniswap import uniswap_pool_address -from dexorder.contract.contract_proxy import ContractProxy -from dexorder.blockchain import ByBlockchainDict - - -class _UniswapContracts (ByBlockchainDict[ContractProxy]): - - def __init__(self): - std = { - 'factory': ContractProxy('0x1F98431c8aD98523631AE4a59f267346ea31F984', 'IUniswapV3Factory'), - 'nfpm': ContractProxy('0xC36442b4a4522E871399CD717aBDD847Ab11FE88', 'INonfungiblePositionManager'), - 'quoter': ContractProxy('0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6', 'IQuoter'), - 'swap_router': ContractProxy('0xE592427A0AEce92De3Edee1F18E0157C05861564', 'ISwapRouter'), - } - super().__init__({chain.chain_id:std for chain in (Ethereum, Polygon, Goerli, Mumbai, Arbitrum, Mock)}) - - -uniswapV3 = _UniswapContracts() - - -def uniswapV3_pool_address( addr_a: str, addr_b: str, fee: int): - return uniswap_pool_address(uniswapV3['factory'].address, addr_a, addr_b, fee) diff --git a/src/dexorder/data/__init__.py b/src/dexorder/data/__init__.py index 3867bcd..b541339 100644 --- a/src/dexorder/data/__init__.py +++ b/src/dexorder/data/__init__.py @@ -1,6 +1,7 @@ from dexorder import dec from dexorder.base.chain import current_chain from dexorder.blockstate import BlockDict +from dexorder.blockstate.blockdata import K, V from dexorder.util import json, defaultdictk # pub=... publishes to a channel for web clients to consume. argument is (key,value) and return must be (event,room,args) @@ -14,5 +15,15 @@ vault_balances: BlockDict[str, dict[str, int]] = BlockDict( str2value=lambda s: {k: int(v) for k, v in json.loads(s).items()}, pub=lambda k, v: (f'{current_chain.get().chain_id}|{vault_owners[k]}', 'vb', (k,json.dumps({k2: str(v2) for k2, v2 in v.items()}))) ) -pool_prices: BlockDict[str, dec] = BlockDict('p', db=True, redis=True, value2str=lambda d: f'{d:f}', str2value=dec, - pub=lambda k, v: (f'{current_chain.get().chain_id}|{k}', 'p', (k, str(v)))) + + +class PoolPrices (BlockDict[str, dec]): + def __setitem__(self, item: K, value: V) -> None: + super().__setitem__(item, value) + new_pool_prices[item] = value + + +new_pool_prices: dict[str, dec] = {} # tracks which prices were set during the current block. cleared every block. +pool_prices: PoolPrices = PoolPrices('p', db=True, redis=True, + value2str=lambda d: f'{d:f}', str2value=dec, + pub=lambda k, v: (f'{current_chain.get().chain_id}|{k}', 'p', (k, str(v)))) diff --git a/src/dexorder/database/model/series.py b/src/dexorder/database/model/series.py index 5530e36..a867ab5 100644 --- a/src/dexorder/database/model/series.py +++ b/src/dexorder/database/model/series.py @@ -1,10 +1,7 @@ import logging -from typing import Union -from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import mapped_column, Mapped -from dexorder.database.column import Json from dexorder.database.model import Base log = logging.getLogger(__name__) diff --git a/src/dexorder/database/model/transaction.py b/src/dexorder/database/model/transaction.py index c4f02c6..3df9566 100644 --- a/src/dexorder/database/model/transaction.py +++ b/src/dexorder/database/model/transaction.py @@ -4,11 +4,9 @@ from typing import Optional import sqlalchemy as sa from sqlalchemy import ForeignKey -from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import mapped_column, Mapped, relationship from dexorder.base.order import TransactionRequest as TransactionRequestDict, deserialize_transaction_request -from dexorder.base import TransactionDict from dexorder.database.column import Dict, Bytes, UUID_PK, Blockchain, UUID from dexorder.database.column_types import DataclassDict from dexorder.database.model import Base diff --git a/src/dexorder/event_handler.py b/src/dexorder/event_handler.py index b81de8e..2859b7d 100644 --- a/src/dexorder/event_handler.py +++ b/src/dexorder/event_handler.py @@ -3,20 +3,21 @@ from uuid import UUID from web3.types import EventData -from dexorder import current_pub, db +from dexorder import current_pub, db, dec from dexorder.base.chain import current_chain from dexorder.base.order import TrancheExecutionRequest, TrancheKey, ExecutionRequest, new_tranche_execution_request -from dexorder.transaction import handle_create_transactions, submit_transaction_request, handle_transaction_receipts, handle_send_transactions -from dexorder.blockchain.uniswap import uniswap_price +from dexorder.transaction import create_transactions, submit_transaction_request, handle_transaction_receipts, send_transactions +from dexorder.uniswap import UniswapV3Pool, uniswap_price from dexorder.contract.dexorder import get_factory_contract, vault_address, VaultContract, get_dexorder_contract -from dexorder.contract import UniswapV3Pool, get_contract_event -from dexorder.data import pool_prices, vault_owners, vault_balances +from dexorder.contract import get_contract_event +from dexorder.data import pool_prices, vault_owners, vault_balances, new_pool_prices from dexorder.database.model.block import current_block from dexorder.database.model.transaction import TransactionJob from dexorder.order.orderlib import SwapOrderState, SwapOrderStatus from dexorder.order.orderstate import Order from dexorder.order.triggers import OrderTriggers, close_order_and_disable_triggers, price_triggers, time_triggers, \ - unconstrained_price_triggers, execution_requests, inflight_execution_requests, TrancheStatus + unconstrained_price_triggers, execution_requests, inflight_execution_requests, TrancheStatus, active_tranches, new_price_triggers +from dexorder.util.async_util import maywait log = logging.getLogger(__name__) @@ -60,6 +61,7 @@ def setup_logevent_triggers(runner): # THIS IS BASICALLY THE MAIN RUN LOOP FOR EVERY BLOCK # + runner.add_event_trigger(init) runner.add_event_trigger(handle_vault_created, vault_created) runner.add_event_trigger(handle_order_placed, get_contract_event('OrderLib', 'DexorderSwapPlaced')) runner.add_event_trigger(handle_transfer, get_contract_event('ERC20', 'Transfer')) @@ -71,9 +73,15 @@ def setup_logevent_triggers(runner): runner.add_event_trigger(handle_dexorderexecutions, executions) runner.add_event_trigger(activate_time_triggers) runner.add_event_trigger(activate_price_triggers) + runner.add_event_trigger(process_active_tranches) runner.add_event_trigger(process_execution_requests) - runner.add_event_trigger(handle_create_transactions) - runner.add_event_trigger(handle_send_transactions) + runner.add_event_trigger(create_transactions) + runner.add_event_trigger(send_transactions) + + +def init(): + new_pool_prices.clear() + new_price_triggers.clear() async def handle_order_placed(event: EventData): @@ -169,17 +177,16 @@ def handle_transfer(transfer: EventData): log.debug(f'vaults: {list(vaults)}') -new_pool_prices: dict[str, int] = {} -def handle_uniswap_swap(swap: EventData): +async def handle_uniswap_swap(swap: EventData): try: sqrt_price = swap['args']['sqrtPriceX96'] except KeyError: return addr = swap['address'] - price = uniswap_price(sqrt_price) + price: dec = await uniswap_price(addr, sqrt_price) log.debug(f'pool {addr} {price}') - new_pool_prices[addr] = price + pool_prices[addr] = price def handle_vault_created(created: EventData): @@ -204,22 +211,38 @@ def handle_vault_created(created: EventData): current_pub.get()(f'{current_chain.get().chain_id}|{owner}', 'vaults', vaults) -def activate_time_triggers(): +async def activate_time_triggers(): now = current_block.get().timestamp log.debug(f'activating time triggers') # time triggers for tt in time_triggers: - tt(now) + await maywait(tt(now)) -def activate_price_triggers(): - log.debug('activating price triggers') +async def activate_price_triggers(): + log.debug(f'activating price triggers') + pools_triggered = set() for pool, price in new_pool_prices.items(): + pools_triggered.add(pool) for pt in price_triggers[pool]: - pt(price) - new_pool_prices.clear() + await maywait(pt(price)) + for pool, triggers in new_price_triggers.items(): + if pool not in pools_triggered: + price = pool_prices[pool] + for pt in triggers: + await maywait(pt(price)) for t in unconstrained_price_triggers: # noinspection PyTypeChecker - t(None) + await maywait(t(None)) + + +def process_active_tranches(): + for tk, proof in active_tranches.items(): + old_req = execution_requests.get(tk) + height = current_block.get().height + if old_req is None or old_req.height <= height: + log.info(f'execution request for {tk}') + execution_requests[tk] = ExecutionRequest(height, proof) + async def process_execution_requests(): height = current_block.get().height diff --git a/src/dexorder/order/orderlib.py b/src/dexorder/order/orderlib.py index 918afe7..2d2ecc7 100644 --- a/src/dexorder/order/orderlib.py +++ b/src/dexorder/order/orderlib.py @@ -5,8 +5,7 @@ from enum import Enum from typing import Optional from dexorder import dec -from dexorder.blockchain.uniswap import uniswap_price -from dexorder.contract.uniswap_contracts import uniswapV3_pool_address +from dexorder.uniswap import uniswapV3_pool_address, uniswap_price from dexorder.contract import abi_decoder, abi_encoder log = logging.getLogger(__name__) @@ -106,10 +105,8 @@ NO_OCO = 18446744073709551615 # max uint64 class ConstraintMode (Enum): Time = 0 - Limit = 1 - Trailing = 2 - Barrier = 3 - Line = 4 + Line = 1 + Barrier = 2 @dataclass class Constraint (ABC): @@ -120,51 +117,16 @@ class Constraint (ABC): mode = ConstraintMode(obj[0]) if mode == ConstraintMode.Time: return TimeConstraint.load(obj[1]) + elif mode == ConstraintMode.Line: + return LineConstraint.load(obj[1]) else: - raise NotImplementedError + raise ValueError(f'Unknown constraint mode {mode}') @abstractmethod def dump(self): ... def _dump(self, types, values): - return self.mode, abi_encoder.encode(types, values) - - -class PriceConstraint (Constraint, ABC): - @abstractmethod - def passes(self, price: dec) -> bool:... - - -@dataclass -class LimitConstraint (PriceConstraint): - isAbove: bool - isRatio: bool - valueSqrtX96: int - - TYPES = 'bool','bool','uint160' - - def __init__(self, *args): - self.isAbove, self.isRatio, self.valueSqrtX96 = args - self.limit = uniswap_price(self.valueSqrtX96) - - def load(self, obj): - isAbove, isRatio, valueSqrtX96 = abi_decoder.decode(LimitConstraint.TYPES, obj) - return LimitConstraint(ConstraintMode.Limit, isAbove, isRatio, valueSqrtX96) - - def dump(self): - return self._dump(LimitConstraint.TYPES, (self.isAbove, self.isRatio, self.valueSqrtX96)) - - def passes(self, price: dec) -> bool: - return self.isAbove and price >= self.limit or not self.isAbove and price <= self.limit - - -@dataclass -class LineConstraint (Constraint): - isAbove: bool - isRatio: bool - time: int - valueSqrtX96: int - slopeSqrtX96: int + return self.mode.value, abi_encoder.encode(types, values) class TimeMode (Enum): @@ -201,6 +163,31 @@ class TimeConstraint (Constraint): return self._dump(TimeConstraint.TYPES, (self.earliest.mode.value, self.earliest.time, self.latest.mode.value, self.latest.time)) +@dataclass +class LineConstraint (Constraint): + isAbove: bool + isRatio: bool + time: int + valueSqrtX96: int + slopeSqrtX96: int + + TYPES = 'bool','bool','uint32','uint160','int160' + + @staticmethod + def load(obj): + return LineConstraint(ConstraintMode.Line, *abi_decoder.decode(LineConstraint.TYPES, obj)) + + def dump(self): + return self._dump(LineConstraint.TYPES, (self.isAbove, self.isRatio, self.time, self.valueSqrtX96, self.slopeSqrtX96)) + + async def passes(self, pool_addr: str, price: dec) -> bool: + limit = await uniswap_price(pool_addr, self.valueSqrtX96) + # todo slopes + # todo ratios + # prices AT the limit get zero volume, so we only trigger on >, not >= + return self.isAbove and price > limit or not self.isAbove and price < limit + + @dataclass class Tranche: fraction: int # 18-decimal fraction of the order amount which is available to this tranche. must be <= 1 diff --git a/src/dexorder/order/triggers.py b/src/dexorder/order/triggers.py index cb0f011..00ce252 100644 --- a/src/dexorder/order/triggers.py +++ b/src/dexorder/order/triggers.py @@ -1,24 +1,30 @@ +import asyncio import logging +from collections import defaultdict from enum import Enum, auto -from typing import Callable +from typing import Callable, Optional, Union, Coroutine, Awaitable from dexorder.blockstate import BlockSet, BlockDict -from .orderlib import TimeConstraint, LimitConstraint, ConstraintMode, SwapOrderState, PriceProof +from .orderlib import TimeConstraint, LineConstraint, ConstraintMode, SwapOrderState, PriceProof from dexorder.util import defaultdictk from .orderstate import Order +from .. import dec from ..base.order import OrderKey, TrancheKey, ExecutionRequest +from ..data import pool_prices from ..database.model.block import current_block log = logging.getLogger(__name__) -# todo time and price triggers should be BlockSortedSets that support range queries, for efficient lookup of triggers +# todo time and price triggers should be BlockSortedSets that support range queries for efficient lookup of triggers TimeTrigger = Callable[[int], None] # func(timestamp) time_triggers:BlockSet[TimeTrigger] = BlockSet('tt') -PriceTrigger = Callable[[int], None] # func(pool_price) +PriceTrigger = Callable[[dec], Union[Awaitable[None],None]] # [async] func(pool_price) price_triggers:dict[str, BlockSet[PriceTrigger]] = defaultdictk(lambda addr:BlockSet(f'pt|{addr}')) # different BlockSet per pool address +new_price_triggers:dict[str, set[PriceTrigger]] = defaultdict(set) # when price triggers are first set, they must be tested against the current price even if it didnt change this block unconstrained_price_triggers: BlockSet[PriceTrigger] = BlockSet('upt') # tranches with no price constraints, whose time constraint is fulfilled -execution_requests:BlockDict[TrancheKey, ExecutionRequest] = BlockDict('e') +active_tranches: BlockDict[TrancheKey, Optional[PriceProof]] = BlockDict('at') # tranches which have passed all constraints and should be executed +execution_requests:BlockDict[TrancheKey, ExecutionRequest] = BlockDict('e') # generated by the active tranches # todo should this really be blockdata? inflight_execution_requests:BlockDict[TrancheKey, int] = BlockDict('ei') # value is block height when the request was sent @@ -53,16 +59,16 @@ class TrancheTrigger: return time_constraint = None # stored as a tuple of two ints for earliest and latest absolute timestamps - self.price_constraints = [] + self.line_constraints: list[LineConstraint] = [] for c in tranche.constraints: if c.mode == ConstraintMode.Time: c: TimeConstraint earliest = c.earliest.timestamp(start) latest = c.latest.timestamp(start) time_constraint = (earliest, latest) if time_constraint is None else intersect_ranges(*time_constraint, earliest, latest) - elif c.mode == ConstraintMode.Limit: - c: LimitConstraint - raise NotImplementedError + elif c.mode == ConstraintMode.Line: + c: LineConstraint + self.line_constraints.append(c) else: raise NotImplementedError self.time_constraint = time_constraint @@ -92,8 +98,8 @@ class TrancheTrigger: return if now >= self.time_constraint[1]: log.debug(f'tranche expired {self.tk}') - self.disable() self.status = TrancheStatus.Expired + self.disable() # check for all tranches expired OrderTriggers.instances[self.order.key].check_complete() elif self.status == TrancheStatus.Early and now >= self.time_constraint[0]: @@ -102,30 +108,25 @@ class TrancheTrigger: self.enable_price_trigger() def enable_price_trigger(self): - if self.price_constraints: + if self.line_constraints: price_triggers[self.order.pool_address].add(self.price_trigger) + new_price_triggers[self.order.pool_address].add(self.price_trigger) else: unconstrained_price_triggers.add(self.price_trigger) def disable_price_trigger(self): - if self.price_constraints: + if self.line_constraints: price_triggers[self.order.pool_address].remove(self.price_trigger) else: unconstrained_price_triggers.remove(self.price_trigger) - def price_trigger(self, cur): + async def price_trigger(self, cur: dec): + # must be idempotent. could be called twice when first activated: once for the initial price lookup then once again if that price was changed in the current block if self.closed: log.debug(f'price trigger ignored because trigger status is {self.status}') return - if not self.price_constraints or all(pc.passes(cur) for pc in self.price_constraints): - self.execute() - - def execute(self, proof: PriceProof = None): - old_req = execution_requests.get(self.tk) - height = current_block.get().height - if old_req is None or old_req.height <= height: - log.info(f'execution request for {self.tk}') - execution_requests[self.tk] = ExecutionRequest(height, proof) + if not self.line_constraints or all(await asyncio.gather(*[pc.passes(self.order.pool_address, cur) for pc in self.line_constraints])): + active_tranches[self.tk] = None # or PriceProof(...) def fill(self, _amount_in, _amount_out ): remaining = self.order.tranche_remaining(self.tk) @@ -137,6 +138,10 @@ class TrancheTrigger: return filled def disable(self): + try: + del active_tranches[self.tk] + except KeyError: + pass self.disable_time_trigger() self.disable_price_trigger() diff --git a/src/dexorder/transaction.py b/src/dexorder/transaction.py index 3ee977d..e3d4967 100644 --- a/src/dexorder/transaction.py +++ b/src/dexorder/transaction.py @@ -50,7 +50,7 @@ def submit_transaction_request(tr: TransactionRequest): return job -async def handle_create_transactions(): +async def create_transactions(): for job in db.session.query(TransactionJob).filter( TransactionJob.chain == current_chain.get(), TransactionJob.state == TransactionJobState.Requested @@ -75,7 +75,7 @@ async def create_transaction(job: TransactionJob): log.info(f'servicing transaction request {job.request.__class__.__name__} {job.id} with tx {ctx.id}') -async def handle_send_transactions(): +async def send_transactions(): w3 = current_w3.get() for job in db.session.query(TransactionJob).filter( TransactionJob.chain == current_chain.get(), diff --git a/src/dexorder/uniswap.py b/src/dexorder/uniswap.py new file mode 100644 index 0000000..b323223 --- /dev/null +++ b/src/dexorder/uniswap.py @@ -0,0 +1,87 @@ +from charset_normalizer.md import getLogger +from eth_utils import keccak, to_bytes, to_checksum_address + +from dexorder import dec, db +from dexorder.base.chain import Ethereum, Polygon, Goerli, Mumbai, Arbitrum, Mock +from dexorder.blockchain import ByBlockchainDict +from dexorder.contract import abi_encoder, ContractProxy +from dexorder.contract.decimals import token_decimals +from dexorder.util import hexbytes + +UNISWAPV3_POOL_INIT_CODE_HASH = hexbytes('0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54') + +log = getLogger(__name__) + +class Fee: + LOWEST = 100 + LOW = 500 + MEDIUM = 3000 + HIGH = 10000 + + +class UniswapV3Pool (ContractProxy): + def __init__(self, address: str = None): + super().__init__(address, 'IUniswapV3Pool') + + async def price(self): + if not self.address: + raise ValueError + return await uniswap_price(self.address, (await self.slot0())[0]) + + +def ordered_addresses(addr_a:str, addr_b:str): + return (addr_a, addr_b) if addr_a.lower() <= addr_b.lower() else (addr_b, addr_a) + + +def uniswap_pool_address(factory_addr: str, addr_a: str, addr_b: str, fee: int) -> str: + token0, token1 = ordered_addresses(addr_a, addr_b) + salt = keccak(abi_encoder.encode(['address','address','uint24'],[token0, token1, fee])) + contract_address = keccak( + b"\xff" + + to_bytes(hexstr=factory_addr) + + salt + + UNISWAPV3_POOL_INIT_CODE_HASH + ).hex()[-40:] + result = to_checksum_address(contract_address) + # log.debug(f'uniswap pool address {factory_addr} {addr_a} {addr_b} {fee} => {result}') + return result + + +async def uniswap_price(addr, sqrt_price) -> dec: + price = dec(sqrt_price*sqrt_price) / 2 ** (96 * 2) + decimals = await pool_decimals(addr) + return price * dec(10) ** dec(decimals) + + +async def pool_decimals(addr): + key = f'pd|{addr}' + try: + return db.kv[key] + except KeyError: + pool = UniswapV3Pool(addr) + token0 = await pool.token0() + token1 = await pool.token1() + decimals0 = await token_decimals(token0) + decimals1 = await token_decimals(token1) + decimals = decimals0 - decimals1 + db.kv[key] = decimals + return decimals + + +class _UniswapContracts (ByBlockchainDict[ContractProxy]): + + def __init__(self): + std = { + 'factory': ContractProxy('0x1F98431c8aD98523631AE4a59f267346ea31F984', 'IUniswapV3Factory'), + 'nfpm': ContractProxy('0xC36442b4a4522E871399CD717aBDD847Ab11FE88', 'INonfungiblePositionManager'), + 'quoter': ContractProxy('0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6', 'IQuoter'), + 'swap_router': ContractProxy('0xE592427A0AEce92De3Edee1F18E0157C05861564', 'ISwapRouter'), + } + super().__init__({chain.chain_id:std for chain in (Ethereum, Polygon, Goerli, Mumbai, Arbitrum, Mock)}) + + +uniswapV3 = _UniswapContracts() + + +def uniswapV3_pool_address( addr_a: str, addr_b: str, fee: int): + return uniswap_pool_address(uniswapV3['factory'].address, addr_a, addr_b, fee)