From d9f0711ce83378be4440d089e06bd9cef3b8ec46 Mon Sep 17 00:00:00 2001 From: Tim Olson <> Date: Mon, 8 Jan 2024 22:43:25 -0400 Subject: [PATCH] transaction management bugfixes --- src/dexorder/base/orderlib.py | 27 +++++++++++++++++++++++++ src/dexorder/contract/__init__.py | 2 +- src/dexorder/contract/contract_proxy.py | 11 ++++++++++ src/dexorder/event_handler.py | 25 ++++++++++++++--------- src/dexorder/order/executionhandler.py | 2 +- src/dexorder/runner.py | 2 ++ src/dexorder/transaction.py | 4 +++- 7 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/dexorder/base/orderlib.py b/src/dexorder/base/orderlib.py index 51bdb40..67fe28d 100644 --- a/src/dexorder/base/orderlib.py +++ b/src/dexorder/base/orderlib.py @@ -57,6 +57,19 @@ class SwapOrder: return (self.tokenIn, self.tokenOut, self.route.dump(), str(self.amount), str(self.minFillAmount), self.amountIsInput, self.outputDirectlyToOwner, self.chainOrder, [t.dump() for t in self.tranches]) + def __str__(self): + msg = f''' +SwapOrder + in: {self.tokenIn} + out: {self.tokenOut} + exchange: {self.route.exchange, self.route.fee} + amount: {"input" if self.amountIsInput else "output"} {self.amount} {"to owner" if self.outputDirectlyToOwner else ""} + minFill: {self.minFillAmount} + tranches: +''' + for tranche in self.tranches: + msg += f' {tranche}\n' + return msg @dataclass class SwapStatus: @@ -207,6 +220,20 @@ class Tranche: self.startTime, self.endTime, minB, minM, maxB, maxM, ) + def __str__(self): + msg = f'{self.fraction/MAX_FRACTION:.1%} {self.startTime} to {self.endTime}' + if self.marketOrder: + msg += ' market order' + else: + if self.minIntercept or self.minSlope: + msg += f' >{self.minIntercept:.5g}' + if self.minSlope: + msg += f'{self.minSlope:+.5g}' + if self.maxIntercept or self.maxSlope: + msg += f' <{self.maxIntercept:.5g}' + if self.maxSlope: + msg += f'{self.maxSlope:+.5g}' + return msg @dataclass class PriceProof: diff --git a/src/dexorder/contract/__init__.py b/src/dexorder/contract/__init__.py index 09baff3..0da128c 100644 --- a/src/dexorder/contract/__init__.py +++ b/src/dexorder/contract/__init__.py @@ -7,7 +7,7 @@ from .contract_proxy import ContractProxy def get_contract_data(name): if name == "Vault" and os.path.exists(f'../contract/out/I{name}.sol/I{name}.json') : - logging.warning("getting abi from IVault.json instead of Vault.json") + # logging.debug("getting abi from IVault.json instead of Vault.json") name = "IVault" # Special case for proxy Vault with open(f'../contract/out/{name}.sol/{name}.json', 'rt') as file: return json.load(file) diff --git a/src/dexorder/contract/contract_proxy.py b/src/dexorder/contract/contract_proxy.py index faf7845..423e1e9 100644 --- a/src/dexorder/contract/contract_proxy.py +++ b/src/dexorder/contract/contract_proxy.py @@ -17,6 +17,12 @@ def call_wrapper(func): def transact_wrapper(func): + async def f(*args, **kwargs): + return await func(*args, **kwargs).transact() + return f + + +def build_wrapper(func): async def f(*args, **kwargs): try: account = current_account.get() @@ -78,6 +84,11 @@ class ContractProxy: # noinspection PyTypeChecker return ContractProxy(self.address, self._interface_name, _contracts=self._contracts, _wrapper=transact_wrapper, abi=self._abi) + @property + def build(self): + # noinspection PyTypeChecker + return ContractProxy(self.address, self._interface_name, _contracts=self._contracts, _wrapper=build_wrapper, abi=self._abi) + def __getattr__(self, item): return self._wrapper(self.contract.constructor if item == 'constructor' else self.contract.functions[item]) diff --git a/src/dexorder/event_handler.py b/src/dexorder/event_handler.py index 8cdfa6f..35c87ab 100644 --- a/src/dexorder/event_handler.py +++ b/src/dexorder/event_handler.py @@ -115,7 +115,7 @@ async def handle_order_placed(event: EventData): log.debug(f'raw order status {obj}') order = Order.create(vault.address, index, obj) await activate_order(order) - log.debug(f'new order {order}') + log.debug(f'new order {order} {order.order}') def handle_swap_filled(event: EventData): @@ -265,26 +265,27 @@ 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: + if old_req is None or old_req.height <= height: # <= used so proof is updated with more recent values log.info(f'execution request for {tk}') execution_requests[tk] = ExecutionRequest(height, proof) async def process_execution_requests(): height = current_block.get().height - execs = [] # which requests to act on + execs = {} # which requests to act on for tk, er in execution_requests.items(): tk: TrancheKey er: ExecutionRequest pending = inflight_execution_requests.get(tk) - if pending is None or pending > height or height-pending >= 30: # todo execution timeout => retry ; should we use timestamps? configure per-chain. - execs.append((tk,er)) + log.debug(f'tranche key {tk} pending height: {pending}') + if pending is None or height-pending >= 30: # todo execution timeout => retry ; should we use timestamps? configure per-chain. + execs[tk] = er else: log.debug(f'tranche {tk} is pending execution') # execute the list # todo batch execution - for tk, er in execs: + for tk, er in execs.items(): log.info(f'executing tranche {tk}') job = submit_transaction_request(new_tranche_execution_request(tk, er.proof)) inflight_execution_requests[tk] = height @@ -326,10 +327,14 @@ def finish_execution_request(req: TrancheExecutionRequest, error: str): 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() + try: + triggers = OrderTriggers.instances[order.key] + tranche_trigger = triggers.triggers[tk.tranche_index] + except KeyError: + pass + else: + tranche_trigger.status = TrancheStatus.Filled + tranche_trigger.disable() elif error == 'Too little received': # from UniswapV3 SwapRouter when not even 1 satoshi of output was gained log.debug('warning: de minimis liquidity in pool') diff --git a/src/dexorder/order/executionhandler.py b/src/dexorder/order/executionhandler.py index 85d92ea..c150fde 100644 --- a/src/dexorder/order/executionhandler.py +++ b/src/dexorder/order/executionhandler.py @@ -19,7 +19,7 @@ class TrancheExecutionHandler (TransactionHandler): async def build_transaction(self, job_id: UUID, req: TrancheExecutionRequest) -> dict: # noinspection PyBroadException try: - return await get_dexorder_contract().transact.execute(job_id.bytes, (req.vault, req.order_index, req.tranche_index, req.price_proof)) + return await get_dexorder_contract().build.execute(job_id.bytes, (req.vault, req.order_index, req.tranche_index, req.price_proof)) except ContractPanicError as px: log.error(f'While executing job {job_id}: {px}') await self.complete_transaction(db.session.get(TransactionJob, job_id)) diff --git a/src/dexorder/runner.py b/src/dexorder/runner.py index 58d34dd..cf12812 100644 --- a/src/dexorder/runner.py +++ b/src/dexorder/runner.py @@ -327,6 +327,8 @@ class BlockStateRunner: async def handle_time_tick(self, blockhash): + if current_blockstate.get() is None: + return # similar to handle_head, but we only call the postprocess events, since there was only a time tick and no new block data block = self.state.by_hash[blockhash] fork = self.state.fork(block) diff --git a/src/dexorder/transaction.py b/src/dexorder/transaction.py index 2502c90..cad99f6 100644 --- a/src/dexorder/transaction.py +++ b/src/dexorder/transaction.py @@ -67,9 +67,10 @@ async def create_transaction(job: TransactionJob): else: ctx: ContractTransaction = await handler.build_transaction(job.id, job.request) if ctx is None: - log.warning(f'unable to build transaction for job {job.id}') + log.warning(f'unable to send transaction for job {job.id}') return job.state = TransactionJobState.Signed # todo lazy signing + db.session.add(job) dbtx = DbTransaction(id=ctx.id_bytes, job=job, data=ctx.data, receipt=None) db.session.add(dbtx) log.info(f'servicing transaction request {job.request.__class__.__name__} {job.id} with tx {ctx.id}') @@ -83,6 +84,7 @@ async def send_transactions(): TransactionJob.chain == current_chain.get(), TransactionJob.state == TransactionJobState.Signed ): + log.debug(f'sending transaction for job {job.id}') sent = await w3.eth.send_raw_transaction(job.tx.data) assert sent == job.tx.id job.state = TransactionJobState.Sent