diff --git a/src/dexorder/base/chain.py b/src/dexorder/base/chain.py index e37405f..2dc4ec2 100644 --- a/src/dexorder/base/chain.py +++ b/src/dexorder/base/chain.py @@ -43,6 +43,6 @@ Goerli = Blockchain(5, 'Goerli') Polygon = Blockchain(137, 'Polygon') # POS not zkEVM Mumbai = Blockchain(80001, 'Mumbai') BSC = Blockchain(56, 'BSC') -Arbitrum = Blockchain(42161, 'Arbitrum', 10) +Arbitrum = Blockchain(42161, 'Arbitrum', 10, batch_size=1000) # todo configure batch size... does it depend on log count? :( current_chain = ContextVar[Blockchain]('current_chain') diff --git a/src/dexorder/event_handler.py b/src/dexorder/event_handler.py index 7a2156f..633ae3c 100644 --- a/src/dexorder/event_handler.py +++ b/src/dexorder/event_handler.py @@ -8,7 +8,7 @@ from dexorder.blockchain.util import vault_address, get_contract_event, get_fact from dexorder.contract import VaultContract from dexorder.data import pool_prices, vault_addresses, vault_tokens, underfunded_vaults from dexorder.database.model.block import current_block -from dexorder.orderlib.orders import SwapOrderStatus +from dexorder.orderlib.orderlib import SwapOrderStatus log = logging.getLogger(__name__) diff --git a/src/dexorder/order/__init__.py b/src/dexorder/order/__init__.py new file mode 100644 index 0000000..c42cfd2 --- /dev/null +++ b/src/dexorder/order/__init__.py @@ -0,0 +1,4 @@ + +def order_key(vault:str, ): + return f'{vault}' + diff --git a/src/dexorder/orderlib/orders.py b/src/dexorder/order/orderlib.py similarity index 81% rename from src/dexorder/orderlib/orders.py rename to src/dexorder/order/orderlib.py index db85e76..39b6b95 100644 --- a/src/dexorder/orderlib/orders.py +++ b/src/dexorder/order/orderlib.py @@ -62,6 +62,8 @@ class SwapOrderStatus: ocoGroup: Optional[int] filledIn: int filledOut: int + trancheFilledIn: list[int] + trancheFilledOut: list[int] @staticmethod def load(obj): @@ -71,10 +73,12 @@ class SwapOrderStatus: ocoGroup = None if obj[3] == NO_OCO else obj[3] filledIn = obj[4] filledOut = obj[5] - return SwapOrderStatus(order, state, start, ocoGroup, filledIn, filledOut) + trancheFilledIn = obj[6] + trancheFilledOut = obj[7] + return SwapOrderStatus(order, state, start, ocoGroup, filledIn, filledOut, trancheFilledIn, trancheFilledOut) def dump(self): - return self.order.dump(), self.state.value, self.start, self.ocoGroup, self.filledIn, self.filledOut + return self.order.dump(), self.state.value, self.start, self.ocoGroup, self.filledIn, self.filledOut, self.trancheFilledIn, self.trancheFilledOut NO_OCO = 18446744073709551615 # max uint64 @@ -88,6 +92,8 @@ class ConstraintMode (Enum): @dataclass class Constraint (ABC): + mode: ConstraintMode + @staticmethod def load(obj): mode = ConstraintMode(obj[0]) @@ -99,9 +105,8 @@ class Constraint (ABC): @abstractmethod def dump(self): ... - @staticmethod - def _dump(mode, types, values): - return mode, abi_encoder.encode(types, values) + def _dump(self, types, values): + return self.mode, abi_encoder.encode(types, values) @dataclass class PriceConstraint (Constraint): @@ -109,8 +114,14 @@ class PriceConstraint (Constraint): isRatio: bool valueSqrtX96: int + TYPES = 'bool','bool','uint160' + + def load(self, obj): + isAbove, isRatio, valueSqrtX96 = abi_decoder.decode(PriceConstraint.TYPES, obj) + return PriceConstraint(ConstraintMode.Limit, isAbove, isRatio, valueSqrtX96) + def dump(self): - return Constraint._dump(ConstraintMode.Limit, ('bool','bool','uint160'), (self.isAbove, self.isRatio, self.valueSqrtX96)) + return self._dump(PriceConstraint.TYPES, (self.isAbove, self.isRatio, self.valueSqrtX96)) @dataclass @@ -147,7 +158,7 @@ class TimeConstraint (Constraint): @staticmethod def load(obj: bytes): earliest_mode, earliest_time, latest_mode, latest_time = abi_decoder.decode(TimeConstraint.TYPES, obj) - return TimeConstraint(Time(TimeMode(earliest_mode),earliest_time), Time(TimeMode(latest_mode),latest_time)) + return TimeConstraint(ConstraintMode.Time, Time(TimeMode(earliest_mode),earliest_time), Time(TimeMode(latest_mode),latest_time)) def dump(self): return Constraint._dump(ConstraintMode.Time, TimeConstraint.TYPES, diff --git a/src/dexorder/order/orderstate.py b/src/dexorder/order/orderstate.py new file mode 100644 index 0000000..db01a5a --- /dev/null +++ b/src/dexorder/order/orderstate.py @@ -0,0 +1,30 @@ +import logging + +from dexorder.blockstate import BlockSet, BlockDict +from dexorder.order.orderlib import SwapOrderStatus +from dexorder.util import keystr + +log = logging.getLogger(__name__) + +def order_key( vault:str, order_index:int ): + return keystr(vault, str(order_index)) + +def tranche_key( vault:str, order_index:int, tranche_index:int ): + return keystr(vault,str(order_index),str(tranche_index)) + +active_orders = BlockSet('ao') # unfilled, not-canceled orders whose triggers have been loaded/set +order_remaining = BlockDict('or') # by order key +tranche_remaining = BlockDict('tr') # by tranche key + +# todo forcibly dispose of entire series + +class OrderState: + def __init__(self, vault:str, order_index:int, status: SwapOrderStatus): + self.vault = vault + self.order_index = order_index + self.key = f'{vault}|{order_index}' + self.tranche_keys = [f'{self.key}|{i}' for i in range(len(status.trancheFilledIn))] + +###### TODO TODO TODO vault state needs to be a dict pointing to owner addr + + diff --git a/src/dexorder/order/triggers.py b/src/dexorder/order/triggers.py new file mode 100644 index 0000000..25315db --- /dev/null +++ b/src/dexorder/order/triggers.py @@ -0,0 +1,55 @@ +import logging +from typing import Callable + +from dexorder.blockstate import BlockSet +from .orderlib import SwapOrderStatus, TimeConstraint, PriceConstraint, ConstraintMode +from dexorder.util import defaultdictk + +log = logging.getLogger(__name__) + +# todo time and price triggers should be BlockSortedSets that support range queries +TimeTrigger = Callable[[int, int], None] # func(start_timestamp, end_timestamp) +time_triggers:BlockSet[TimeTrigger] = BlockSet('tt') + +PriceTrigger = Callable[[int, int], None] # pool previous price, pool new price +price_triggers:dict[str, BlockSet[PriceTrigger]] = defaultdictk(BlockSet) # different BlockSet per pool address + + +def intersect_ranges( a_low, a_high, b_low, b_high): + low, high = max(a_low,b_low), min(a_high,b_high) + if high <= low: + low, high = None, None + return low, high + +class TrancheTrigger: + def __init__(self, vault: str, order_index:int, tranche_index: int): + self.series = f'{vault}|{order_index}|{tranche_index}|' + self.vault = vault + self.order_index = order_index + self.tranche_index = tranche_index + + # todo refactor so we have things like tranche amount filled as blockstate, order amount remaining + + def enable(self, status: SwapOrderStatus): + tranche = status.order.tranches[self.tranche_index] + tranche_amount = status.order.amount * tranche.fraction // 10**18 + tranche_filled = status.trancheFilledIn[self.tranche_index] if status.order.amountIsInput else status.trancheFilledOut[self.tranche_index] + order_filled = status.filledIn if status.order.amountIsInput else status.filledOut + remaining = min(tranche_amount - tranche_filled, status.order.amount - order_filled) + if remaining <= 0: # todo dust? + return + + time_constraint = None + price_constraints = [] + if status.filledOut: + ... + for c in tranche.constraints: + if c.mode == ConstraintMode.Time: + c: TimeConstraint + time_constraint = (c.earliest, c.latest) if time_constraint is None else intersect_ranges(*time_constraint, c.earliest, c.latest) + elif c.mode == ConstraintMode.Limit: + c: PriceConstraint + raise NotImplementedError + else: + raise NotImplementedError + diff --git a/src/dexorder/util/__init__.py b/src/dexorder/util/__init__.py index 15c02a5..d7250ba 100644 --- a/src/dexorder/util/__init__.py +++ b/src/dexorder/util/__init__.py @@ -1,4 +1,5 @@ import re +from typing import Callable, TypeVar, Generic from eth_utils import keccak from hexbytes import HexBytes @@ -23,6 +24,7 @@ def hexstr(value: bytes): elif type(value) is bytes: return '0x' + value.hex() elif type(value) is str: + # noinspection PyTypeChecker return value if value.startswith('0x') else '0x' + value else: raise ValueError @@ -32,14 +34,14 @@ def hexbytes(value: str): """ converts an optionally 0x-prefixed hex string into bytes """ return bytes.fromhex(value[2:] if value.startswith('0x') else value) -def keystr(value): - if type(value) is str: - return value - if type(value) is HexBytes: - return value.hex() - if type(value) is bytes: - return '0x' + value.hex() - return str(value) +def keystr(*keys): + return '|'.join( + value if type(value) is str else + value.hex() if type(value) is HexBytes else + '0x' + value.hex() if type(value) is bytes else + str(value) + for value in keys + ) def strkey(s): if s.startswith('0x'): @@ -51,3 +53,18 @@ def topic(event_abi): result = '0x' + keccak(text=event_name).hex() print(event_name, result) return result + + +K = TypeVar('K') +V = TypeVar('V') +class defaultdictk (Generic[K,V], dict[K,V]): + def __init__(self, default_factory: Callable[[K],V]): + super().__init__() + self.default_factory = default_factory + + def __getitem__(self, item: K) -> V: + try: + return super().__getitem__(item) + except KeyError: + default = self[item] = self.default_factory(item) + return default