From 70e73915b313b6bac964c8f109b77a3c13753688 Mon Sep 17 00:00:00 2001 From: etzellux Date: Fri, 12 Sep 2025 16:23:55 +0300 Subject: [PATCH 1/2] add ordering module --- tinyman/ordering/__init__.py | 0 tinyman/ordering/base_client.py | 118 +++++ tinyman/ordering/client.py | 682 +++++++++++++++++++++++++ tinyman/ordering/constants.py | 34 ++ tinyman/ordering/event.py | 85 +++ tinyman/ordering/events.py | 314 ++++++++++++ tinyman/ordering/order_structs.json | 119 +++++ tinyman/ordering/registry_structs.json | 29 ++ tinyman/ordering/struct.py | 160 ++++++ tinyman/ordering/structs.py | 13 + tinyman/ordering/utils.py | 26 + 11 files changed, 1580 insertions(+) create mode 100644 tinyman/ordering/__init__.py create mode 100644 tinyman/ordering/base_client.py create mode 100644 tinyman/ordering/client.py create mode 100644 tinyman/ordering/constants.py create mode 100644 tinyman/ordering/event.py create mode 100644 tinyman/ordering/events.py create mode 100644 tinyman/ordering/order_structs.json create mode 100644 tinyman/ordering/registry_structs.json create mode 100644 tinyman/ordering/struct.py create mode 100644 tinyman/ordering/structs.py create mode 100644 tinyman/ordering/utils.py diff --git a/tinyman/ordering/__init__.py b/tinyman/ordering/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tinyman/ordering/base_client.py b/tinyman/ordering/base_client.py new file mode 100644 index 0000000..d86e627 --- /dev/null +++ b/tinyman/ordering/base_client.py @@ -0,0 +1,118 @@ +from base64 import b64decode, b64encode +import time +from tinyman.utils import TransactionGroup +from algosdk import transaction +from algosdk.logic import get_application_address + +from .struct import get_struct, get_box_costs + + +class BaseClient(): + def __init__(self, algod, app_id, user_address, user_sk) -> None: + self.algod = algod + self.app_id = app_id + self.application_address = get_application_address(self.app_id) + self.user_address = user_address + self.keys = {} + self.add_key(user_address, user_sk) + self.current_timestamp = None + self.simulate = False + + def get_suggested_params(self): + return self.algod.suggested_params() + + def get_current_timestamp(self): + return self.current_timestamp or time.time() + + def _submit(self, transactions, additional_fees=0): + transactions = self.flatten_transactions(transactions) + fee = transactions[0].fee + n = 0 + for txn in transactions: + if txn.fee == fee: + txn.fee = 0 + n += 1 + transactions[0].fee = (n + additional_fees) * fee + txn_group = TransactionGroup(transactions) + for address, key in self.keys.items(): + if isinstance(key, transaction.LogicSigAccount): + txn_group.sign_with_logicsig(key, address=address) + else: + txn_group.sign_with_private_key(address, key) + if self.simulate: + txn_info = self.algod.simulate_raw_transactions(txn_group.signed_transactions) + else: + txn_info = txn_group.submit(self.algod, wait=True) + return txn_info + + def flatten_transactions(self, txns): + result = [] + if isinstance(txns, transaction.Transaction): + result = [txns] + elif type(txns) == list: + for txn in txns: + result += self.flatten_transactions(txn) + return result + + def calculate_min_balance(self, accounts=0, assets=0, boxes=None): + cost = 0 + cost += accounts * 100_000 + cost += assets * 100_000 + cost += get_box_costs(boxes or {}) + return cost + + def add_key(self, address, key): + self.keys[address] = key + + def get_global(self, key, default=None, app_id=None): + app_id = app_id or self.app_id + global_state = {s["key"]: s["value"] for s in self.algod.application_info(app_id)["params"]["global-state"]} + key = b64encode(key).decode() + if key in global_state: + value = global_state[key] + if value["type"] == 2: + return value["uint"] + else: + return b64decode(value["bytes"]) + else: + return default + + def get_box(self, box_name, struct_name, app_id=None): + app_id = app_id or self.app_id + box_value = b64decode(self.algod.application_box_by_name(app_id, box_name)["value"]) + struct_class = get_struct(struct_name) + struct = struct_class(box_value) + return struct + + def box_exists(self, box_name, app_id=None): + app_id = app_id or self.app_id + try: + self.algod.application_box_by_name(app_id, box_name) + return True + except Exception: + return False + + def get_reward_slot(self, staking_asset_id, reward_asset_id): + asset_box = self.get_asset_box(staking_asset_id) + for i in range(8): + if asset_box.reward_slots[i].asset_id == reward_asset_id: + return i + + def is_opted_in(self, address, asset_id): + if asset_id == 0: + return True + + try: + self.algod.account_asset_info(address, asset_id) + return True + except Exception: + return False + + def get_optin_if_needed_txn(self, sender, asset_id): + if not self.is_opted_in(sender, asset_id): + txn = transaction.AssetOptInTxn( + sender=sender, + sp=self.get_suggested_params(), + index=asset_id, + ) + return txn diff --git a/tinyman/ordering/client.py b/tinyman/ordering/client.py new file mode 100644 index 0000000..968bf7d --- /dev/null +++ b/tinyman/ordering/client.py @@ -0,0 +1,682 @@ +from typing import List + +from algosdk.encoding import decode_address +from algosdk import transaction +from algosdk.encoding import decode_address, encode_address +from algosdk.logic import get_application_address +from tinyman.utils import int_to_bytes + +from .base_client import BaseClient +from .constants import * +from .structs import AppVersion, Entry, TriggerOrder +from .utils import int_array + +from .constants import order_approval_program, order_clear_state_program, order_app_global_schema, order_app_local_schema, order_app_extra_pages + + +class OrderingClient(BaseClient): + def __init__(self, algod, registry_app_id, vault_app_id, router_app_id, user_address, user_sk, order_app_id=None) -> None: + self.algod = algod + self.registry_app_id = registry_app_id + self.registry_application_address = get_application_address(registry_app_id) + self.vault_app_id = vault_app_id + self.vault_application_address = get_application_address(vault_app_id) + self.router_app_id = router_app_id + self.router_application_address = get_application_address(router_app_id) + self.app_id = order_app_id + self.application_address = get_application_address(self.app_id) if self.app_id else None + self.user_address = user_address + self.keys = {} + self.add_key(user_address, user_sk) + self.current_timestamp = None + self.simulate = False + + def get_registry_entry_box_name(self, user_address: str) -> bytes: + return b"e" + decode_address(user_address) + + def create_order_app(self): + sp = self.get_suggested_params() + + version = self.get_global(b"latest_version", app_id=self.registry_app_id) + if version is None: + raise Exception("Registry app has no approved version. Unable to create order app.") + + entry_box_name = self.get_registry_entry_box_name(self.user_address) + new_boxes = {} + if not self.box_exists(entry_box_name, self.registry_app_id): + new_boxes[entry_box_name] = Entry + + transactions = [ + transaction.PaymentTxn( + sender=self.user_address, + sp=sp, + receiver=self.registry_application_address, + amt=self.calculate_min_balance(boxes=new_boxes) + ) if new_boxes else None, + transaction.ApplicationCreateTxn( + sender=self.user_address, + sp=sp, + on_complete=transaction.OnComplete.NoOpOC, + app_args=[b"create_application", self.registry_app_id], + approval_program=order_approval_program.bytecode, + clear_program=order_clear_state_program.bytecode, + global_schema=order_app_global_schema, + local_schema=order_app_local_schema, + extra_pages=order_app_extra_pages, + ), + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=self.registry_app_id, + app_args=["create_entry"], + boxes=[ + (0, entry_box_name), + (self.registry_app_id, b"v" + version.to_bytes(8, "big")) + ], + ) + ] + + return self._submit(transactions, additional_fees=0) + + def update_ordering_app(self, version, approval_program): + sp = self.get_suggested_params() + + transactions = [ + transaction.ApplicationUpdateTxn( + sender=self.user_address, + sp=sp, + index=self.app_id, + app_args=[b"update_application", version], + approval_program=approval_program, + clear_program=order_clear_state_program.bytecode + ), + transaction.ApplicationNoOpTxn( + sender=self.user_address, + sp=sp, + index=self.registry_app_id, + app_args=[b"verify_update", version], + boxes=[(self.registry_app_id, b"v" + version.to_bytes(8, "big"))] + ), + transaction.ApplicationNoOpTxn( + sender=self.user_address, + sp=sp, + index=self.app_id, + app_args=[b"post_update"], + ), + ] + + return self._submit(transactions) + + def prepare_asset_opt_in_txn(self, asset_ids: List[int], sp): + asset_ids = int_array(asset_ids, 8, 0) + transactions = [ + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=self.app_id, + app_args=["asset_opt_in", asset_ids], + ) + ] + return transactions + + def asset_opt_in(self, asset_ids: List[int]): + sp = self.get_suggested_params() + transactions = self.prepare_asset_opt_in_txn(asset_ids, sp) + transactions.insert( + 0, + transaction.PaymentTxn( + sender=self.user_address, + sp=sp, + receiver=self.application_address, + amt=self.calculate_min_balance(assets=len(asset_ids)) + ), + ) + + return self._submit(transactions, additional_fees=len(asset_ids)) + + def get_order_count(self): + return self.get_global(TOTAL_ORDER_COUNT_KEY, 0, self.app_id) + + def get_order_box_name(self, id: int): + return b"o" + int_to_bytes(id) + + def get_recurring_order_box_name(self, id: int): + return b"r" + int_to_bytes(id) + + def put_trigger_order(self, asset_id: int, amount: int, target_asset_id: int, target_amount: int, is_partial_allowed: bool, duration: int=0, order_id: int=None): + sp = self.get_suggested_params() + + if order_id is None: + order_id = self.get_order_count() + + order_box_name = self.get_order_box_name(order_id) + new_boxes = {} + if not self.box_exists(order_box_name, self.app_id): + new_boxes[order_box_name] = TriggerOrder + + assets_to_optin = [asset_id, target_asset_id] + assets_to_optin = [aid for aid in assets_to_optin if not self.is_opted_in(self.application_address, aid)] + + transactions = [ + transaction.PaymentTxn( + sender=self.user_address, + sp=sp, + receiver=self.application_address, + amt=self.calculate_min_balance(boxes=new_boxes, assets=len(assets_to_optin)) + ) if new_boxes else None, + self.prepare_asset_opt_in_txn(assets_to_optin, sp) if assets_to_optin else None, + # Asset Transfer + transaction.PaymentTxn( + sender=self.user_address, + sp=sp, + receiver=self.application_address, + amt=amount + ) if asset_id == 0 else + transaction.AssetTransferTxn( + sender=self.user_address, + sp=sp, + receiver=self.application_address, + index=asset_id, + amt=amount, + ), + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=self.app_id, + app_args=[ + "put_trigger_order", + int_to_bytes(asset_id), + int_to_bytes(amount), + int_to_bytes(target_asset_id), + int_to_bytes(target_amount), + int_to_bytes(int(is_partial_allowed)), + int_to_bytes(duration) + ], + foreign_assets=[target_asset_id], + foreign_apps=[self.registry_app_id, self.vault_app_id], + boxes=[ + (0, order_box_name), + (self.vault_app_id, decode_address(self.user_address)), + (self.registry_app_id, self.get_registry_entry_box_name(self.user_address)), + ], + ) + ] + + return self._submit(transactions, additional_fees=2 + len(assets_to_optin)) + + def cancel_trigger_order(self, order_id: int): + sp = self.get_suggested_params() + + order_box_name = self.get_order_box_name(order_id) + order = self.get_box(order_box_name, "TriggerOrder") + + transactions = [ + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=self.app_id, + app_args=[ + "cancel_trigger_order", + int_to_bytes(order_id) + ], + boxes=[ + (0, order_box_name), + (self.registry_app_id, self.get_registry_entry_box_name(self.user_address)), + ], + foreign_assets=[order.asset_id], + foreign_apps=[self.registry_app_id] + ) + ] + + return self._submit(transactions, additional_fees=2) + + def prepare_start_execute_trigger_order_transaction(self, order_app_id: int, order_id: int, account_address: str, fill_amount: int, index_diff: int, sp) -> transaction.ApplicationCallTxn: + """ + It is assumed that the caller of this method is a filler. + + Parameters + ---------- + order_app_id : Id of the account's order app that will be filled. + account_address : Account whom its order will be filled. + """ + + order_box_name = self.get_order_box_name(order_id) + order = self.get_box(order_box_name, "TriggerOrder", app_id=order_app_id) + + txn = transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=order_app_id, + app_args=[ + "start_execute_trigger_order", + int_to_bytes(order_id), + int_to_bytes(fill_amount), + int_to_bytes(index_diff) + ], + boxes=[ + (0, order_box_name), + ], + accounts=[account_address], + foreign_assets=[order.asset_id] + ) + + return txn + + def prepare_end_execute_trigger_order_transaction(self, order_app_id: int, order_id: int, account_address: str, fill_amount: int, index_diff: int, sp) -> transaction.ApplicationCallTxn: + """ + It is assumed that the caller of this method is a filler. + + Parameters + ---------- + order_app_id : Id of the account's order app that will be filled. + account_address : Account whom its order will be filled. + """ + + order_box_name = self.get_order_box_name(order_id) + order = self.get_box(order_box_name, "TriggerOrder", app_id=order_app_id) + + txn = transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=order_app_id, + app_args=[ + "end_execute_trigger_order", + int_to_bytes(order_id), + int_to_bytes(fill_amount), + int_to_bytes(index_diff) + ], + boxes=[ + (0, order_box_name), + (self.registry_app_id, self.get_registry_entry_box_name(account_address)), + ], + accounts=[account_address, self.registry_application_address], + foreign_apps=[self.registry_app_id], + foreign_assets=[order.target_asset_id] + ) + + return txn + + def collect(self, order_id: int, order_type: str): + sp = self.get_suggested_params() + + if order_type == "o": + order_box_name = self.get_order_box_name(order_id) + elif order_type == "r": + order_box_name = self.get_recurring_order_box_name(order_id) + else: + raise NotImplementedError() + + order = self.get_box(order_box_name, "TriggerOrder") + + transactions = [ + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=self.app_id, + app_args=[ + "collect", + int_to_bytes(order_id), + order_type + ], + boxes=[ + (0, order_box_name), + ], + foreign_assets=[order.target_asset_id], + foreign_apps=[self.registry_app_id] + ) + ] + + return self._submit(transactions, additional_fees=2) + + def put_recurring_order(self, asset_id: int, amount: int, target_asset_id: int, target_recurrence: int, interval: int, min_target_amount: int=0, max_target_amount: int=0, order_id: int=None): + sp = self.get_suggested_params() + + if order_id is None: + order_id = self.get_order_count() + + order_box_name = self.get_recurring_order_box_name(order_id) + new_boxes = {} + if not self.box_exists(order_box_name, self.app_id): + new_boxes[order_box_name] = TriggerOrder + + assets_to_optin = [asset_id, target_asset_id] + assets_to_optin = [aid for aid in assets_to_optin if not self.is_opted_in(self.application_address, aid)] + + total_amount = amount * target_recurrence + transactions = [ + transaction.PaymentTxn( + sender=self.user_address, + sp=sp, + receiver=self.application_address, + amt=self.calculate_min_balance(boxes=new_boxes, assets=len(assets_to_optin)) + ) if new_boxes else None, + self.prepare_asset_opt_in_txn(assets_to_optin, sp) if assets_to_optin else None, + # Asset Transfer + transaction.PaymentTxn( + sender=self.user_address, + sp=sp, + receiver=self.application_address, + amt=total_amount + ) if asset_id == 0 else + transaction.AssetTransferTxn( + sender=self.user_address, + sp=sp, + receiver=self.application_address, + index=asset_id, + amt=total_amount, + ), + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=self.app_id, + app_args=[ + "put_recurring_order", + int_to_bytes(asset_id), + int_to_bytes(amount), + int_to_bytes(target_asset_id), + int_to_bytes(min_target_amount), + int_to_bytes(max_target_amount), + int_to_bytes(target_recurrence), + int_to_bytes(interval), + ], + foreign_assets=[target_asset_id], + foreign_apps=[self.registry_app_id, self.vault_app_id], + boxes=[ + (0, order_box_name), + (self.vault_app_id, decode_address(self.user_address)), + (self.registry_app_id, self.get_registry_entry_box_name(self.user_address)), + ], + ) + ] + + return self._submit(transactions, additional_fees=2 + len(assets_to_optin)) + + def cancel_recurring_order(self, order_id: int): + sp = self.get_suggested_params() + + order_box_name = self.get_recurring_order_box_name(order_id) + order = self.get_box(order_box_name, "RecurringOrder") + + transactions = [ + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=self.app_id, + app_args=[ + "cancel_recurring_order", + int_to_bytes(order_id) + ], + boxes=[ + (0, order_box_name), + (self.registry_app_id, self.get_registry_entry_box_name(self.user_address)), + ], + foreign_assets=[order.asset_id], + foreign_apps=[self.registry_app_id] + ) + ] + + return self._submit(transactions, additional_fees=2) + + def execute_recurring_order(self, order_app_id: int, order_id: int, route_bytes: bytes, pools_bytes: bytes, num_swaps: int, grouped_references: list, extra_txns=0): + sp = self.get_suggested_params() + + order_box_name = self.get_recurring_order_box_name(order_id) + order = self.get_box(order_box_name, "RecurringOrder", app_id=order_app_id) + user_address = encode_address(self.get_global(USER_ADDRESS_KEY, app_id=order_app_id)) + + num_inner_txns = (num_swaps * 3) + 2 + + transactions = [ + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=order_app_id, + app_args=[ + "execute_recurring_order", + order_id, + route_bytes, + pools_bytes, + num_swaps, + ], + boxes=[ + (0, order_box_name), + (self.registry_app_id, self.get_registry_entry_box_name(user_address)), + ], + accounts=[self.registry_application_address, user_address], + foreign_assets=[order.asset_id, order.target_asset_id], + foreign_apps=[self.registry_app_id, self.router_app_id], + ) + ] + for i in range(0, len(grouped_references)): + transactions.append(transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=self.router_app_id, + app_args=[ + "noop", + ], + accounts=grouped_references[i]["accounts"], + foreign_assets=grouped_references[i]["assets"], + foreign_apps=grouped_references[i]["apps"], + )) + + return self._submit(transactions, additional_fees=num_inner_txns + extra_txns) + + def registry_user_opt_in(self): + sp = self.get_suggested_params() + + transactions = [ + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.OptInOC, + sp=sp, + index=self.registry_app_id, + app_args=["user_opt_in"] + ) + ] + + return self._submit(transactions) + + +class RegistryClient(BaseClient): + def __init__(self, algod, registry_app_id, vault_app_id, user_address, user_sk) -> None: + self.algod = algod + self.app_id = registry_app_id + self.application_address = get_application_address(registry_app_id) + self.vault_app_id = vault_app_id + self.vault_application_address = get_application_address(vault_app_id) + self.user_address = user_address + self.keys = {} + self.add_key(user_address, user_sk) + self.current_timestamp = None + self.simulate = False + + def get_registry_entry_box_name(self, user_address: str) -> bytes: + return b"e" + decode_address(user_address) + + def propose_manager(self, new_manager_address): + sp = self.get_suggested_params() + + transactions = [ + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=self.app_id, + app_args=["propose_manager", decode_address(new_manager_address)], + ) + ] + + return self._submit(transactions) + + def accept_manager(self): + sp = self.get_suggested_params() + + transactions = [ + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=self.app_id, + app_args=["accept_manager"], + ) + ] + + return self._submit(transactions) + + def asset_opt_in(self, asset_id: int): + sp = self.get_suggested_params() + + transactions = [ + transaction.PaymentTxn( + sender=self.user_address, + sp=sp, + receiver=self.application_address, + amt=self.calculate_min_balance(assets=1) + ), + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=self.app_id, + app_args=["asset_opt_in", asset_id], + foreign_assets=[asset_id] + ) + ] + + return self._submit(transactions, additional_fees=1) + + def set_order_fee_rate(self, fee_rate: int): + sp = self.get_suggested_params() + + transactions = [ + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=self.app_id, + app_args=["set_order_fee_rate", fee_rate], + ) + ] + + return self._submit(transactions) + + def set_governor_order_fee_rate(self, fee_rate: int): + sp = self.get_suggested_params() + + transactions = [ + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=self.app_id, + app_args=["set_governor_order_fee_rate", fee_rate], + ) + ] + + return self._submit(transactions) + + def set_governor_fee_rate_power_threshold(self, threshold: int): + sp = self.get_suggested_params() + + transactions = [ + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=self.app_id, + app_args=["set_governor_fee_rate_power_threshold", threshold], + ) + ] + + return self._submit(transactions) + + def claim_fees(self, asset_id: int): + sp = self.get_suggested_params() + + transactions = [ + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=self.app_id, + app_args=["claim_fees", asset_id], + foreign_assets=[asset_id] + ) + ] + + return self._submit(transactions, additional_fees=1) + + def endorse(self, user_address: str): + sp = self.get_suggested_params() + + transactions = [ + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=self.app_id, + app_args=["endorse", decode_address(user_address)], + accounts=[user_address] + ) + ] + + return self._submit(transactions) + + def deendorse(self, user_address: str): + sp = self.get_suggested_params() + + transactions = [ + transaction.ApplicationCallTxn( + sender=self.user_address, + on_complete=transaction.OnComplete.NoOpOC, + sp=sp, + index=self.app_id, + app_args=["deendorse", decode_address(user_address)], + accounts=[user_address] + ) + ] + + return self._submit(transactions) + + def get_app_version_box_name(self, version: int): + return b"v" + int_to_bytes(version) + + def approve_version(self, version: int, approval_hash): + sp = self.get_suggested_params() + + app_version_box_name = self.get_app_version_box_name(version) + + new_boxes = {} + if not self.box_exists(app_version_box_name, self.app_id): + new_boxes[app_version_box_name] = AppVersion + + transactions = [ + transaction.PaymentTxn( + sender=self.user_address, + sp=sp, + receiver=self.application_address, + amt=self.calculate_min_balance(boxes=new_boxes) + ) if new_boxes else None, + transaction.ApplicationCallTxn( + sender=self.user_address, + sp=sp, + on_complete=transaction.OnComplete.NoOpOC, + index=self.app_id, + app_args=[b"approve_version", version, approval_hash], + boxes=[(0, app_version_box_name)] + ) + ] + + return self._submit(transactions) diff --git a/tinyman/ordering/constants.py b/tinyman/ordering/constants.py new file mode 100644 index 0000000..e0ace62 --- /dev/null +++ b/tinyman/ordering/constants.py @@ -0,0 +1,34 @@ +import requests +from algosdk import transaction + +# Order App Globals & Commons with Registry +REGISTRY_APP_ID_KEY = b"registry_app_id" +REGISTRY_APP_ACCOUNT_ADDRESS_KEY = b"registry_app_account_address" +VAULT_APP_ID_KEY = b"vault_app_id" +ROUTER_APP_ID_KEY = b"router_app_id" +ORDER_FEE_RATE_KEY = b"order_fee_rate" +GOVERNOR_ORDER_FEE_RATE_KEY = b"governor_order_fee_rate" +GOVERNOR_FEE_RATE_POWER_THRESHOLD = b"governor_fee_rate_power_threshold" + +USER_ADDRESS_KEY = b"user_address" +TOTAL_ORDER_COUNT_KEY = b"order_count" +PROPOSED_MANAGER_KEY = b"proposed_manager" +MANAGER_KEY = b"manager" +VERSION_KEY = b"version" + +# Registry App Globals +ENTRY_COUNT_KEY = b"entry_count" + +# Registry App Locals +IS_ENDORSED_KEY = b"is_endorsed" + +# App Creation Config +order_approval_program = requests.get("https://raw.githubusercontent.com/tinymanorg/tinyman-order-protocol/refs/tags/v4/contracts/order/build/order_approval.teal.tok").content +order_clear_state_program = requests.get("https://raw.githubusercontent.com/tinymanorg/tinyman-order-protocol/refs/tags/v4/contracts/order/build/order_clear_state.teal.tok").content +order_app_global_schema = transaction.StateSchema(num_uints=16, num_byte_slices=16) +order_app_local_schema = transaction.StateSchema(num_uints=0, num_byte_slices=0) +order_app_extra_pages = 3 + +# App Ids +TESTNET_ORDERING_REGISTRY_APP_ID = 739800082 +MAINNET_ORDERING_REGISTRY_APP_ID = 3019195131 diff --git a/tinyman/ordering/event.py b/tinyman/ordering/event.py new file mode 100644 index 0000000..f30887d --- /dev/null +++ b/tinyman/ordering/event.py @@ -0,0 +1,85 @@ +from dataclasses import dataclass +from typing import Optional + +from Cryptodome.Hash import SHA512 +from algosdk import abi +from algosdk.abi.base_type import ABI_LENGTH_SIZE + + +@dataclass +class Event: + name: str + args: [abi.Argument] + + @property + def signature(self): + arg_string = ",".join(str(arg.type) for arg in self.args) + event_signature = "{}({})".format(self.name, arg_string) + return event_signature + + @property + def selector(self): + sha_512_256_hash = SHA512.new(truncate="256") + sha_512_256_hash.update(self.signature.encode("utf-8")) + selector = sha_512_256_hash.digest()[:4] + return selector + + def decode(self, log): + selector, event_data = log[:4], log[4:] + assert self.selector == selector + + data = { + "event_name": self.name + } + start = 0 + for arg in self.args: + if arg.type.is_dynamic(): + if isinstance(arg.type, abi.StringType): + size = int.from_bytes(event_data[start:start + ABI_LENGTH_SIZE], "big") + elif isinstance(arg.type, abi.ArrayDynamicType): + size = int.from_bytes(event_data[start:start + ABI_LENGTH_SIZE], "big") * arg.type.child_type.byte_len() + elif isinstance(arg.type, abi.TupleType): + pass + else: + raise NotImplementedError() + + end = start + ABI_LENGTH_SIZE + size + else: + end = start + arg.type.byte_len() + + value = event_data[start:end] + if isinstance(arg.type, abi.ArrayStaticType) and isinstance(arg.type.child_type, abi.ByteType): + data[arg.name] = bytes(arg.type.decode(value)) + else: + data[arg.name] = arg.type.decode(value) + start = end + return data + + def encode(self, parameters: Optional[list] = None): + log = self.selector + if parameters is None: + parameters = [] + + assert len(parameters) == len(self.args) + for parameter, arg in zip(parameters, self.args): + log += arg.type.encode(parameter) + return log + + +def get_event_by_log(log: bytes, events: list[Event]): + event_selector = log[:4] + events_filtered = [event for event in events if event.selector == event_selector] + if not events_filtered: + return None + assert len(events_filtered) == 1 + event = events_filtered[0] + return event + + +def decode_logs(logs: list[bytes], events: list[Event]): + decoded_logs = [] + for log in logs: + event = get_event_by_log(log, events) + if event: + decoded_logs.append(event.decode(log)) + return decoded_logs diff --git a/tinyman/ordering/events.py b/tinyman/ordering/events.py new file mode 100644 index 0000000..eed5b54 --- /dev/null +++ b/tinyman/ordering/events.py @@ -0,0 +1,314 @@ +from algosdk import abi + +from .event import Event + + +# Registry Events +entry_event = Event( + name="entry", + args=[ + abi.Argument(arg_type="address", name="user_address"), + abi.Argument(arg_type="uint64", name="app_id") + ] +) + +set_order_fee_rate_event = Event( + name="set_order_fee_rate", + args=[ + abi.Argument(arg_type="uint64", name="fee_rate") + ] +) + +set_governor_order_fee_rate_event = Event( + name="set_governor_order_fee_rate", + args=[ + abi.Argument(arg_type="uint64", name="fee_rate") + ] +) + +set_governor_fee_rate_power_threshold_event = Event( + name="set_governor_fee_rate_power_threshold", + args=[ + abi.Argument(arg_type="uint64", name="threshold") + ] +) + +claim_fees_event = Event( + name="claim_fees", + args=[ + abi.Argument(arg_type="uint64", name="asset_id"), + abi.Argument(arg_type="uint64", name="amount") + ] +) + + +endorse_event = Event( + name="endorse", + args=[ + abi.Argument(arg_type="address", name="user_address") + ] +) + + +deendorse_event = Event( + name="deendorse", + args=[ + abi.Argument(arg_type="address", name="user_address") + ] +) + + +approve_version_event = Event( + name="approve_version", + args=[ + abi.Argument(arg_type="uint64", name="version"), + abi.Argument(arg_type="byte[32]", name="approval_hash") + ] +) + + +propose_manager_event = Event( + name="propose_manager", + args=[ + abi.Argument(arg_type="address", name="proposed_manager"), + ] +) + + +accept_manager_event = Event( + name="accept_manager", + args=[ + abi.Argument(arg_type="address", name="new_manager"), + ] +) + + +trigger_order_fields = [ + abi.Argument(arg_type="uint64", name="asset_id"), + abi.Argument(arg_type="uint64", name="amount"), + abi.Argument(arg_type="uint64", name="target_asset_id"), + abi.Argument(arg_type="uint64", name="target_amount"), + abi.Argument(arg_type="uint64", name="filled_amount"), + abi.Argument(arg_type="uint64", name="collected_target_amount"), + abi.Argument(arg_type="uint64", name="is_partial_allowed"), + abi.Argument(arg_type="uint64", name="fee_rate"), + abi.Argument(arg_type="uint64", name="creation_timestamp"), + abi.Argument(arg_type="uint64", name="expiration_timestamp") +] + +recurring_order_fields = [ + abi.Argument(arg_type="uint64", name="asset_id"), + abi.Argument(arg_type="uint64", name="amount"), + abi.Argument(arg_type="uint64", name="target_asset_id"), + abi.Argument(arg_type="uint64", name="collected_target_amount"), + abi.Argument(arg_type="uint64", name="min_target_amount"), + abi.Argument(arg_type="uint64", name="max_target_amount"), + abi.Argument(arg_type="uint64", name="remaining_recurrences"), + abi.Argument(arg_type="uint64", name="interval"), + abi.Argument(arg_type="uint64", name="fee_rate"), + abi.Argument(arg_type="uint64", name="last_fill_timestamp"), + abi.Argument(arg_type="uint64", name="creation_timestamp") +] + + +registry_update_ordering_application_event = Event( + name="update_ordering_application", + args=[ + abi.Argument(arg_type="uint64", name="order_app_id"), + abi.Argument(arg_type="uint64", name="version"), + ] +) + + +registry_put_trigger_order_event = Event( + name="put_trigger_order", + args=[ + abi.Argument(arg_type="uint64", name="order_app_id"), + abi.Argument(arg_type="uint64", name="order_id"), + ] + trigger_order_fields +) + + +registry_update_trigger_order_event = Event( + name="update_trigger_order", + args=[ + abi.Argument(arg_type="uint64", name="order_app_id"), + abi.Argument(arg_type="uint64", name="order_id"), + ] + trigger_order_fields +) + + +registry_cancel_trigger_order_event = Event( + name="cancel_trigger_order", + args=[ + abi.Argument(arg_type="uint64", name="order_app_id"), + abi.Argument(arg_type="uint64", name="order_id"), + ] +) + + +registry_put_recurring_order_event = Event( + name="put_recurring_order", + args=[ + abi.Argument(arg_type="uint64", name="order_app_id"), + abi.Argument(arg_type="uint64", name="order_id"), + ] + recurring_order_fields +) + + +registry_update_recurring_order_event = Event( + name="update_recurring_order", + args=[ + abi.Argument(arg_type="uint64", name="order_app_id"), + abi.Argument(arg_type="uint64", name="order_id"), + ] + recurring_order_fields +) + + +registry_cancel_recurring_order_event = Event( + name="cancel_recurring_order", + args=[ + abi.Argument(arg_type="uint64", name="order_app_id"), + abi.Argument(arg_type="uint64", name="order_id"), + ] +) + + +# Order Events +update_application_event = Event( + name="update_application", + args=[ + abi.Argument(arg_type="address", name="user_address"), + abi.Argument(arg_type="uint64", name="version"), + ] +) + + +trigger_order_event = Event( + name="trigger_order", + args=[ + abi.Argument(arg_type="address", name="user_address"), + abi.Argument(arg_type="uint64", name="order_id"), + ] + trigger_order_fields +) + + +put_trigger_order_event = Event( + name="put_trigger_order", + args=[ + abi.Argument(arg_type="uint64", name="order_id"), + ] +) + + +cancel_trigger_order_event = Event( + name="cancel_trigger_order", + args=[ + abi.Argument(arg_type="uint64", name="order_id"), + ] +) + + +start_execute_trigger_order_event = Event( + name="start_execute_trigger_order", + args=[ + abi.Argument(arg_type="address", name="user_address"), + abi.Argument(arg_type="uint64", name="order_id"), + abi.Argument(arg_type="address", name="filler_address"), + ] +) + + +end_execute_trigger_order_event = Event( + name="end_execute_trigger_order", + args=[ + abi.Argument(arg_type="address", name="user_address"), + abi.Argument(arg_type="uint64", name="order_id"), + abi.Argument(arg_type="address", name="filler_address"), + abi.Argument(arg_type="uint64", name="fill_amount"), + abi.Argument(arg_type="uint64", name="bought_amount"), + ] +) + + +collect_event = Event( + name="collect", + args=[ + abi.Argument(arg_type="uint64", name="order_id"), + abi.Argument(arg_type="uint64", name="collected_target_amount") + ] +) + + +# Recurring Order Events +recurring_order_event = Event( + name="recurring_order", + args=[ + abi.Argument(arg_type="address", name="user_address"), + abi.Argument(arg_type="uint64", name="order_id"), + ] + recurring_order_fields +) + + +put_recurring_order_event = Event( + name="put_recurring_order", + args=[ + abi.Argument(arg_type="uint64", name="order_id"), + ] +) + + +cancel_recurring_order_event = Event( + name="cancel_recurring_order", + args=[ + abi.Argument(arg_type="uint64", name="order_id"), + ] +) + + +execute_recurring_order_event = Event( + name="execute_recurring_order", + args=[ + abi.Argument(arg_type="address", name="user_address"), + abi.Argument(arg_type="uint64", name="order_id"), + abi.Argument(arg_type="address", name="filler_address"), + abi.Argument(arg_type="uint64", name="fill_amount"), + abi.Argument(arg_type="uint64", name="bought_amount"), + ] +) + + +registry_events = [ + set_order_fee_rate_event, + set_governor_order_fee_rate_event, + set_governor_fee_rate_power_threshold_event, + claim_fees_event, + endorse_event, + deendorse_event, + approve_version_event, + propose_manager_event, + accept_manager_event, + entry_event, + registry_update_ordering_application_event, + registry_put_trigger_order_event, + registry_update_trigger_order_event, + registry_cancel_trigger_order_event, + registry_put_recurring_order_event, + registry_update_recurring_order_event, + registry_cancel_recurring_order_event, +] + + +ordering_events = [ + update_application_event, + trigger_order_event, + put_trigger_order_event, + cancel_trigger_order_event, + start_execute_trigger_order_event, + end_execute_trigger_order_event, + collect_event, + recurring_order_event, + put_recurring_order_event, + cancel_recurring_order_event, + execute_recurring_order_event, +] \ No newline at end of file diff --git a/tinyman/ordering/order_structs.json b/tinyman/ordering/order_structs.json new file mode 100644 index 0000000..a01cccc --- /dev/null +++ b/tinyman/ordering/order_structs.json @@ -0,0 +1,119 @@ +{ + "structs": { + "TriggerOrder": { + "size": 80, + "fields": { + "asset_id": { + "type": "int", + "size": 8, + "offset": 0 + }, + "amount": { + "type": "int", + "size": 8, + "offset": 8 + }, + "target_asset_id": { + "type": "int", + "size": 8, + "offset": 16 + }, + "target_amount": { + "type": "int", + "size": 8, + "offset": 24 + }, + "filled_amount": { + "type": "int", + "size": 8, + "offset": 32 + }, + "collected_target_amount": { + "type": "int", + "size": 8, + "offset": 40 + }, + "is_partial_allowed": { + "type": "int", + "size": 8, + "offset": 48 + }, + "fee_rate": { + "type": "int", + "size": 8, + "offset": 56 + }, + "creation_timestamp": { + "type": "int", + "size": 8, + "offset": 64 + }, + "expiration_timestamp": { + "type": "int", + "size": 8, + "offset": 72 + } + } + }, + "RecurringOrder": { + "size": 88, + "fields": { + "asset_id": { + "type": "int", + "size": 8, + "offset": 0 + }, + "amount": { + "type": "int", + "size": 8, + "offset": 8 + }, + "target_asset_id": { + "type": "int", + "size": 8, + "offset": 16 + }, + "collected_target_amount": { + "type": "int", + "size": 8, + "offset": 24 + }, + "min_target_amount": { + "type": "int", + "size": 8, + "offset": 32 + }, + "max_target_amount": { + "type": "int", + "size": 8, + "offset": 40 + }, + "remaining_recurrences": { + "type": "int", + "size": 8, + "offset": 48 + }, + "interval": { + "type": "int", + "size": 8, + "offset": 56 + }, + "fee_rate": { + "type": "int", + "size": 8, + "offset": 64 + }, + "last_fill_timestamp": { + "type": "int", + "size": 8, + "offset": 72 + }, + "creation_timestamp": { + "type": "int", + "size": 8, + "offset": 80 + } + } + } + } +} diff --git a/tinyman/ordering/registry_structs.json b/tinyman/ordering/registry_structs.json new file mode 100644 index 0000000..3758988 --- /dev/null +++ b/tinyman/ordering/registry_structs.json @@ -0,0 +1,29 @@ +{ + "structs": { + "Entry": { + "size": 8, + "fields": { + "app_id": { + "type": "int", + "size": 8, + "offset": 0 + } + } + }, + "AppVersion": { + "size": 982, + "fields": { + "approval_hash": { + "type": "bytes[32]", + "size": 32, + "offset": 0 + }, + "unused": { + "type": "bytes[950]", + "size": 950, + "offset": 32 + } + } + } + } +} diff --git a/tinyman/ordering/struct.py b/tinyman/ordering/struct.py new file mode 100644 index 0000000..38027c7 --- /dev/null +++ b/tinyman/ordering/struct.py @@ -0,0 +1,160 @@ +import json +import re +from typing import Any, Dict + + +MINIMUM_BALANCE_REQUIREMENT_PER_BOX = 2_500 +MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE = 400 + + +class StructRegistry: + def __init__(self): + self.struct_definitions: Dict[str, Dict] = {} + + def load_from_file(self, filepath: str) -> None: + """Load struct definitions from a JSON file.""" + with open(filepath, 'r') as f: + data = json.load(f) + self.load_from_dict(data.get('structs', {})) + + def load_from_dict(self, struct_dict: Dict) -> None: + """Load struct definitions from a dictionary.""" + self.struct_definitions.update(struct_dict) + + def get_type(self, name: str) -> Any: + """Get the appropriate type handler for a given type name.""" + if name == "int": + return TealishInt() + elif name.startswith("uint"): + return TealishInt() + elif name.startswith("bytes"): + return TealishBytes() + elif name in self.struct_definitions: + return Struct(name=name, manager=self, **self.struct_definitions[name]) + elif "[" in name: + name, length = re.match(r"([A-Za-z_0-9]+)\[(\d+)\]", name).groups() + return ArrayData( + Struct(name=name, manager=self, **self.struct_definitions[name]), + int(length) + ) + else: + raise KeyError(f"Unknown type: {name}") + + def get_struct(self, name: str) -> 'Struct': + """Get a Struct instance by name.""" + if name not in self.struct_definitions: + raise KeyError(f"Struct '{name}' not found") + return Struct(name=name, **self.struct_definitions[name]) + + +STRUCT_REGISTRY = StructRegistry() + + +def register_struct_file(filepath: str) -> None: + STRUCT_REGISTRY.load_from_file(filepath) + + +def get_struct(name: str) -> 'Struct': + return STRUCT_REGISTRY.get_struct(name) + + +class Struct(): + def __init__(self, name, size, fields): + self._name = name + self._size = size + self._fields = fields + + def __call__(self, data=None) -> Any: + if data is None: + data = bytearray(self._size) + self._data = memoryview(data) + return self + + def __getattribute__(self, name: str) -> Any: + if name.startswith("_"): + return super().__getattribute__(name) + field = self._fields[name] + start = field["offset"] + end = field["offset"] + field["size"] + value = self._data[start:end] + type = STRUCT_REGISTRY.get_type(field["type"]) + return type(value) + + def __setattr__(self, name: str, value: Any) -> None: + if name.startswith("_"): + return super().__setattr__(name, value) + field = self._fields[name] + start = field["offset"] + end = field["offset"] + field["size"] + if field["type"] in ("int",): + value = value.to_bytes(field["size"], "big") + if isinstance(value, (Struct, ArrayData)): + value = value._data + self._data[start:end] = value + + def __setitem__(self, index, value): + if isinstance(value, (Struct, ArrayData)): + value = value._data + self._data[:] = value + + def __str__(self) -> str: + return repr(bytes(self._data)) + + def __repr__(self) -> str: + fields = {f: getattr(self, f) for f in self._fields} + return f"{self._name}({fields})" + + def __len__(self): + return len(self._data) + + def __conform__(self, protocol): + return bytes(self._data) + + def __bytes__(self): + return bytes(self._data.tobytes()) + + +class ArrayData(): + def __init__(self, struct, length): + self._struct = struct + self._length = length + + def __call__(self, data=None) -> Any: + if data is None: + data = bytearray(self._struct._size * self.length) + self._data = memoryview(data) + return self + + def __getitem__(self, index): + offset = self._struct._size * index + end = offset + self._struct._size + value = self._data[offset:end] + return self._struct(value) + + def __setitem__(self, index, value): + offset = self._struct._size * index + end = offset + self._struct._size + if isinstance(value, Struct): + value = value._data + self._data[offset:end] = value + + def __repr__(self) -> str: + return ", ".join(repr(self[i]) for i in range(self._length)) + + +class TealishInt(): + def __call__(self, value) -> Any: + return int.from_bytes(value, "big") + + +class TealishBytes(): + def __call__(self, value) -> Any: + return value + + +def get_box_costs(boxes): + cost = MINIMUM_BALANCE_REQUIREMENT_PER_BOX + for name, struct in boxes.items(): + cost += len(name) * MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE + cost += struct._size * MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE + return cost diff --git a/tinyman/ordering/structs.py b/tinyman/ordering/structs.py new file mode 100644 index 0000000..0cfe759 --- /dev/null +++ b/tinyman/ordering/structs.py @@ -0,0 +1,13 @@ +from pathlib import Path +from .struct import register_struct_file, get_struct + +SDK_DIR = Path(__file__).parent + + +register_struct_file(filepath=SDK_DIR / "order_structs.json") +register_struct_file(filepath=SDK_DIR / "registry_structs.json") + +AppVersion = get_struct("AppVersion") +Entry = get_struct("Entry") +TriggerOrder = get_struct("TriggerOrder") +RecurringOrder = get_struct("RecurringOrder") diff --git a/tinyman/ordering/utils.py b/tinyman/ordering/utils.py new file mode 100644 index 0000000..39cd402 --- /dev/null +++ b/tinyman/ordering/utils.py @@ -0,0 +1,26 @@ +from math import ceil +from tinyman.utils import int_to_bytes +from hashlib import sha256 + + +def int_array(elements, size, default=0): + array = [default] * size + + for i in range(len(elements)): + array[i] = elements[i] + bytes = b"".join(map(int_to_bytes, array)) + return bytes + + +def calculate_approval_hash(bytecode): + approval_hash = bytes(32) + # the AVM gives access to approval programs in chunks of up to 4096 bytes + chunk_size = 4096 + num_chunks = ceil(len(bytecode) / chunk_size) + chunk_hashes = b"" + for i in range(num_chunks): + offset = (i * chunk_size) + chunk = bytecode[offset: offset + chunk_size] + chunk_hashes += sha256(chunk).digest() + approval_hash = sha256(chunk_hashes).digest() + return approval_hash From ff5ac07c263c1080513a30a9d7f62932bdace5a5 Mon Sep 17 00:00:00 2001 From: etzellux Date: Fri, 12 Sep 2025 16:28:41 +0300 Subject: [PATCH 2/2] fix linting --- tinyman/ordering/base_client.py | 2 +- tinyman/ordering/client.py | 9 ++++----- tinyman/ordering/events.py | 2 +- tinyman/ordering/struct.py | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tinyman/ordering/base_client.py b/tinyman/ordering/base_client.py index d86e627..4332293 100644 --- a/tinyman/ordering/base_client.py +++ b/tinyman/ordering/base_client.py @@ -49,7 +49,7 @@ def flatten_transactions(self, txns): result = [] if isinstance(txns, transaction.Transaction): result = [txns] - elif type(txns) == list: + elif isinstance(txns, list): for txn in txns: result += self.flatten_transactions(txn) return result diff --git a/tinyman/ordering/client.py b/tinyman/ordering/client.py index 968bf7d..f993d2d 100644 --- a/tinyman/ordering/client.py +++ b/tinyman/ordering/client.py @@ -1,6 +1,5 @@ from typing import List -from algosdk.encoding import decode_address from algosdk import transaction from algosdk.encoding import decode_address, encode_address from algosdk.logic import get_application_address @@ -145,7 +144,7 @@ def get_order_box_name(self, id: int): def get_recurring_order_box_name(self, id: int): return b"r" + int_to_bytes(id) - def put_trigger_order(self, asset_id: int, amount: int, target_asset_id: int, target_amount: int, is_partial_allowed: bool, duration: int=0, order_id: int=None): + def put_trigger_order(self, asset_id: int, amount: int, target_asset_id: int, target_amount: int, is_partial_allowed: bool, duration: int = 0, order_id: int = None): sp = self.get_suggested_params() if order_id is None: @@ -335,7 +334,7 @@ def collect(self, order_id: int, order_type: str): return self._submit(transactions, additional_fees=2) - def put_recurring_order(self, asset_id: int, amount: int, target_asset_id: int, target_recurrence: int, interval: int, min_target_amount: int=0, max_target_amount: int=0, order_id: int=None): + def put_recurring_order(self, asset_id: int, amount: int, target_asset_id: int, target_recurrence: int, interval: int, min_target_amount: int = 0, max_target_amount: int = 0, order_id: int = None): sp = self.get_suggested_params() if order_id is None: @@ -556,7 +555,7 @@ def asset_opt_in(self, asset_id: int): ] return self._submit(transactions, additional_fees=1) - + def set_order_fee_rate(self, fee_rate: int): sp = self.get_suggested_params() @@ -571,7 +570,7 @@ def set_order_fee_rate(self, fee_rate: int): ] return self._submit(transactions) - + def set_governor_order_fee_rate(self, fee_rate: int): sp = self.get_suggested_params() diff --git a/tinyman/ordering/events.py b/tinyman/ordering/events.py index eed5b54..b3ff5d5 100644 --- a/tinyman/ordering/events.py +++ b/tinyman/ordering/events.py @@ -311,4 +311,4 @@ put_recurring_order_event, cancel_recurring_order_event, execute_recurring_order_event, -] \ No newline at end of file +] diff --git a/tinyman/ordering/struct.py b/tinyman/ordering/struct.py index 38027c7..248a2f5 100644 --- a/tinyman/ordering/struct.py +++ b/tinyman/ordering/struct.py @@ -34,7 +34,7 @@ def get_type(self, name: str) -> Any: elif "[" in name: name, length = re.match(r"([A-Za-z_0-9]+)\[(\d+)\]", name).groups() return ArrayData( - Struct(name=name, manager=self, **self.struct_definitions[name]), + Struct(name=name, manager=self, **self.struct_definitions[name]), int(length) ) else: