diff --git a/src/dexorder/blockstate/state.py b/src/dexorder/blockstate/state.py index cc7a0c3..7e6e6e2 100644 --- a/src/dexorder/blockstate/state.py +++ b/src/dexorder/blockstate/state.py @@ -175,7 +175,7 @@ class BlockState: for d in block_diffs: if d.key == BlockState._DELETE_SERIES_KEY and dead.hash in new_root_fork: series_deletions.append(d.series) - elif d.value is UNLOAD and dead.hash in new_root_fork: + elif d.value is UNLOAD and dead in new_root_fork: key_unloads.append((d.series, d.key)) else: updated_keys.add((d.series, d.key)) diff --git a/src/dexorder/event_handler.py b/src/dexorder/event_handler.py index 7bed007..5f9c25e 100644 --- a/src/dexorder/event_handler.py +++ b/src/dexorder/event_handler.py @@ -5,7 +5,7 @@ from web3.types import EventData 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.base.order import TrancheExecutionRequest, TrancheKey, ExecutionRequest, new_tranche_execution_request, OrderKey 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.contract.dexorder import get_factory_contract, vault_address, VaultContract, get_dexorder_contract @@ -16,7 +16,7 @@ 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 + unconstrained_price_triggers, execution_requests, inflight_execution_requests, TrancheStatus log = logging.getLogger(__name__) @@ -97,15 +97,40 @@ async def handle_order_placed(event: EventData): log.debug(f'created order {order_status}') if triggers.closed: log.warning(f'order {order.key} was immediately closed') - close_order_and_disable_triggers(order.key, SwapOrderState.Filled if not order.remaining else SwapOrderState.Expired) + close_order_and_disable_triggers(order, SwapOrderState.Filled if order.remaining <= 0 else SwapOrderState.Expired) def handle_swap_filled(event: EventData): # event DexorderSwapFilled (uint64 orderIndex, uint8 trancheIndex, uint256 amountIn, uint256 amountOut); log.debug(f'DexorderSwapFilled {event}') + args = event['args'] + vault = event['address'] + order_index = args['orderIndex'] + tranche_index = args['trancheIndex'] + amount_in = args['amountIn'] + amount_out = args['amountOut'] + try: + order: Order = Order.of(vault, order_index) + except KeyError: + log.warning(f'DexorderSwapFilled IGNORED due to missing order {vault} {order_index}') + return + triggers = OrderTriggers.instances[order.key] + triggers.fill(tranche_index, amount_in, amount_out) + # check for fill + if order.remaining <= 0: + close_order_and_disable_triggers(order.key, SwapOrderState.Filled) -def handle_order_completed(event: EventData): +async def handle_order_completed(event: EventData): # event DexorderCompleted (uint64 orderIndex); // todo remove? log.debug(f'DexorderCompleted {event}') + vault = event['address'] + order_index = event['args']['orderIndex'] + try: + order: Order = Order.of(vault, order_index) + except KeyError: + log.warning(f'DexorderCompleted IGNORED due to missing order {vault} {order_index}') + status = await VaultContract(vault).swapOrderStatus(order_index) + log.debug(f'SwapOrderStatus #todo {status}') + # todo def handle_order_error(event: EventData): # event DexorderError (uint64 orderIndex, string reason); @@ -211,12 +236,11 @@ def handle_dexorderexecutions(event: EventData): if job is None: log.warning(f'Job {exe_id} not found!') return - finish_execution_request(job.request, errors[0]) def finish_execution_request(req: TrancheExecutionRequest, error: str): - order = Order.of(req.vault, req.order_index) + order: Order = Order.of(req.vault, req.order_index) tk = TrancheKey(req.vault, req.order_index, req.tranche_index) if error != '': log.debug(f'execution request for tranche {tk} had error "{error}"') @@ -227,5 +251,15 @@ def finish_execution_request(req: TrancheExecutionRequest, error: str): # todo vault balance checks token = order.order.tokenIn log.debug(f'insufficient funds {req.vault} {token} ') + elif error == 'TF': + # Tranche Filled + log.warning(f'tranche already filled {tk}') + triggers = OrderTriggers.instances[order.key] + tranche_trigger = triggers.triggers[tk.tranche_index] + tranche_trigger.status = TrancheStatus.Filled + tranche_trigger.disable() else: log.error(f'Unhandled execution error for transaction request {req} ERROR: "{error}"') + er = execution_requests[tk] + if er.height < current_block.get().height: + del execution_requests[tk] diff --git a/src/dexorder/order/orderstate.py b/src/dexorder/order/orderstate.py index 9b2f197..6fd08f1 100644 --- a/src/dexorder/order/orderstate.py +++ b/src/dexorder/order/orderstate.py @@ -41,7 +41,7 @@ class Order: def of(vault: str, order_index: int):... @staticmethod - def of(a, b=None): + def of(a, b=None) -> 'Order': key = a if b is None else OrderKey(a, b) try: return Order.instances[key] @@ -108,6 +108,9 @@ class Order: def tranche_filled(self, tk: TrancheKey): return self.tranche_filled_in(tk) if self.amount_is_input else self.tranche_filled_out(tk) + def tranche_remaining(self, tk: TrancheKey): + return self.tranche_amounts[tk.tranche_index] - self.tranche_filled(tk) + @property def filled(self): return self.filled_in if self.amount_is_input else self.filled_out @@ -130,7 +133,6 @@ class Order: fin = old.filled_in + filled_in fout = old.filled_out + filled_out Order._tranche_filled[tk] = Filled(fin, fout) - # todo check for completion def complete(self, final_state: SwapOrderState): """ updates the static order record with its final values, then deletes all its dynamic blockstate and removes the Order from the actives list """ diff --git a/src/dexorder/order/triggers.py b/src/dexorder/order/triggers.py index 243ea24..cb0f011 100644 --- a/src/dexorder/order/triggers.py +++ b/src/dexorder/order/triggers.py @@ -1,5 +1,5 @@ import logging -from enum import Enum +from enum import Enum, auto from typing import Callable from dexorder.blockstate import BlockSet, BlockDict @@ -18,7 +18,7 @@ time_triggers:BlockSet[TimeTrigger] = BlockSet('tt') PriceTrigger = Callable[[int], None] # func(pool_price) price_triggers:dict[str, BlockSet[PriceTrigger]] = defaultdictk(lambda addr:BlockSet(f'pt|{addr}')) # different BlockSet per pool address unconstrained_price_triggers: BlockSet[PriceTrigger] = BlockSet('upt') # tranches with no price constraints, whose time constraint is fulfilled -execution_requests:BlockDict[TrancheKey, ExecutionRequest] = BlockDict('e') # value is block height when the request was placed +execution_requests:BlockDict[TrancheKey, ExecutionRequest] = BlockDict('e') # todo should this really be blockdata? inflight_execution_requests:BlockDict[TrancheKey, int] = BlockDict('ei') # value is block height when the request was sent @@ -30,10 +30,10 @@ def intersect_ranges( a_low, a_high, b_low, b_high): return low, high class TrancheStatus (Enum): - Early = 0 # first time trigger hasnt happened yet - Pricing = 1 # we are inside the time window and checking prices - Filled = 1 # tranche has no more available amount - Expired = 2 # time deadline has past and this tranche cannot be filled + Early = auto() # first time trigger hasnt happened yet + Pricing = auto() # we are inside the time window and checking prices + Filled = auto() # tranche has no more available amount + Expired = auto() # time deadline has past and this tranche cannot be filled class TrancheTrigger: def __init__(self, order: Order, tranche_key: TrancheKey): @@ -52,7 +52,7 @@ class TrancheTrigger: self.status = TrancheStatus.Filled return - self.time_constraint = time_constraint = None # stored as a tuple of two ints for earliest and latest absolute timestamps + time_constraint = None # stored as a tuple of two ints for earliest and latest absolute timestamps self.price_constraints = [] for c in tranche.constraints: if c.mode == ConstraintMode.Time: @@ -65,6 +65,7 @@ class TrancheTrigger: raise NotImplementedError else: raise NotImplementedError + self.time_constraint = time_constraint if time_constraint is None: self.status = TrancheStatus.Pricing else: @@ -85,11 +86,18 @@ class TrancheTrigger: time_triggers.remove(self.time_trigger) def time_trigger(self, now): - log.debug(f'time_trigger {now}') + log.debug(f'time_trigger {now} {self.status} {self.time_constraint}') + if self.closed: + log.debug(f'price trigger ignored because trigger status is {self.status}') + return if now >= self.time_constraint[1]: + log.debug(f'tranche expired {self.tk}') self.disable() self.status = TrancheStatus.Expired - if self.status == TrancheStatus.Early and now >= self.time_constraint[0]: + # check for all tranches expired + OrderTriggers.instances[self.order.key].check_complete() + elif self.status == TrancheStatus.Early and now >= self.time_constraint[0]: + log.debug(f'tranche time enabled {self.tk}') self.status = TrancheStatus.Pricing self.enable_price_trigger() @@ -100,9 +108,15 @@ class TrancheTrigger: unconstrained_price_triggers.add(self.price_trigger) def disable_price_trigger(self): - price_triggers[self.order.pool_address].remove(self.price_trigger) + if self.price_constraints: + price_triggers[self.order.pool_address].remove(self.price_trigger) + else: + unconstrained_price_triggers.remove(self.price_trigger) def price_trigger(self, cur): + 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() @@ -113,6 +127,15 @@ class TrancheTrigger: log.info(f'execution request for {self.tk}') execution_requests[self.tk] = ExecutionRequest(height, proof) + def fill(self, _amount_in, _amount_out ): + remaining = self.order.tranche_remaining(self.tk) + filled = remaining <= 0 + if filled: + log.debug(f'tranche filled {self.tk}') + self.status = TrancheStatus.Filled + self.disable() + return filled + def disable(self): self.disable_time_trigger() self.disable_price_trigger() @@ -149,11 +172,21 @@ class OrderTriggers: def open(self): return not self.closed + def check_complete(self): + if all(t.closed for t in self.triggers): + final_state = SwapOrderState.Filled if self.order.remaining <= 0 else SwapOrderState.Expired + close_order_and_disable_triggers(self.order, final_state) -def close_order_and_disable_triggers(order_key: OrderKey, final_state: SwapOrderState): - Order.instances[order_key].complete(final_state) + def fill(self, tranche_index, amount_in, amount_out): + self.order.add_fill(tranche_index, amount_in, amount_out) + if self.triggers[tranche_index].fill(amount_in, amount_out): + self.check_complete() + + +def close_order_and_disable_triggers(order: Order, final_state: SwapOrderState): + order.complete(final_state) try: - triggers = OrderTriggers.instances[order_key] + triggers = OrderTriggers.instances[order.key] except KeyError: pass else: