From 1fd40c891f1d2fffe0b91a56673b1c108e7774f3 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 7 Dec 2022 19:30:33 +0300 Subject: [PATCH 01/32] initialize api integration --- tinyman/client.py | 34 ++++++++++++- tinyman/swap_router/__init__.py | 0 tinyman/swap_router/constants.py | 6 +++ tinyman/swap_router/routes.py | 82 ++++++++++++++++++++++++++++++ tinyman/swap_router/swap_router.py | 72 ++++++++++++++++++++++++++ tinyman/v1/client.py | 9 ++-- tinyman/v2/client.py | 2 + 7 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 tinyman/swap_router/__init__.py create mode 100644 tinyman/swap_router/constants.py create mode 100644 tinyman/swap_router/routes.py create mode 100644 tinyman/swap_router/swap_router.py diff --git a/tinyman/client.py b/tinyman/client.py index 0af997c..f1204e8 100644 --- a/tinyman/client.py +++ b/tinyman/client.py @@ -2,8 +2,11 @@ from algosdk.v2client.algod import AlgodClient from algosdk.future.transaction import wait_for_confirmation +from requests import request, HTTPError + from tinyman.assets import Asset from tinyman.optin import prepare_asset_optin_transactions +from tinyman.swap_router.constants import FIXED_INPUT_SWAP_TYPE, FIXED_OUTPUT_SWAP_TYPE class BaseTinymanClient: @@ -11,10 +14,12 @@ def __init__( self, algod_client: AlgodClient, validator_app_id: int, - user_address=None, + api_base_url: Optional[str] = None, + user_address: str = None, staking_app_id: Optional[int] = None, ): self.algod = algod_client + self.api_base_url = api_base_url self.validator_app_id = validator_app_id self.staking_app_id = staking_app_id self.assets_cache = {} @@ -66,3 +71,30 @@ def asset_is_opted_in(self, asset_id, user_address=None): if a["asset-id"] == asset_id: return True return False + + def fetch_best_swap_route(self, asset_in_id: int, asset_out_id: int, swap_type: str, amount: int): + assert swap_type in (FIXED_INPUT_SWAP_TYPE, FIXED_OUTPUT_SWAP_TYPE) + assert amount > 0 + assert asset_in_id >= 0 + assert asset_out_id >= 0 + + data = { + "asset_in_id": str(asset_in_id), + "asset_out_id": str(asset_out_id), + "swap_type": swap_type, + "amount": str(amount) + } + + response = request( + method="POST", + url=self.api_base_url + "v1/swap-router/", + json=data, + ) + if response.status_code != 200: + breakpoint() + raise HTTPError(response=response) + response_data = response.json() + + # TODO: Handle the response and create transactions. + print(response_data) + raise NotImplementedError() diff --git a/tinyman/swap_router/__init__.py b/tinyman/swap_router/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tinyman/swap_router/constants.py b/tinyman/swap_router/constants.py new file mode 100644 index 0000000..810c1b8 --- /dev/null +++ b/tinyman/swap_router/constants.py @@ -0,0 +1,6 @@ + +TESTNET_SWAP_ROUTER_APP_ID_V1 = 0 # TODO +MAINNET_SWAP_ROUTER_APP_ID_V1 = 0 # TODO + +FIXED_INPUT_SWAP_TYPE = "fixed-input" +FIXED_OUTPUT_SWAP_TYPE = "fixed-output" diff --git a/tinyman/swap_router/routes.py b/tinyman/swap_router/routes.py new file mode 100644 index 0000000..5313172 --- /dev/null +++ b/tinyman/swap_router/routes.py @@ -0,0 +1,82 @@ +from dataclasses import dataclass + +from tinyman.assets import Asset, AssetAmount +from tinyman.v2.exceptions import InsufficientReserve +from tinyman.v2.pools import Pool + + +@dataclass +class Route: + asset_in: Asset + asset_out: Asset + pools: list[Pool] # TODO: Naming? pools or path + + def __str__(self): + return "Route: " + "-> ".join(f"{pool.asset_1.unit_name}/{pool.asset_2.unit_name}" for pool in self.pools) + + # Fixed-Input + def get_fixed_input_quotes(self, amount_in: int, slippage: float = 0.05): + quotes = [] + assert self.pools + + current_asset_in_amount = AssetAmount( + asset=self.asset_in, + amount=amount_in + ) + + for pool in self.pools: + quote = pool.fetch_fixed_input_swap_quote( + amount_in=current_asset_in_amount, + slippage=slippage, + refresh=False, + ) + + quotes.append(quote) + current_asset_in_amount = quote.amount_out + + last_quote = quotes[-1] + assert last_quote.amount_out.asset.id == self.asset_out.id + return quotes + + def get_fixed_input_last_quote(self, amount_in: int, slippage: float = 0.05): + try: + quotes = self.get_fixed_input_quotes(amount_in=amount_in, slippage=slippage) + except InsufficientReserve: + return None + + last_quote = quotes[-1] + return last_quote + + # Fixed-Output + def get_fixed_output_quotes(self, amount_out: int, slippage: float = 0.05): + quotes = [] + assert self.pools + + current_asset_out_amount = AssetAmount( + asset=self.asset_out, + amount=amount_out + ) + + for pool in self.pools[::-1]: + quote = pool.fetch_fixed_output_swap_quote( + amount_out=current_asset_out_amount, + slippage=slippage, + refresh=False, + ) + + quotes.append(quote) + current_asset_out_amount = quote.amount_in + + quotes.reverse() + first_quote = quotes[0] + assert first_quote.amount_in.asset.id == self.asset_in.id + return quotes + + def get_fixed_output_first_quote(self, amount_out: int, slippage: float = 0.05): + try: + quotes = self.get_fixed_output_quotes(amount_out=amount_out, slippage=slippage) + except InsufficientReserve: + return None + + first_quote = quotes[0] + return first_quote diff --git a/tinyman/swap_router/swap_router.py b/tinyman/swap_router/swap_router.py new file mode 100644 index 0000000..3824ff7 --- /dev/null +++ b/tinyman/swap_router/swap_router.py @@ -0,0 +1,72 @@ +from algosdk.future.transaction import AssetTransferTxn, ApplicationNoOpTxn, SuggestedParams, PaymentTxn +from tinyman.utils import TransactionGroup +from tinyman.v2.constants import FIXED_INPUT_APP_ARGUMENT, FIXED_OUTPUT_APP_ARGUMENT +from tinyman.v2.contracts import get_pool_logicsig + +SWAP_ROUTER_APP_ID = 0 +SWAP_ROUTER_ADDRESS = "" + + +def prepare_swap_router_transactions( + router_app_id: int, + amm_app_id: int, + input_asset_id: int, + intermediary_asset_id: int, + output_asset_id: int, + asset_in_amount: int, + asset_out_amount: int, + swap_type: [str, bytes], + sender: str, + suggested_params: SuggestedParams, +) -> TransactionGroup: + # TODO: WIP + pool_1_logicsig = get_pool_logicsig(amm_app_id, input_asset_id, intermediary_asset_id) + pool_1_address = pool_1_logicsig.address() + + pool_2_logicsig = get_pool_logicsig(amm_app_id, intermediary_asset_id, output_asset_id) + pool_2_address = pool_2_logicsig.address() + + txns = [ + AssetTransferTxn( + sender=sender, + sp=suggested_params, + receiver=SWAP_ROUTER_ADDRESS, + index=input_asset_id, + amt=asset_in_amount, + ) + if input_asset_id != 0 + else PaymentTxn( + sender=sender, + sp=suggested_params, + receiver=SWAP_ROUTER_ADDRESS, + amt=asset_in_amount, + ), + ApplicationNoOpTxn( + sender=sender, + sp=suggested_params, + index=router_app_id, + app_args=["swap", swap_type, asset_out_amount], + accounts=[pool_1_address, pool_2_address], + foreign_apps=[amm_app_id], + foreign_assets=[input_asset_id, intermediary_asset_id, output_asset_id], + ) + ] + + if isinstance(swap_type, bytes): + pass + elif isinstance(swap_type, str): + swap_type = swap_type.encode() + else: + raise NotImplementedError() + + min_fee = suggested_params.min_fee + if swap_type == FIXED_INPUT_APP_ARGUMENT: + app_call_fee = min_fee * 8 + elif swap_type == FIXED_OUTPUT_APP_ARGUMENT: + app_call_fee = min_fee * 9 + else: + raise NotImplementedError() + + txns[-1].fee = app_call_fee + txn_group = TransactionGroup(txns) + return txn_group diff --git a/tinyman/v1/client.py b/tinyman/v1/client.py index 88a4908..c309eae 100644 --- a/tinyman/v1/client.py +++ b/tinyman/v1/client.py @@ -1,14 +1,15 @@ from base64 import b64decode -from algosdk.v2client.algod import AlgodClient + from algosdk.encoding import encode_address +from algosdk.v2client.algod import AlgodClient + from tinyman.assets import AssetAmount from tinyman.client import BaseTinymanClient +from tinyman.optin import prepare_app_optin_transactions from tinyman.staking.constants import ( TESTNET_STAKING_APP_ID, MAINNET_STAKING_APP_ID, ) - -from tinyman.optin import prepare_app_optin_transactions from tinyman.v1.constants import ( TESTNET_VALIDATOR_APP_ID, MAINNET_VALIDATOR_APP_ID, @@ -68,6 +69,7 @@ def __init__(self, algod_client: AlgodClient, user_address=None): super().__init__( algod_client, validator_app_id=TESTNET_VALIDATOR_APP_ID, + api_base_url="https://testnet.analytics.tinyman.org/api/", user_address=user_address, staking_app_id=TESTNET_STAKING_APP_ID, ) @@ -78,6 +80,7 @@ def __init__(self, algod_client: AlgodClient, user_address=None): super().__init__( algod_client, validator_app_id=MAINNET_VALIDATOR_APP_ID, + api_base_url="https://mainnet.analytics.tinyman.org/api/", user_address=user_address, staking_app_id=MAINNET_STAKING_APP_ID, ) diff --git a/tinyman/v2/client.py b/tinyman/v2/client.py index 789c262..8048f69 100644 --- a/tinyman/v2/client.py +++ b/tinyman/v2/client.py @@ -23,6 +23,7 @@ def __init__(self, algod_client: AlgodClient, user_address=None): super().__init__( algod_client, validator_app_id=TESTNET_VALIDATOR_APP_ID, + api_base_url="https://testnet.analytics.tinyman.org/api/", user_address=user_address, staking_app_id=TESTNET_STAKING_APP_ID, ) @@ -33,6 +34,7 @@ def __init__(self, algod_client: AlgodClient, user_address=None): super().__init__( algod_client, validator_app_id=MAINNET_VALIDATOR_APP_ID, + api_base_url="https://mainnet.analytics.tinyman.org/api/", user_address=user_address, staking_app_id=MAINNET_STAKING_APP_ID, ) From 8313a22d27fb089aa2d6ccf84f9a60085709fe62 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Tue, 20 Dec 2022 23:18:16 +0300 Subject: [PATCH 02/32] handle swap router response --- tinyman/client.py | 31 +--- tinyman/swap_router/constants.py | 5 +- tinyman/swap_router/routes.py | 22 ++- tinyman/swap_router/swap_router.py | 234 +++++++++++++++++++++++++++-- tinyman/swap_router/utils.py | 51 +++++++ tinyman/v1/pools.py | 35 ++++- 6 files changed, 309 insertions(+), 69 deletions(-) create mode 100644 tinyman/swap_router/utils.py diff --git a/tinyman/client.py b/tinyman/client.py index f1204e8..62e8264 100644 --- a/tinyman/client.py +++ b/tinyman/client.py @@ -1,12 +1,10 @@ from typing import Optional -from algosdk.v2client.algod import AlgodClient from algosdk.future.transaction import wait_for_confirmation -from requests import request, HTTPError +from algosdk.v2client.algod import AlgodClient from tinyman.assets import Asset from tinyman.optin import prepare_asset_optin_transactions -from tinyman.swap_router.constants import FIXED_INPUT_SWAP_TYPE, FIXED_OUTPUT_SWAP_TYPE class BaseTinymanClient: @@ -71,30 +69,3 @@ def asset_is_opted_in(self, asset_id, user_address=None): if a["asset-id"] == asset_id: return True return False - - def fetch_best_swap_route(self, asset_in_id: int, asset_out_id: int, swap_type: str, amount: int): - assert swap_type in (FIXED_INPUT_SWAP_TYPE, FIXED_OUTPUT_SWAP_TYPE) - assert amount > 0 - assert asset_in_id >= 0 - assert asset_out_id >= 0 - - data = { - "asset_in_id": str(asset_in_id), - "asset_out_id": str(asset_out_id), - "swap_type": swap_type, - "amount": str(amount) - } - - response = request( - method="POST", - url=self.api_base_url + "v1/swap-router/", - json=data, - ) - if response.status_code != 200: - breakpoint() - raise HTTPError(response=response) - response_data = response.json() - - # TODO: Handle the response and create transactions. - print(response_data) - raise NotImplementedError() diff --git a/tinyman/swap_router/constants.py b/tinyman/swap_router/constants.py index 810c1b8..db60d6b 100644 --- a/tinyman/swap_router/constants.py +++ b/tinyman/swap_router/constants.py @@ -1,6 +1,5 @@ - -TESTNET_SWAP_ROUTER_APP_ID_V1 = 0 # TODO -MAINNET_SWAP_ROUTER_APP_ID_V1 = 0 # TODO +TESTNET_SWAP_ROUTER_APP_ID_V1 = 0 # TODO +MAINNET_SWAP_ROUTER_APP_ID_V1 = 0 # TODO FIXED_INPUT_SWAP_TYPE = "fixed-input" FIXED_OUTPUT_SWAP_TYPE = "fixed-output" diff --git a/tinyman/swap_router/routes.py b/tinyman/swap_router/routes.py index 5313172..4bed531 100644 --- a/tinyman/swap_router/routes.py +++ b/tinyman/swap_router/routes.py @@ -1,28 +1,27 @@ from dataclasses import dataclass +from typing import Union from tinyman.assets import Asset, AssetAmount from tinyman.v2.exceptions import InsufficientReserve -from tinyman.v2.pools import Pool +from tinyman.v1.pools import Pool as TinymanV1Pool +from tinyman.v2.pools import Pool as TinymanV2Pool @dataclass class Route: asset_in: Asset asset_out: Asset - pools: list[Pool] # TODO: Naming? pools or path + pools: list[Union[TinymanV1Pool, TinymanV2Pool]] # TODO: Naming? pools or path def __str__(self): - return "Route: " + "-> ".join(f"{pool.asset_1.unit_name}/{pool.asset_2.unit_name}" for pool in self.pools) + return "Route: " + "-> ".join(f"{pool}" for pool in self.pools) # Fixed-Input def get_fixed_input_quotes(self, amount_in: int, slippage: float = 0.05): quotes = [] assert self.pools - current_asset_in_amount = AssetAmount( - asset=self.asset_in, - amount=amount_in - ) + current_asset_in_amount = AssetAmount(asset=self.asset_in, amount=amount_in) for pool in self.pools: quote = pool.fetch_fixed_input_swap_quote( @@ -52,10 +51,7 @@ def get_fixed_output_quotes(self, amount_out: int, slippage: float = 0.05): quotes = [] assert self.pools - current_asset_out_amount = AssetAmount( - asset=self.asset_out, - amount=amount_out - ) + current_asset_out_amount = AssetAmount(asset=self.asset_out, amount=amount_out) for pool in self.pools[::-1]: quote = pool.fetch_fixed_output_swap_quote( @@ -74,7 +70,9 @@ def get_fixed_output_quotes(self, amount_out: int, slippage: float = 0.05): def get_fixed_output_first_quote(self, amount_out: int, slippage: float = 0.05): try: - quotes = self.get_fixed_output_quotes(amount_out=amount_out, slippage=slippage) + quotes = self.get_fixed_output_quotes( + amount_out=amount_out, slippage=slippage + ) except InsufficientReserve: return None diff --git a/tinyman/swap_router/swap_router.py b/tinyman/swap_router/swap_router.py index 3824ff7..5011f3e 100644 --- a/tinyman/swap_router/swap_router.py +++ b/tinyman/swap_router/swap_router.py @@ -1,55 +1,78 @@ -from algosdk.future.transaction import AssetTransferTxn, ApplicationNoOpTxn, SuggestedParams, PaymentTxn +from algosdk.future.transaction import ( + AssetTransferTxn, + ApplicationNoOpTxn, + SuggestedParams, + PaymentTxn, +) +from algosdk.logic import get_application_address +from requests import request, HTTPError + +from tinyman.swap_router.constants import ( + FIXED_INPUT_SWAP_TYPE, + FIXED_OUTPUT_SWAP_TYPE, + TESTNET_SWAP_ROUTER_APP_ID_V1, +) +from tinyman.swap_router.routes import Route +from tinyman.swap_router.utils import ( + get_best_fixed_input_route, + get_best_fixed_output_route, +) from tinyman.utils import TransactionGroup +from tinyman.v1.client import TinymanClient +from tinyman.v1.pools import Pool as TinymanV1Pool +from tinyman.v2.client import TinymanV2Client from tinyman.v2.constants import FIXED_INPUT_APP_ARGUMENT, FIXED_OUTPUT_APP_ARGUMENT from tinyman.v2.contracts import get_pool_logicsig - -SWAP_ROUTER_APP_ID = 0 -SWAP_ROUTER_ADDRESS = "" +from tinyman.v2.pools import Pool as TinymanV2Pool def prepare_swap_router_transactions( router_app_id: int, - amm_app_id: int, + validator_app_id: int, input_asset_id: int, intermediary_asset_id: int, output_asset_id: int, asset_in_amount: int, asset_out_amount: int, swap_type: [str, bytes], - sender: str, + user_address: str, suggested_params: SuggestedParams, ) -> TransactionGroup: - # TODO: WIP - pool_1_logicsig = get_pool_logicsig(amm_app_id, input_asset_id, intermediary_asset_id) + # TODO: MVP + pool_1_logicsig = get_pool_logicsig( + validator_app_id, input_asset_id, intermediary_asset_id + ) pool_1_address = pool_1_logicsig.address() - pool_2_logicsig = get_pool_logicsig(amm_app_id, intermediary_asset_id, output_asset_id) + pool_2_logicsig = get_pool_logicsig( + validator_app_id, intermediary_asset_id, output_asset_id + ) pool_2_address = pool_2_logicsig.address() txns = [ AssetTransferTxn( - sender=sender, + sender=user_address, sp=suggested_params, - receiver=SWAP_ROUTER_ADDRESS, + receiver=get_application_address(router_app_id), index=input_asset_id, amt=asset_in_amount, ) if input_asset_id != 0 else PaymentTxn( - sender=sender, + sender=user_address, sp=suggested_params, - receiver=SWAP_ROUTER_ADDRESS, + receiver=get_application_address(router_app_id), amt=asset_in_amount, ), ApplicationNoOpTxn( - sender=sender, + sender=user_address, sp=suggested_params, index=router_app_id, app_args=["swap", swap_type, asset_out_amount], accounts=[pool_1_address, pool_2_address], - foreign_apps=[amm_app_id], + foreign_apps=[validator_app_id], foreign_assets=[input_asset_id, intermediary_asset_id, output_asset_id], - ) + ), ] if isinstance(swap_type, bytes): @@ -70,3 +93,182 @@ def prepare_swap_router_transactions( txns[-1].fee = app_call_fee txn_group = TransactionGroup(txns) return txn_group + + +def prepare_transactions( + route: Route, + swap_type: str, + amount: int, + slippage: float = 0.05, + user_address: str = None, + suggested_params: SuggestedParams = None, +): + if swap_type == FIXED_INPUT_SWAP_TYPE: + quotes = route.get_fixed_input_quotes(amount_in=amount, slippage=slippage) + elif swap_type == FIXED_OUTPUT_SWAP_TYPE: + quotes = route.get_fixed_output_quotes(amount_out=amount, slippage=slippage) + else: + raise NotImplementedError() + + swap_count = len(route.pools) + if swap_count == 1: + pool = route.pools[0] + quote = quotes[0] + + if isinstance(pool, TinymanV1Pool): + return pool.prepare_swap_transactions_from_quote( + quote=quote, + swapper_address=user_address, + ) + elif isinstance(pool, TinymanV2Pool): + return pool.prepare_swap_transactions_from_quote( + quote=quote, + user_address=user_address, + suggested_params=suggested_params, + ) + else: + raise NotImplementedError() + + elif swap_count == 2: + if quotes[0].amount_in.asset.id == route.asset_in.id: + intermediary_asset_id = quotes[0].amount_out.asset.id + else: + intermediary_asset_id = quotes[0].amount_in.asset.id + + prepare_swap_router_transactions( + # TODO: Add router_app_id to client. + router_app_id=TESTNET_SWAP_ROUTER_APP_ID_V1, + validator_app_id=TinymanV2Client.validator_app_id, + input_asset_id=route.asset_in.id, + intermediary_asset_id=intermediary_asset_id, + output_asset_id=route.asset_out.id, + asset_in_amount=quotes[0].amount_in_with_slippage, + asset_out_amount=quotes[-1].amount_out_with_slippage, + swap_type=swap_type, + user_address=user_address, + suggested_params=suggested_params, + ) + + else: + raise NotImplementedError() + + +def fetch_smart_swap_route( + tinyman_v1_client: TinymanClient, + tinyman_v2_client: TinymanV2Client, + asset_in_id: int, + asset_out_id: int, + swap_type: str, + amount: int, +): + assert swap_type in (FIXED_INPUT_SWAP_TYPE, FIXED_OUTPUT_SWAP_TYPE) + assert amount > 0 + assert asset_in_id >= 0 + assert asset_out_id >= 0 + assert isinstance(tinyman_v1_client, TinymanClient) + assert isinstance(tinyman_v2_client, TinymanV2Client) + + payload = { + "asset_in_id": str(asset_in_id), + "asset_out_id": str(asset_out_id), + "swap_type": swap_type, + "amount": str(amount), + } + + raw_response = request( + method="POST", + url="http://dev.analytics.tinyman.org/api/v1/swap-router/", + # url=client.api_base_url + "v1/swap-router/", + json=payload, + ) + + # TODO: Handle all errors properly. + if raw_response.status_code != 200: + raise HTTPError(response=raw_response) + + response = raw_response.json() + print(response) + + pools = [] + for swap in response["route"]: + if swap["pool"]["version"] == "1.1": + pool = TinymanV1Pool( + client=tinyman_v1_client, + asset_a=swap["pool"]["asset_1_id"], + asset_b=swap["pool"]["asset_2_id"], + fetch=True, + ) + elif swap["pool"]["version"] == "2.0": + pool = TinymanV2Pool( + client=tinyman_v2_client, + asset_a=swap["pool"]["asset_1_id"], + asset_b=swap["pool"]["asset_2_id"], + fetch=True, + ) + else: + raise NotImplementedError() + pools.append(pool) + + route = Route( + asset_in=tinyman_v2_client.fetch_asset(asset_in_id), + asset_out=tinyman_v2_client.fetch_asset(asset_out_id), + pools=pools, + ) + return route + + +def fetch_best_route( + tinyman_v1_client: TinymanClient, + tinyman_v2_client: TinymanV2Client, + asset_in_id: int, + asset_out_id: int, + swap_type: str, + amount: int, +): + asset_in = tinyman_v2_client.fetch_asset(asset_in_id) + asset_out = tinyman_v2_client.fetch_asset(asset_out_id) + routes = [] + + v1_pool = TinymanV1Pool( + client=tinyman_v1_client, + asset_a=asset_in, + asset_b=asset_out, + fetch=True, + ) + if v1_pool.exists: + direct_v1_route = Route(asset_in=asset_in, asset_out=asset_out, pools=[v1_pool]) + routes.append(direct_v1_route) + + v2_pool = TinymanV2Pool( + client=tinyman_v2_client, + asset_a=asset_in, + asset_b=asset_out, + fetch=True, + ) + if v2_pool.exists: + direct_v2_route = Route(asset_in=asset_in, asset_out=asset_out, pools=[v2_pool]) + routes.append(direct_v2_route) + + try: + smart_swap_route = fetch_smart_swap_route( + tinyman_v1_client=tinyman_v1_client, + tinyman_v2_client=tinyman_v2_client, + asset_in_id=asset_in_id, + asset_out_id=asset_out_id, + swap_type=swap_type, + amount=amount, + ) + except Exception: # TODO: Handle the exception properly. + smart_swap_route = None + + if smart_swap_route is not None: + routes.append(smart_swap_route) + + if swap_type == FIXED_INPUT_SWAP_TYPE: + best_route = get_best_fixed_input_route(routes=routes, amount_in=amount) + elif swap_type == FIXED_OUTPUT_SWAP_TYPE: + best_route = get_best_fixed_output_route(routes=routes, amount_out=amount) + else: + raise NotImplementedError() + + return best_route diff --git a/tinyman/swap_router/utils.py b/tinyman/swap_router/utils.py new file mode 100644 index 0000000..04588bc --- /dev/null +++ b/tinyman/swap_router/utils.py @@ -0,0 +1,51 @@ +from tinyman.v2.exceptions import InsufficientReserve + + +def get_best_fixed_input_route(routes, amount_in): + best_route = None + best_route_max_price_impact = None + best_route_amount_out = None + + for route in routes: + try: + quotes = route.get_fixed_input_quotes(amount_in=amount_in) + except InsufficientReserve: + continue + + last_quote = quotes[-1] + max_price_impact = max(quote.price_impact for quote in quotes) + + if (not best_route) or ( + (best_route_amount_out, -best_route_max_price_impact) + < (last_quote.amount_out, -max_price_impact) + ): + best_route = route + best_route_amount_out = last_quote.amount_out + best_route_max_price_impact = max_price_impact + + return best_route + + +def get_best_fixed_output_route(routes, amount_out): + best_route = None + best_route_max_price_impact = None + best_route_amount_in = None + + for route in routes: + try: + quotes = route.get_fixed_output_quotes(amount_out=amount_out) + except InsufficientReserve: + continue + + first_quote = quotes[0] + max_price_impact = max(quote.price_impact for quote in quotes) + + if (not best_route) or ( + (best_route_amount_in, best_route_max_price_impact) + > (first_quote.amount_in, max_price_impact) + ): + best_route = route + best_route_amount_in = first_quote.amount_in + best_route_max_price_impact = max_price_impact + + return best_route diff --git a/tinyman/v1/pools.py b/tinyman/v1/pools.py index 76dc0a8..5f2b53d 100644 --- a/tinyman/v1/pools.py +++ b/tinyman/v1/pools.py @@ -189,6 +189,9 @@ def __init__( elif info is not None: self.update_from_info(info) + def __repr__(self): + return f"Pool {self.asset1.unit_name}({self.asset1.id})-{self.asset2.unit_name}({self.asset2.id}) {self.address}" + @classmethod def from_account_info(cls, account_info, client=None): info = get_pool_info_from_account_info(account_info) @@ -284,11 +287,18 @@ def convert(self, amount: AssetAmount): return AssetAmount(self.asset1, int(amount.amount * self.asset2_price)) def fetch_mint_quote( - self, amount_a: AssetAmount, amount_b: AssetAmount = None, slippage=0.05 + self, + amount_a: AssetAmount, + amount_b: AssetAmount = None, + slippage=0.05, + refresh: bool = True, ): amount1 = amount_a if amount_a.asset == self.asset1 else amount_b amount2 = amount_a if amount_a.asset == self.asset2 else amount_b - self.refresh() + + if refresh: + self.refresh() + if not self.exists: raise Exception("Pool has not been bootstrapped yet!") if self.issued_liquidity: @@ -322,10 +332,13 @@ def fetch_mint_quote( ) return quote - def fetch_burn_quote(self, liquidity_asset_in, slippage=0.05): + def fetch_burn_quote(self, liquidity_asset_in, slippage=0.05, refresh: bool = True): if isinstance(liquidity_asset_in, int): liquidity_asset_in = AssetAmount(self.liquidity_asset, liquidity_asset_in) - self.refresh() + + if refresh: + self.refresh() + asset1_amount = ( liquidity_asset_in.amount * self.asset1_reserves ) / self.issued_liquidity @@ -344,10 +357,13 @@ def fetch_burn_quote(self, liquidity_asset_in, slippage=0.05): return quote def fetch_fixed_input_swap_quote( - self, amount_in: AssetAmount, slippage=0.05 + self, amount_in: AssetAmount, slippage=0.05, refresh: bool = True ) -> SwapQuote: asset_in, asset_in_amount = amount_in.asset, amount_in.amount - self.refresh() + + if refresh: + self.refresh() + if asset_in == self.asset1: asset_out = self.asset2 input_supply = self.asset1_reserves @@ -390,10 +406,13 @@ def fetch_fixed_input_swap_quote( return quote def fetch_fixed_output_swap_quote( - self, amount_out: AssetAmount, slippage=0.05 + self, amount_out: AssetAmount, slippage=0.05, refresh: bool = True ) -> SwapQuote: asset_out, asset_out_amount = amount_out.asset, amount_out.amount - self.refresh() + + if refresh: + self.refresh() + if asset_out == self.asset1: asset_in = self.asset2 input_supply = self.asset2_reserves From c34e34cfda5cb849a4fa2240b50adf39d0bc47e7 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Fri, 13 Jan 2023 17:05:39 +0300 Subject: [PATCH 03/32] fix imports --- tinyman/swap_router/routes.py | 8 ++++---- tinyman/swap_router/utils.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tinyman/swap_router/routes.py b/tinyman/swap_router/routes.py index 4bed531..dc18965 100644 --- a/tinyman/swap_router/routes.py +++ b/tinyman/swap_router/routes.py @@ -2,8 +2,8 @@ from typing import Union from tinyman.assets import Asset, AssetAmount -from tinyman.v2.exceptions import InsufficientReserve from tinyman.v1.pools import Pool as TinymanV1Pool +from tinyman.v2.exceptions import InsufficientReserves from tinyman.v2.pools import Pool as TinymanV2Pool @@ -11,7 +11,7 @@ class Route: asset_in: Asset asset_out: Asset - pools: list[Union[TinymanV1Pool, TinymanV2Pool]] # TODO: Naming? pools or path + pools: list[Union[TinymanV1Pool, TinymanV2Pool]] def __str__(self): return "Route: " + "-> ".join(f"{pool}" for pool in self.pools) @@ -40,7 +40,7 @@ def get_fixed_input_quotes(self, amount_in: int, slippage: float = 0.05): def get_fixed_input_last_quote(self, amount_in: int, slippage: float = 0.05): try: quotes = self.get_fixed_input_quotes(amount_in=amount_in, slippage=slippage) - except InsufficientReserve: + except InsufficientReserves: return None last_quote = quotes[-1] @@ -73,7 +73,7 @@ def get_fixed_output_first_quote(self, amount_out: int, slippage: float = 0.05): quotes = self.get_fixed_output_quotes( amount_out=amount_out, slippage=slippage ) - except InsufficientReserve: + except InsufficientReserves: return None first_quote = quotes[0] diff --git a/tinyman/swap_router/utils.py b/tinyman/swap_router/utils.py index 04588bc..d7753b8 100644 --- a/tinyman/swap_router/utils.py +++ b/tinyman/swap_router/utils.py @@ -1,4 +1,4 @@ -from tinyman.v2.exceptions import InsufficientReserve +from tinyman.v2.exceptions import InsufficientReserves def get_best_fixed_input_route(routes, amount_in): @@ -9,7 +9,7 @@ def get_best_fixed_input_route(routes, amount_in): for route in routes: try: quotes = route.get_fixed_input_quotes(amount_in=amount_in) - except InsufficientReserve: + except InsufficientReserves: continue last_quote = quotes[-1] @@ -34,7 +34,7 @@ def get_best_fixed_output_route(routes, amount_out): for route in routes: try: quotes = route.get_fixed_output_quotes(amount_out=amount_out) - except InsufficientReserve: + except InsufficientReserves: continue first_quote = quotes[0] From 29edd88a1ac2b3772d44917898dcb162e5c378e8 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Tue, 17 Jan 2023 20:17:03 +0300 Subject: [PATCH 04/32] improve swap router api integration --- examples/swap_router/__init__.py | 0 examples/swap_router/swap_fixed_input.py | 39 ++++ tinyman/client.py | 4 +- tinyman/swap_router/swap_router.py | 228 ++++++++++++----------- tinyman/swap_router/utils.py | 4 +- tinyman/v1/pools.py | 2 + tinyman/v2/client.py | 21 ++- 7 files changed, 184 insertions(+), 114 deletions(-) create mode 100644 examples/swap_router/__init__.py create mode 100644 examples/swap_router/swap_fixed_input.py diff --git a/examples/swap_router/__init__.py b/examples/swap_router/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/swap_router/swap_fixed_input.py b/examples/swap_router/swap_fixed_input.py new file mode 100644 index 0000000..3b1116b --- /dev/null +++ b/examples/swap_router/swap_fixed_input.py @@ -0,0 +1,39 @@ +from algosdk.account import generate_account +from algosdk.v2client.algod import AlgodClient + +from tinyman.swap_router.swap_router import fetch_swap_route_quotes, prepare_swap_router_transactions_from_quotes +from tinyman.v1.client import TinymanTestnetClient +from tinyman.v2.client import TinymanV2TestnetClient + +ALGO_ASSET_ID = 0 +USDC_ASSET_ID = 10458941 +private_key, address = generate_account() + +algod_client = AlgodClient("", "https://testnet-api.algonode.network") + +tinyman_v1_client = TinymanTestnetClient(algod_client=algod_client) +tinyman_v2_client = TinymanV2TestnetClient(algod_client=algod_client) + +swap_type = "fixed-input" +amount = 1_000_000 + +route_pools_and_quotes = fetch_swap_route_quotes( + tinyman_v1_client=tinyman_v1_client, + tinyman_v2_client=tinyman_v2_client, + asset_in_id=ALGO_ASSET_ID, + asset_out_id=USDC_ASSET_ID, + swap_type=swap_type, + amount=amount +) +print(route_pools_and_quotes) + +txn_group = prepare_swap_router_transactions_from_quotes( + route_pools_and_quotes=route_pools_and_quotes, + swap_type=swap_type, + user_address=address, + suggested_params=algod_client.suggested_params() +) + +for txn in txn_group.transactions: + print() + print(txn.dictify()) diff --git a/tinyman/client.py b/tinyman/client.py index ec4aba4..9d62634 100644 --- a/tinyman/client.py +++ b/tinyman/client.py @@ -29,7 +29,9 @@ def __init__( def fetch_pool(self, *args, **kwargs): raise NotImplementedError() - def fetch_asset(self, asset_id): + def fetch_asset(self, asset_id: int): + asset_id = int(asset_id) + if asset_id not in self.assets_cache: asset = Asset(asset_id) asset.fetch(self.algod) diff --git a/tinyman/swap_router/swap_router.py b/tinyman/swap_router/swap_router.py index 5011f3e..bcb8a05 100644 --- a/tinyman/swap_router/swap_router.py +++ b/tinyman/swap_router/swap_router.py @@ -1,3 +1,5 @@ +from typing import Union + from algosdk.future.transaction import ( AssetTransferTxn, ApplicationNoOpTxn, @@ -7,23 +9,19 @@ from algosdk.logic import get_application_address from requests import request, HTTPError +from tinyman.assets import AssetAmount from tinyman.swap_router.constants import ( FIXED_INPUT_SWAP_TYPE, FIXED_OUTPUT_SWAP_TYPE, - TESTNET_SWAP_ROUTER_APP_ID_V1, -) -from tinyman.swap_router.routes import Route -from tinyman.swap_router.utils import ( - get_best_fixed_input_route, - get_best_fixed_output_route, ) from tinyman.utils import TransactionGroup from tinyman.v1.client import TinymanClient -from tinyman.v1.pools import Pool as TinymanV1Pool +from tinyman.v1.pools import Pool as TinymanV1Pool, SwapQuote as TinymanV1SwapQuote from tinyman.v2.client import TinymanV2Client from tinyman.v2.constants import FIXED_INPUT_APP_ARGUMENT, FIXED_OUTPUT_APP_ARGUMENT from tinyman.v2.contracts import get_pool_logicsig from tinyman.v2.pools import Pool as TinymanV2Pool +from tinyman.v2.quotes import SwapQuote as TinymanV2SwapQuote def prepare_swap_router_transactions( @@ -95,30 +93,27 @@ def prepare_swap_router_transactions( return txn_group -def prepare_transactions( - route: Route, +def prepare_swap_router_transactions_from_quotes( + route_pools_and_quotes: list[Union[tuple[TinymanV1Pool, TinymanV1SwapQuote], tuple[TinymanV2Pool, TinymanV2SwapQuote]]], swap_type: str, - amount: int, slippage: float = 0.05, user_address: str = None, suggested_params: SuggestedParams = None, -): - if swap_type == FIXED_INPUT_SWAP_TYPE: - quotes = route.get_fixed_input_quotes(amount_in=amount, slippage=slippage) - elif swap_type == FIXED_OUTPUT_SWAP_TYPE: - quotes = route.get_fixed_output_quotes(amount_out=amount, slippage=slippage) - else: - raise NotImplementedError() +) -> TransactionGroup: + # override slippage + for i in range(len(route_pools_and_quotes)): + route_pools_and_quotes[i][1].slippage = slippage - swap_count = len(route.pools) + swap_count = len(route_pools_and_quotes) if swap_count == 1: - pool = route.pools[0] - quote = quotes[0] + pool, quote = route_pools_and_quotes[0] + quote.slippage = slippage if isinstance(pool, TinymanV1Pool): return pool.prepare_swap_transactions_from_quote( quote=quote, swapper_address=user_address, + # suggested_params=suggested_params, ) elif isinstance(pool, TinymanV2Pool): return pool.prepare_swap_transactions_from_quote( @@ -130,20 +125,18 @@ def prepare_transactions( raise NotImplementedError() elif swap_count == 2: - if quotes[0].amount_in.asset.id == route.asset_in.id: - intermediary_asset_id = quotes[0].amount_out.asset.id - else: - intermediary_asset_id = quotes[0].amount_in.asset.id - - prepare_swap_router_transactions( - # TODO: Add router_app_id to client. - router_app_id=TESTNET_SWAP_ROUTER_APP_ID_V1, - validator_app_id=TinymanV2Client.validator_app_id, - input_asset_id=route.asset_in.id, - intermediary_asset_id=intermediary_asset_id, - output_asset_id=route.asset_out.id, - asset_in_amount=quotes[0].amount_in_with_slippage, - asset_out_amount=quotes[-1].amount_out_with_slippage, + pools, quotes = zip(*route_pools_and_quotes) + router_app_id = pools[0].client.router_app_id + validator_app_id = pools[0].client.validator_app_id + + return prepare_swap_router_transactions( + router_app_id=router_app_id, + validator_app_id=validator_app_id, + input_asset_id=quotes[0].amount_in.asset.id, + intermediary_asset_id=quotes[0].amount_out.asset.id, + output_asset_id=quotes[-1].amount_out.asset.id, + asset_in_amount=quotes[0].amount_in_with_slippage.amount, + asset_out_amount=quotes[-1].amount_out_with_slippage.amount, swap_type=swap_type, user_address=user_address, suggested_params=suggested_params, @@ -153,14 +146,14 @@ def prepare_transactions( raise NotImplementedError() -def fetch_smart_swap_route( +def fetch_swap_route_quotes( tinyman_v1_client: TinymanClient, tinyman_v2_client: TinymanV2Client, asset_in_id: int, asset_out_id: int, swap_type: str, amount: int, -): +) -> list[Union[tuple[TinymanV1Pool, TinymanV1SwapQuote], tuple[TinymanV2Pool, TinymanV2SwapQuote]]]: assert swap_type in (FIXED_INPUT_SWAP_TYPE, FIXED_OUTPUT_SWAP_TYPE) assert amount > 0 assert asset_in_id >= 0 @@ -178,7 +171,7 @@ def fetch_smart_swap_route( raw_response = request( method="POST", url="http://dev.analytics.tinyman.org/api/v1/swap-router/", - # url=client.api_base_url + "v1/swap-router/", + # TODO: url=client.api_base_url + "v1/swap-router/", "v1/swap-router/quotes/", json=payload, ) @@ -189,86 +182,109 @@ def fetch_smart_swap_route( response = raw_response.json() print(response) - pools = [] + route_pools_and_quotes = [] for swap in response["route"]: if swap["pool"]["version"] == "1.1": + client = tinyman_v1_client pool = TinymanV1Pool( - client=tinyman_v1_client, - asset_a=swap["pool"]["asset_1_id"], - asset_b=swap["pool"]["asset_2_id"], + client=client, + asset_a=client.fetch_asset(int(swap["pool"]["asset_1_id"])), + asset_b=client.fetch_asset(int(swap["pool"]["asset_2_id"])), fetch=True, ) + + asset_in = client.fetch_asset(int(swap["quote"]["amount_in"]["asset_id"])) + amount_out = client.fetch_asset(int(swap["quote"]["amount_out"]["asset_id"])) + + quote = TinymanV1SwapQuote( + swap_type=swap_type, + amount_in=AssetAmount(asset_in, int(swap["quote"]["amount_in"]["amount"])), + amount_out=AssetAmount(amount_out, int(swap["quote"]["amount_out"]["amount"])), + swap_fees=AssetAmount(asset_in, int(swap["quote"]["amount_in"]["amount"])), + slippage=0, + price_impact=swap["quote"]["price_impact"], + ) + elif swap["pool"]["version"] == "2.0": + client = tinyman_v2_client + pool = TinymanV2Pool( - client=tinyman_v2_client, - asset_a=swap["pool"]["asset_1_id"], - asset_b=swap["pool"]["asset_2_id"], + client=client, + asset_a=client.fetch_asset(int(swap["pool"]["asset_1_id"])), + asset_b=client.fetch_asset(int(swap["pool"]["asset_2_id"])), fetch=True, ) - else: - raise NotImplementedError() - pools.append(pool) - - route = Route( - asset_in=tinyman_v2_client.fetch_asset(asset_in_id), - asset_out=tinyman_v2_client.fetch_asset(asset_out_id), - pools=pools, - ) - return route + asset_in = client.fetch_asset(int(swap["quote"]["amount_in"]["asset_id"])) + amount_out = client.fetch_asset(int(swap["quote"]["amount_out"]["asset_id"])) -def fetch_best_route( - tinyman_v1_client: TinymanClient, - tinyman_v2_client: TinymanV2Client, - asset_in_id: int, - asset_out_id: int, - swap_type: str, - amount: int, -): - asset_in = tinyman_v2_client.fetch_asset(asset_in_id) - asset_out = tinyman_v2_client.fetch_asset(asset_out_id) - routes = [] - - v1_pool = TinymanV1Pool( - client=tinyman_v1_client, - asset_a=asset_in, - asset_b=asset_out, - fetch=True, - ) - if v1_pool.exists: - direct_v1_route = Route(asset_in=asset_in, asset_out=asset_out, pools=[v1_pool]) - routes.append(direct_v1_route) - - v2_pool = TinymanV2Pool( - client=tinyman_v2_client, - asset_a=asset_in, - asset_b=asset_out, - fetch=True, - ) - if v2_pool.exists: - direct_v2_route = Route(asset_in=asset_in, asset_out=asset_out, pools=[v2_pool]) - routes.append(direct_v2_route) - - try: - smart_swap_route = fetch_smart_swap_route( - tinyman_v1_client=tinyman_v1_client, - tinyman_v2_client=tinyman_v2_client, - asset_in_id=asset_in_id, - asset_out_id=asset_out_id, - swap_type=swap_type, - amount=amount, - ) - except Exception: # TODO: Handle the exception properly. - smart_swap_route = None + quote = TinymanV2SwapQuote( + swap_type=swap_type, + amount_in=AssetAmount(asset_in, int(swap["quote"]["amount_in"]["amount"])), + amount_out=AssetAmount(amount_out, int(swap["quote"]["amount_out"]["amount"])), + swap_fees=AssetAmount(asset_in, int(swap["quote"]["amount_in"]["amount"])), + slippage=0, + price_impact=swap["quote"]["price_impact"], + ) + else: + raise NotImplementedError() + route_pools_and_quotes.append((pool, quote)) - if smart_swap_route is not None: - routes.append(smart_swap_route) + return route_pools_and_quotes - if swap_type == FIXED_INPUT_SWAP_TYPE: - best_route = get_best_fixed_input_route(routes=routes, amount_in=amount) - elif swap_type == FIXED_OUTPUT_SWAP_TYPE: - best_route = get_best_fixed_output_route(routes=routes, amount_out=amount) - else: - raise NotImplementedError() - return best_route +# def fetch_best_route( +# tinyman_v1_client: TinymanClient, +# tinyman_v2_client: TinymanV2Client, +# asset_in_id: int, +# asset_out_id: int, +# swap_type: str, +# amount: int, +# ): +# asset_in = tinyman_v2_client.fetch_asset(asset_in_id) +# asset_out = tinyman_v2_client.fetch_asset(asset_out_id) +# routes = [] +# +# v1_pool = TinymanV1Pool( +# client=tinyman_v1_client, +# asset_a=asset_in, +# asset_b=asset_out, +# fetch=True, +# ) +# if v1_pool.exists: +# direct_v1_route = Route(asset_in=asset_in, asset_out=asset_out, pools=[v1_pool]) +# routes.append(direct_v1_route) +# +# v2_pool = TinymanV2Pool( +# client=tinyman_v2_client, +# asset_a=asset_in, +# asset_b=asset_out, +# fetch=True, +# ) +# if v2_pool.exists: +# direct_v2_route = Route(asset_in=asset_in, asset_out=asset_out, pools=[v2_pool]) +# routes.append(direct_v2_route) +# +# try: +# smart_route = fetch_swap_route( +# tinyman_v1_client=tinyman_v1_client, +# tinyman_v2_client=tinyman_v2_client, +# asset_in_id=asset_in_id, +# asset_out_id=asset_out_id, +# swap_type=swap_type, +# amount=amount, +# ) +# except Exception: # TODO: Handle the exception properly. +# smart_swap_route = None +# +# if smart_swap_route is not None: +# routes.append(smart_swap_route) +# +# if swap_type == FIXED_INPUT_SWAP_TYPE: +# best_route = get_best_fixed_input_route(routes=routes, amount_in=amount) +# elif swap_type == FIXED_OUTPUT_SWAP_TYPE: +# best_route = get_best_fixed_output_route(routes=routes, amount_out=amount) +# else: +# raise NotImplementedError() +# +# return best_route diff --git a/tinyman/swap_router/utils.py b/tinyman/swap_router/utils.py index d7753b8..7582e63 100644 --- a/tinyman/swap_router/utils.py +++ b/tinyman/swap_router/utils.py @@ -9,7 +9,7 @@ def get_best_fixed_input_route(routes, amount_in): for route in routes: try: quotes = route.get_fixed_input_quotes(amount_in=amount_in) - except InsufficientReserves: + except (InsufficientReserves, AssertionError): continue last_quote = quotes[-1] @@ -34,7 +34,7 @@ def get_best_fixed_output_route(routes, amount_out): for route in routes: try: quotes = route.get_fixed_output_quotes(amount_out=amount_out) - except InsufficientReserves: + except (InsufficientReserves, AssertionError): continue first_quote = quotes[0] diff --git a/tinyman/v1/pools.py b/tinyman/v1/pools.py index e50c225..a9fb95d 100644 --- a/tinyman/v1/pools.py +++ b/tinyman/v1/pools.py @@ -422,6 +422,8 @@ def fetch_fixed_output_swap_quote( input_supply = self.asset1_reserves output_supply = self.asset2_reserves + assert output_supply > amount_out.amount, "InsufficientReserves" + # k = input_supply * output_supply # ignoring fees, k must remain constant # (input_supply + asset_in) * (output_supply - amount_out) = k diff --git a/tinyman/v2/client.py b/tinyman/v2/client.py index 628afc0..6b2bd27 100644 --- a/tinyman/v2/client.py +++ b/tinyman/v2/client.py @@ -1,23 +1,32 @@ from typing import Optional from algosdk.v2client.algod import AlgodClient + from tinyman.client import BaseTinymanClient +from tinyman.errors import LogicError from tinyman.staking.constants import ( TESTNET_STAKING_APP_ID, MAINNET_STAKING_APP_ID, ) - +from tinyman.swap_router.constants import TESTNET_SWAP_ROUTER_APP_ID_V1, MAINNET_SWAP_ROUTER_APP_ID_V1 +from tinyman.utils import find_app_id_from_txn_id, parse_error from tinyman.v2.constants import ( TESTNET_VALIDATOR_APP_ID, MAINNET_VALIDATOR_APP_ID, ) - -from tinyman.utils import find_app_id_from_txn_id, parse_error -from .utils import lookup_error -from tinyman.errors import LogicError +from tinyman.v2.utils import lookup_error class TinymanV2Client(BaseTinymanClient): + + def __init__( + self, + *args, + **kwargs + ): + self.router_app_id = kwargs.pop("router_app_id", None) + super().__init__(*args, **kwargs) + def fetch_pool(self, asset_a, asset_b, fetch=True): from .pools import Pool @@ -43,6 +52,7 @@ def __init__( super().__init__( algod_client, validator_app_id=TESTNET_VALIDATOR_APP_ID, + router_app_id=TESTNET_SWAP_ROUTER_APP_ID_V1, api_base_url="https://testnet.analytics.tinyman.org/api/", user_address=user_address, staking_app_id=TESTNET_STAKING_APP_ID, @@ -60,6 +70,7 @@ def __init__( super().__init__( algod_client, validator_app_id=MAINNET_VALIDATOR_APP_ID, + router_app_id=MAINNET_SWAP_ROUTER_APP_ID_V1, api_base_url="https://mainnet.analytics.tinyman.org/api/", user_address=user_address, staking_app_id=MAINNET_STAKING_APP_ID, From 18e67e520a463e0d5e3236f7a68d71e5d4e2afa5 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Fri, 27 Jan 2023 17:28:54 +0300 Subject: [PATCH 05/32] add asset opt-in app call for swap router --- tinyman/swap_router/swap_router.py | 58 ++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/tinyman/swap_router/swap_router.py b/tinyman/swap_router/swap_router.py index bcb8a05..f3291f1 100644 --- a/tinyman/swap_router/swap_router.py +++ b/tinyman/swap_router/swap_router.py @@ -24,6 +24,28 @@ from tinyman.v2.quotes import SwapQuote as TinymanV2SwapQuote +def prepare_swap_router_asset_opt_in_transaction( + router_app_id: int, + asset_ids: [int], + user_address: str, + suggested_params: SuggestedParams, +) -> TransactionGroup: + + asset_opt_in_app_call = ApplicationNoOpTxn( + sender=user_address, + sp=suggested_params, + index=router_app_id, + app_args=["asset_opt_in"], + foreign_assets=asset_ids, + ) + min_fee = suggested_params.min_fee + inner_transaction_count = len(asset_ids) + asset_opt_in_app_call.fee = min_fee * (1 + inner_transaction_count) + + txn_group = TransactionGroup([asset_opt_in_app_call]) + return txn_group + + def prepare_swap_router_transactions( router_app_id: int, validator_app_id: int, @@ -36,7 +58,6 @@ def prepare_swap_router_transactions( user_address: str, suggested_params: SuggestedParams, ) -> TransactionGroup: - # TODO: MVP pool_1_logicsig = get_pool_logicsig( validator_app_id, input_asset_id, intermediary_asset_id ) @@ -82,9 +103,11 @@ def prepare_swap_router_transactions( min_fee = suggested_params.min_fee if swap_type == FIXED_INPUT_APP_ARGUMENT: - app_call_fee = min_fee * 8 + inner_transaction_count = 7 + app_call_fee = min_fee * (1 + inner_transaction_count) elif swap_type == FIXED_OUTPUT_APP_ARGUMENT: - app_call_fee = min_fee * 9 + inner_transaction_count = 8 + app_call_fee = min_fee * (1 + inner_transaction_count) else: raise NotImplementedError() @@ -129,12 +152,16 @@ def prepare_swap_router_transactions_from_quotes( router_app_id = pools[0].client.router_app_id validator_app_id = pools[0].client.validator_app_id - return prepare_swap_router_transactions( + input_asset_id = quotes[0].amount_in.asset.id + intermediary_asset_id = quotes[0].amount_out.asset.id + output_asset_id = quotes[-1].amount_out.asset.id + + txn_group = prepare_swap_router_transactions( router_app_id=router_app_id, validator_app_id=validator_app_id, - input_asset_id=quotes[0].amount_in.asset.id, - intermediary_asset_id=quotes[0].amount_out.asset.id, - output_asset_id=quotes[-1].amount_out.asset.id, + input_asset_id=input_asset_id, + intermediary_asset_id=intermediary_asset_id, + output_asset_id=output_asset_id, asset_in_amount=quotes[0].amount_in_with_slippage.amount, asset_out_amount=quotes[-1].amount_out_with_slippage.amount, swap_type=swap_type, @@ -142,6 +169,23 @@ def prepare_swap_router_transactions_from_quotes( suggested_params=suggested_params, ) + algod_client = pools[0].client.algod_client + swap_router_app_address = get_application_address(router_app_id) + account_info = algod_client.account_info(swap_router_app_address) + opted_in_asset_ids = {int(asset['asset-id']) for asset in account_info['assets']} + asset_ids = {input_asset_id, intermediary_asset_id, output_asset_id} - {0} - opted_in_asset_ids + + if asset_ids: + opt_in_txn_group = prepare_swap_router_asset_opt_in_transaction( + router_app_id=router_app_id, + asset_ids=list(asset_ids), + user_address=user_address, + suggested_params=suggested_params, + ) + txn_group = opt_in_txn_group + txn_group + + return txn_group + else: raise NotImplementedError() From 02ca45f60c7b4355385885545dfd07c18e89c586 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Fri, 27 Jan 2023 17:34:44 +0300 Subject: [PATCH 06/32] reformat the code according to linters --- examples/swap_router/swap_fixed_input.py | 9 ++-- tinyman/swap_router/swap_router.py | 56 +++++++++++++++++++----- tinyman/v2/client.py | 12 +++-- 3 files changed, 55 insertions(+), 22 deletions(-) diff --git a/examples/swap_router/swap_fixed_input.py b/examples/swap_router/swap_fixed_input.py index 3b1116b..927f09c 100644 --- a/examples/swap_router/swap_fixed_input.py +++ b/examples/swap_router/swap_fixed_input.py @@ -1,7 +1,10 @@ from algosdk.account import generate_account from algosdk.v2client.algod import AlgodClient -from tinyman.swap_router.swap_router import fetch_swap_route_quotes, prepare_swap_router_transactions_from_quotes +from tinyman.swap_router.swap_router import ( + fetch_swap_route_quotes, + prepare_swap_router_transactions_from_quotes, +) from tinyman.v1.client import TinymanTestnetClient from tinyman.v2.client import TinymanV2TestnetClient @@ -23,7 +26,7 @@ asset_in_id=ALGO_ASSET_ID, asset_out_id=USDC_ASSET_ID, swap_type=swap_type, - amount=amount + amount=amount, ) print(route_pools_and_quotes) @@ -31,7 +34,7 @@ route_pools_and_quotes=route_pools_and_quotes, swap_type=swap_type, user_address=address, - suggested_params=algod_client.suggested_params() + suggested_params=algod_client.suggested_params(), ) for txn in txn_group.transactions: diff --git a/tinyman/swap_router/swap_router.py b/tinyman/swap_router/swap_router.py index f3291f1..88c36c1 100644 --- a/tinyman/swap_router/swap_router.py +++ b/tinyman/swap_router/swap_router.py @@ -117,7 +117,12 @@ def prepare_swap_router_transactions( def prepare_swap_router_transactions_from_quotes( - route_pools_and_quotes: list[Union[tuple[TinymanV1Pool, TinymanV1SwapQuote], tuple[TinymanV2Pool, TinymanV2SwapQuote]]], + route_pools_and_quotes: list[ + Union[ + tuple[TinymanV1Pool, TinymanV1SwapQuote], + tuple[TinymanV2Pool, TinymanV2SwapQuote], + ] + ], swap_type: str, slippage: float = 0.05, user_address: str = None, @@ -172,8 +177,14 @@ def prepare_swap_router_transactions_from_quotes( algod_client = pools[0].client.algod_client swap_router_app_address = get_application_address(router_app_id) account_info = algod_client.account_info(swap_router_app_address) - opted_in_asset_ids = {int(asset['asset-id']) for asset in account_info['assets']} - asset_ids = {input_asset_id, intermediary_asset_id, output_asset_id} - {0} - opted_in_asset_ids + opted_in_asset_ids = { + int(asset["asset-id"]) for asset in account_info["assets"] + } + asset_ids = ( + {input_asset_id, intermediary_asset_id, output_asset_id} + - {0} + - opted_in_asset_ids + ) if asset_ids: opt_in_txn_group = prepare_swap_router_asset_opt_in_transaction( @@ -197,7 +208,12 @@ def fetch_swap_route_quotes( asset_out_id: int, swap_type: str, amount: int, -) -> list[Union[tuple[TinymanV1Pool, TinymanV1SwapQuote], tuple[TinymanV2Pool, TinymanV2SwapQuote]]]: +) -> list[ + Union[ + tuple[TinymanV1Pool, TinymanV1SwapQuote], + tuple[TinymanV2Pool, TinymanV2SwapQuote], + ] +]: assert swap_type in (FIXED_INPUT_SWAP_TYPE, FIXED_OUTPUT_SWAP_TYPE) assert amount > 0 assert asset_in_id >= 0 @@ -238,13 +254,21 @@ def fetch_swap_route_quotes( ) asset_in = client.fetch_asset(int(swap["quote"]["amount_in"]["asset_id"])) - amount_out = client.fetch_asset(int(swap["quote"]["amount_out"]["asset_id"])) + amount_out = client.fetch_asset( + int(swap["quote"]["amount_out"]["asset_id"]) + ) quote = TinymanV1SwapQuote( swap_type=swap_type, - amount_in=AssetAmount(asset_in, int(swap["quote"]["amount_in"]["amount"])), - amount_out=AssetAmount(amount_out, int(swap["quote"]["amount_out"]["amount"])), - swap_fees=AssetAmount(asset_in, int(swap["quote"]["amount_in"]["amount"])), + amount_in=AssetAmount( + asset_in, int(swap["quote"]["amount_in"]["amount"]) + ), + amount_out=AssetAmount( + amount_out, int(swap["quote"]["amount_out"]["amount"]) + ), + swap_fees=AssetAmount( + asset_in, int(swap["quote"]["amount_in"]["amount"]) + ), slippage=0, price_impact=swap["quote"]["price_impact"], ) @@ -260,13 +284,21 @@ def fetch_swap_route_quotes( ) asset_in = client.fetch_asset(int(swap["quote"]["amount_in"]["asset_id"])) - amount_out = client.fetch_asset(int(swap["quote"]["amount_out"]["asset_id"])) + amount_out = client.fetch_asset( + int(swap["quote"]["amount_out"]["asset_id"]) + ) quote = TinymanV2SwapQuote( swap_type=swap_type, - amount_in=AssetAmount(asset_in, int(swap["quote"]["amount_in"]["amount"])), - amount_out=AssetAmount(amount_out, int(swap["quote"]["amount_out"]["amount"])), - swap_fees=AssetAmount(asset_in, int(swap["quote"]["amount_in"]["amount"])), + amount_in=AssetAmount( + asset_in, int(swap["quote"]["amount_in"]["amount"]) + ), + amount_out=AssetAmount( + amount_out, int(swap["quote"]["amount_out"]["amount"]) + ), + swap_fees=AssetAmount( + asset_in, int(swap["quote"]["amount_in"]["amount"]) + ), slippage=0, price_impact=swap["quote"]["price_impact"], ) diff --git a/tinyman/v2/client.py b/tinyman/v2/client.py index 6b2bd27..60f1fe1 100644 --- a/tinyman/v2/client.py +++ b/tinyman/v2/client.py @@ -8,7 +8,10 @@ TESTNET_STAKING_APP_ID, MAINNET_STAKING_APP_ID, ) -from tinyman.swap_router.constants import TESTNET_SWAP_ROUTER_APP_ID_V1, MAINNET_SWAP_ROUTER_APP_ID_V1 +from tinyman.swap_router.constants import ( + TESTNET_SWAP_ROUTER_APP_ID_V1, + MAINNET_SWAP_ROUTER_APP_ID_V1, +) from tinyman.utils import find_app_id_from_txn_id, parse_error from tinyman.v2.constants import ( TESTNET_VALIDATOR_APP_ID, @@ -18,12 +21,7 @@ class TinymanV2Client(BaseTinymanClient): - - def __init__( - self, - *args, - **kwargs - ): + def __init__(self, *args, **kwargs): self.router_app_id = kwargs.pop("router_app_id", None) super().__init__(*args, **kwargs) From b6cc4ef40f495cf8eaf0eaab701b3fd65f6c854c Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Tue, 31 Jan 2023 13:32:52 +0300 Subject: [PATCH 07/32] add swap router tests --- tests/swap_router/__init__.py | 0 tests/swap_router/test.py | 794 +++++++++++++++++++++++++++++ tinyman/exceptions.py | 18 + tinyman/swap_router/routes.py | 10 +- tinyman/swap_router/swap_router.py | 6 +- tinyman/swap_router/utils.py | 11 +- tinyman/v1/pools.py | 28 +- tinyman/v2/exceptions.py | 27 +- tinyman/v2/formulas.py | 2 +- 9 files changed, 851 insertions(+), 45 deletions(-) create mode 100644 tests/swap_router/__init__.py create mode 100644 tests/swap_router/test.py create mode 100644 tinyman/exceptions.py diff --git a/tests/swap_router/__init__.py b/tests/swap_router/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/swap_router/test.py b/tests/swap_router/test.py new file mode 100644 index 0000000..01e2c30 --- /dev/null +++ b/tests/swap_router/test.py @@ -0,0 +1,794 @@ +from typing import Optional +from unittest import TestCase +from unittest.mock import ANY, patch + +from algosdk.account import generate_account +from algosdk.encoding import decode_address +from algosdk.logic import get_application_address +from algosdk.v2client.algod import AlgodClient + +from tests import get_suggested_params +from tinyman.assets import Asset, AssetAmount +from tinyman.compat import OnComplete +from tinyman.swap_router.routes import Route +from tinyman.swap_router.swap_router import ( + prepare_swap_router_asset_opt_in_transaction, + prepare_swap_router_transactions, + prepare_swap_router_transactions_from_quotes, +) +from tinyman.swap_router.utils import ( + get_best_fixed_input_route, + get_best_fixed_output_route, +) +from tinyman.v1.client import TinymanClient +from tinyman.v1.constants import TESTNET_VALIDATOR_APP_ID +from tinyman.v1.pools import Pool as V1Pool +from tinyman.v1.pools import SwapQuote as V1SwapQuote +from tinyman.v2.client import TinymanV2Client +from tinyman.v2.constants import TESTNET_VALIDATOR_APP_ID_V2 +from tinyman.v2.contracts import get_pool_logicsig as v2_get_pool_logicsig +from tinyman.v2.pools import Pool as V2Pool +from tinyman.v2.quotes import SwapQuote as V2SwapQuote + + +class BaseTestCase(TestCase): + maxDiff = None + + @classmethod + def setUpClass(cls): + cls.VALIDATOR_APP_ID_V1 = TESTNET_VALIDATOR_APP_ID + cls.VALIDATOR_APP_ID_V2 = TESTNET_VALIDATOR_APP_ID_V2 + cls.ROUTER_APP_ID = 987_654 + + @classmethod + def get_tinyman_client(cls, user_address=None): + return TinymanV2Client( + algod_client=AlgodClient("TEST", "https://test.test.network"), + validator_app_id=cls.VALIDATOR_APP_ID_V2, + user_address=user_address, + router_app_id=cls.ROUTER_APP_ID, + staking_app_id=None, + ) + + @classmethod + def get_tinyman_v1_client(cls, user_address=None): + return TinymanClient( + algod_client=AlgodClient("TEST", "https://test.test.network"), + validator_app_id=cls.VALIDATOR_APP_ID_V1, + user_address=user_address, + staking_app_id=None, + ) + + @classmethod + def get_suggested_params(cls): + return get_suggested_params() + + @classmethod + def get_pool_state( + cls, asset_1_id=None, asset_2_id=None, pool_token_asset_id=None, **kwargs + ): + state = { + "asset_1_cumulative_price": 0, + "lock": 0, + "cumulative_price_update_timestamp": 0, + "asset_2_cumulative_price": 0, + "asset_2_protocol_fees": 0, + "asset_1_reserves": 0, + "pool_token_asset_id": pool_token_asset_id, + "asset_1_protocol_fees": 0, + "asset_1_id": asset_1_id, + "asset_2_id": asset_2_id, + "issued_pool_tokens": 0, + "asset_2_reserves": 0, + "protocol_fee_ratio": 6, + "total_fee_share": 30, + } + state.update(**kwargs) + return state + + def setUp(self) -> None: + self.algo = Asset(id=0, name="Algorand", unit_name="Algo", decimals=6) + # Direct Route: Pool + self.asset_in = Asset(id=1, name="Asset In", unit_name="Asset In", decimals=6) + self.asset_out = Asset( + id=2, name="Asset Out", unit_name="Asset Out", decimals=10 + ) + pool_token_asset = Asset(id=3, name="TM", unit_name="TM", decimals=6) + + pool_state = self.get_pool_state( + asset_1_id=self.asset_in.id, + asset_2_id=self.asset_out.id, + pool_token_asset_id=pool_token_asset.id, + issued_pool_tokens=10_000, + ) + pool_logicsig = v2_get_pool_logicsig( + TESTNET_VALIDATOR_APP_ID_V2, self.asset_in.id, self.asset_out.id + ) + self.pool = V2Pool.from_state( + address=pool_logicsig.address(), + state=pool_state, + round_number=100, + client=self.get_tinyman_client(), + ) + + # Indirect Route: Pool 1 - Pool 2 + self.intermediary_asset = Asset(id=4, name="Int", unit_name="Int", decimals=10) + pool_1_token_asset = Asset(id=5, name="TM", unit_name="TM", decimals=6) + pool_1_state = self.get_pool_state( + asset_1_id=self.asset_in.id, + asset_2_id=self.intermediary_asset.id, + pool_token_asset_id=pool_1_token_asset.id, + issued_pool_tokens=10_000, + ) + pool_1_logicsig = v2_get_pool_logicsig( + TESTNET_VALIDATOR_APP_ID_V2, self.asset_in.id, self.intermediary_asset.id + ) + self.pool_1 = V2Pool.from_state( + address=pool_1_logicsig.address(), + state=pool_1_state, + round_number=100, + client=self.get_tinyman_client(), + ) + + pool_2_token_asset = Asset(id=6, name="TM", unit_name="TM", decimals=6) + pool_2_state = self.get_pool_state( + asset_1_id=self.intermediary_asset.id, + asset_2_id=self.asset_out.id, + pool_token_asset_id=pool_2_token_asset.id, + issued_pool_tokens=10_000, + ) + pool_2_logicsig = v2_get_pool_logicsig( + TESTNET_VALIDATOR_APP_ID_V2, self.intermediary_asset.id, self.asset_out.id + ) + self.pool_2 = V2Pool.from_state( + address=pool_2_logicsig.address(), + state=pool_2_state, + round_number=100, + client=self.get_tinyman_client(), + ) + + # Swap 1 -> 2: Price ~= 1/5 + # ID 2 + self.pool.asset_1_reserves = 1_000_000_000_000 + # ID 1 + self.pool.asset_2_reserves = 5_000_000_000_000 + + # Swap 1 -> 2: Price ~= 1/2 * 1/2 = 1/4 + # ID 4 + self.pool_1.asset_1_reserves = 1_000_000_000_000 + # ID 1 + self.pool_1.asset_2_reserves = 2_000_000_000_000 + + # ID 4 + self.pool_2.asset_1_reserves = 2_000_000_000_000 + # ID 2 + self.pool_2.asset_2_reserves = 1_000_000_000_000 + + self.direct_route = Route( + asset_in=self.asset_in, asset_out=self.asset_out, pools=[self.pool] + ) + self.indirect_route = Route( + asset_in=self.asset_in, + asset_out=self.asset_out, + pools=[self.pool_1, self.pool_2], + ) + + def get_mock_account_info( + self, + address: str, + assets: Optional[list] = None, + algo_balance: Optional[int] = None, + min_balance: Optional[int] = None, + ): + if assets is None: + assets = [] + + total_assets_opted_in = len(assets) + + if min_balance is None: + min_balance = 1_000 + min_balance += 100_000 * total_assets_opted_in + + if algo_balance is None: + algo_balance = min_balance + + return { + "address": address, + "amount": algo_balance, + "min-balance": algo_balance, + "total-assets-opted-in": total_assets_opted_in, + # [{'amount': 45682551121, 'asset-id': 10458941, 'is-frozen': False}] + "assets": assets, + } + + +class RouteTestCase(BaseTestCase): + def test_fixed_input_quotes(self): + amount_in = 10_000 + + quotes = self.direct_route.get_fixed_input_quotes( + amount_in=amount_in, slippage=0.05 + ) + self.assertEqual(len(quotes), 1) + self.assertEqual(quotes[0].amount_in.amount, amount_in) + self.assertEqual(quotes[0].amount_out.amount, 1993) + + quotes = self.indirect_route.get_fixed_input_quotes( + amount_in=amount_in, slippage=0.05 + ) + self.assertEqual(len(quotes), 2) + self.assertEqual(quotes[0].amount_in.amount, amount_in) + self.assertEqual(quotes[0].amount_out.amount, 4984) + self.assertEqual(quotes[1].amount_in.amount, 4984) + self.assertEqual(quotes[1].amount_out.amount, 2484) + + def test_fixed_output_quotes(self): + amount_out = 10_000 + + quotes = self.direct_route.get_fixed_output_quotes( + amount_out=amount_out, slippage=0.05 + ) + self.assertEqual(len(quotes), 1) + self.assertEqual(quotes[0].amount_in.amount, 50151) + self.assertEqual(quotes[0].amount_out.amount, amount_out) + + quotes = self.indirect_route.get_fixed_output_quotes( + amount_out=amount_out, slippage=0.05 + ) + self.assertEqual(len(quotes), 2) + self.assertEqual(quotes[0].amount_in.amount, 40243) + self.assertEqual(quotes[0].amount_out.amount, 20061) + self.assertEqual(quotes[1].amount_in.amount, 20061) + self.assertEqual(quotes[1].amount_out.amount, amount_out) + + def test_fixed_input_direct_best_route(self): + # Swap 1 -> 2: Price ~= 1/3 + # ID 2 + self.pool.asset_1_reserves = 1_000_000_000_000 + # ID 1 + self.pool.asset_2_reserves = 3_000_000_000_000 + + self.direct_route = Route( + asset_in=self.asset_in, asset_out=self.asset_out, pools=[self.pool] + ) + routes = [self.direct_route, self.indirect_route] + + amount_in = 10_000 + best_route = get_best_fixed_input_route(routes=routes, amount_in=amount_in) + self.assertEqual(best_route, self.direct_route) + + last_quote = best_route.get_fixed_input_last_quote( + amount_in=amount_in, slippage=0.05 + ) + self.assertEqual(last_quote.amount_out.amount, 3323) + + def test_fixed_input_indirect_best_route(self): + routes = [self.direct_route, self.indirect_route] + + amount_in = 10_000 + best_route = get_best_fixed_input_route(routes=routes, amount_in=amount_in) + self.assertEqual(best_route, self.indirect_route) + + last_quote = best_route.get_fixed_input_last_quote( + amount_in=amount_in, slippage=0.05 + ) + self.assertEqual(last_quote.amount_out.amount, 2484) + + def test_fixed_output_direct_best_route(self): + # Swap 1 -> 2: Price ~= 1/3 + # ID 2 + self.pool.asset_1_reserves = 1_000_000_000_000 + # ID 1 + self.pool.asset_2_reserves = 3_000_000_000_000 + + self.direct_route = Route( + asset_in=self.asset_in, asset_out=self.asset_out, pools=[self.pool] + ) + routes = [self.direct_route, self.indirect_route] + + amount_out = 10_000 + best_route = get_best_fixed_output_route(routes=routes, amount_out=amount_out) + self.assertEqual(best_route, self.direct_route) + + first_quote = best_route.get_fixed_output_first_quote( + amount_out=amount_out, slippage=0.05 + ) + self.assertEqual(first_quote.amount_in.amount, 30091) + self.assertEqual(first_quote.amount_out.amount, amount_out) + + def test_fixed_output_indirect_best_route(self): + routes = [self.direct_route, self.indirect_route] + + amount_out = 10_000 + best_route = get_best_fixed_output_route(routes=routes, amount_out=amount_out) + self.assertEqual(best_route, self.indirect_route) + + first_quote = best_route.get_fixed_output_first_quote( + amount_out=amount_out, slippage=0.05 + ) + self.assertEqual(first_quote.amount_in.amount, 40243) + + +class SwapRouterTransactionsTestCase(BaseTestCase): + def test_prepare_swap_router_asset_opt_in_transaction(self): + sp = self.get_suggested_params() + user_private_key, user_address = generate_account() + asset_ids = [1, 2, 3, 4] + + txn_group = prepare_swap_router_asset_opt_in_transaction( + router_app_id=self.ROUTER_APP_ID, + asset_ids=asset_ids, + user_address=user_address, + suggested_params=sp, + ) + self.assertEqual(len(txn_group.transactions), 1) + + self.assertDictEqual( + dict(txn_group.transactions[0].dictify()), + { + "apaa": [b"asset_opt_in"], + "apan": OnComplete.NoOpOC, + "apas": asset_ids, + "apid": self.ROUTER_APP_ID, + "fee": 1000 + len(asset_ids) * 1000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(user_address), + "type": "appl", + }, + ) + + def test_fixed_input_prepare_swap_router_transactions(self): + sp = self.get_suggested_params() + user_private_key, user_address = generate_account() + router_app_address = get_application_address(self.ROUTER_APP_ID) + asset_in_amount = 1_000_000 + min_output = 2_000_000 + swap_type = "fixed-input" + + txn_group = prepare_swap_router_transactions( + router_app_id=self.ROUTER_APP_ID, + validator_app_id=self.VALIDATOR_APP_ID_V2, + input_asset_id=self.asset_in.id, + intermediary_asset_id=self.intermediary_asset.id, + output_asset_id=self.asset_out.id, + asset_in_amount=asset_in_amount, + asset_out_amount=min_output, + swap_type=swap_type, + user_address=user_address, + suggested_params=sp, + ) + self.assertEqual(len(txn_group.transactions), 2) + self.assertDictEqual( + dict(txn_group.transactions[0].dictify()), + { + "aamt": 1000000, + "arcv": decode_address(router_app_address), + "fee": 1000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(user_address), + "type": "axfer", + "xaid": self.asset_in.id, + }, + ) + self.assertDictEqual( + dict(txn_group.transactions[1].dictify()), + { + "apaa": [b"swap", b"fixed-input", min_output.to_bytes(8, "big")], + "apan": OnComplete.NoOpOC, + "apas": [ + self.asset_in.id, + self.intermediary_asset.id, + self.asset_out.id, + ], + "apat": [ + decode_address(self.pool_1.address), + decode_address(self.pool_2.address), + ], + "apfa": [self.VALIDATOR_APP_ID_V2], + "apid": self.ROUTER_APP_ID, + "fee": 8000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(user_address), + "type": "appl", + }, + ) + + def test_output_input_prepare_swap_router_transactions(self): + sp = self.get_suggested_params() + user_private_key, user_address = generate_account() + router_app_id = 99999 + router_app_address = get_application_address(router_app_id) + asset_in_amount = 1_000_000 + asset_out_amount = 2_000_000 + swap_type = "fixed-output" + + txn_group = prepare_swap_router_transactions( + router_app_id=router_app_id, + validator_app_id=self.VALIDATOR_APP_ID_V2, + input_asset_id=self.asset_in.id, + intermediary_asset_id=self.intermediary_asset.id, + output_asset_id=self.asset_out.id, + asset_in_amount=asset_in_amount, + asset_out_amount=asset_out_amount, + swap_type=swap_type, + user_address=user_address, + suggested_params=sp, + ) + self.assertEqual(len(txn_group.transactions), 2) + self.assertDictEqual( + dict(txn_group.transactions[0].dictify()), + { + "aamt": 1000000, + "arcv": decode_address(router_app_address), + "fee": 1000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(user_address), + "type": "axfer", + "xaid": self.asset_in.id, + }, + ) + self.assertDictEqual( + dict(txn_group.transactions[1].dictify()), + { + "apaa": [b"swap", b"fixed-output", asset_out_amount.to_bytes(8, "big")], + "apan": OnComplete.NoOpOC, + "apas": [ + self.asset_in.id, + self.intermediary_asset.id, + self.asset_out.id, + ], + "apat": [ + decode_address(self.pool_1.address), + decode_address(self.pool_2.address), + ], + "apfa": [self.VALIDATOR_APP_ID_V2], + "apid": router_app_id, + "fee": 9000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(user_address), + "type": "appl", + }, + ) + + def test_algo_input_prepare_swap_router_transactions(self): + sp = self.get_suggested_params() + user_private_key, user_address = generate_account() + router_app_id = 99999 + router_app_address = get_application_address(router_app_id) + asset_in_amount = 1_000_000 + min_output = 2_000_000 + swap_type = "fixed-input" + + txn_group = prepare_swap_router_transactions( + router_app_id=router_app_id, + validator_app_id=self.VALIDATOR_APP_ID_V2, + input_asset_id=self.algo.id, + intermediary_asset_id=self.intermediary_asset.id, + output_asset_id=self.asset_out.id, + asset_in_amount=asset_in_amount, + asset_out_amount=min_output, + swap_type=swap_type, + user_address=user_address, + suggested_params=sp, + ) + self.assertEqual(len(txn_group.transactions), 2) + # Pay instead of axfer + self.assertDictEqual( + dict(txn_group.transactions[0].dictify()), + { + "amt": 1000000, + "rcv": decode_address(router_app_address), + "fee": 1000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(user_address), + "type": "pay", + }, + ) + self.assertEqual( + txn_group.transactions[1].dictify()["apas"], + [self.algo.id, self.intermediary_asset.id, self.asset_out.id], + ) + self.assertEqual(txn_group.transactions[1].dictify()["type"], "appl") + + def test_indirect_route_prepare_swap_router_transactions_from_quotes(self): + sp = self.get_suggested_params() + user_private_key, user_address = generate_account() + router_app_address = get_application_address(self.ROUTER_APP_ID) + asset_in = AssetAmount(self.asset_in, 1_000_000) + asset_out = AssetAmount(self.asset_out, 2_000_000) + asset_intermediary = AssetAmount(self.intermediary_asset, 9_999_999) + swap_type = "fixed-input" + + quote_1 = V2SwapQuote( + swap_type=swap_type, + amount_in=asset_in, + amount_out=asset_intermediary, + swap_fees=None, + slippage=None, + price_impact=None, + ) + quote_2 = V2SwapQuote( + swap_type=swap_type, + amount_in=asset_intermediary, + amount_out=asset_out, + swap_fees=None, + slippage=None, + price_impact=None, + ) + + route_pools_and_quotes = [(self.pool_1, quote_1), (self.pool_2, quote_2)] + + opt_in_app_call_txn = { + "apaa": [b"asset_opt_in"], + "apan": OnComplete.NoOpOC, + "apas": ANY, + "apid": self.ROUTER_APP_ID, + "fee": ANY, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(user_address), + "type": "appl", + } + transfer_input_txn = { + "aamt": asset_in.amount, + "arcv": b"\xd4\xb4\xce\xaa\xc35V\xffg\xfa\xae\xcbz\xd0\x8a\xb3\x8f\x85\x1a\x9e\x06b\x8a\xf4X:\x0b\xae[\x93i\xde", + "fee": ANY, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(user_address), + "type": "axfer", + "xaid": self.asset_in.id, + } + swap_app_call_txn = { + "apaa": [b"swap", b"fixed-input", asset_out.amount.to_bytes(8, "big")], + "apan": OnComplete.NoOpOC, + "apas": [self.asset_in.id, self.intermediary_asset.id, self.asset_out.id], + "apat": [ + decode_address(self.pool_1.address), + decode_address(self.pool_2.address), + ], + "apfa": [self.VALIDATOR_APP_ID_V2], + "apid": self.ROUTER_APP_ID, + "fee": ANY, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(user_address), + "type": "appl", + } + + # 3 Opt-in + account_info = self.get_mock_account_info( + address=router_app_address, + assets=[], + ) + with patch( + "algosdk.v2client.algod.AlgodClient.account_info", return_value=account_info + ): + txn_group = prepare_swap_router_transactions_from_quotes( + route_pools_and_quotes=route_pools_and_quotes, + swap_type=swap_type, + user_address=user_address, + suggested_params=sp, + slippage=0, + ) + self.assertEqual(len(txn_group.transactions), 3) + opt_in_app_call_txn["apas"] = [ + self.asset_in.id, + self.asset_out.id, + self.intermediary_asset.id, + ] + opt_in_app_call_txn["fee"] = 4 * 1000 + self.assertDictEqual( + dict(txn_group.transactions[0].dictify()), opt_in_app_call_txn + ) + self.assertDictEqual( + dict(txn_group.transactions[1].dictify()), transfer_input_txn + ) + self.assertDictEqual( + dict(txn_group.transactions[2].dictify()), swap_app_call_txn + ) + + # 2 Opt-in + account_info = self.get_mock_account_info( + address=router_app_address, + assets=[ + {"amount": 0, "asset-id": self.asset_in.id, "is-frozen": False}, + ], + ) + with patch( + "algosdk.v2client.algod.AlgodClient.account_info", return_value=account_info + ): + txn_group = prepare_swap_router_transactions_from_quotes( + route_pools_and_quotes=route_pools_and_quotes, + swap_type=swap_type, + user_address=user_address, + suggested_params=sp, + slippage=0, + ) + self.assertEqual(len(txn_group.transactions), 3) + opt_in_app_call_txn["apas"] = [ + self.asset_out.id, + self.intermediary_asset.id, + ] + opt_in_app_call_txn["fee"] = 3 * 1000 + self.assertDictEqual( + dict(txn_group.transactions[0].dictify()), opt_in_app_call_txn + ) + self.assertDictEqual( + dict(txn_group.transactions[1].dictify()), transfer_input_txn + ) + self.assertDictEqual( + dict(txn_group.transactions[2].dictify()), swap_app_call_txn + ) + + # No opt-in + Slippage + account_info = self.get_mock_account_info( + address=router_app_address, + assets=[ + {"amount": 0, "asset-id": self.asset_in.id, "is-frozen": False}, + { + "amount": 0, + "asset-id": self.intermediary_asset.id, + "is-frozen": False, + }, + {"amount": 0, "asset-id": self.asset_out.id, "is-frozen": False}, + ], + ) + with patch( + "algosdk.v2client.algod.AlgodClient.account_info", return_value=account_info + ): + txn_group = prepare_swap_router_transactions_from_quotes( + route_pools_and_quotes=route_pools_and_quotes, + swap_type=swap_type, + user_address=user_address, + suggested_params=sp, + slippage=0.05, + ) + self.assertEqual(len(txn_group.transactions), 2) + self.assertDictEqual( + dict(txn_group.transactions[0].dictify()), transfer_input_txn + ) + quote_2.slippage = 0.05 + swap_app_call_txn["apaa"] = [ + b"swap", + b"fixed-input", + quote_2.amount_out_with_slippage.amount.to_bytes(8, "big"), + ] + self.assertDictEqual( + dict(txn_group.transactions[1].dictify()), swap_app_call_txn + ) + + def test_direct_route_prepare_swap_router_transactions_from_quotes(self): + sp = self.get_suggested_params() + user_private_key, user_address = generate_account() + asset_in = AssetAmount(self.asset_in, 1_000_000) + asset_out = AssetAmount(self.asset_out, 2_000_000) + swap_type = "fixed-input" + + quote = V2SwapQuote( + swap_type=swap_type, + amount_in=asset_in, + amount_out=asset_out, + swap_fees=None, + slippage=0.05, + price_impact=None, + ) + route_pools_and_quotes = [ + (self.pool, quote), + ] + + txn_group = prepare_swap_router_transactions_from_quotes( + route_pools_and_quotes=route_pools_and_quotes, + swap_type=swap_type, + user_address=user_address, + suggested_params=sp, + slippage=0, + ) + self.assertEqual(len(txn_group.transactions), 2) + self.assertDictEqual( + dict(txn_group.transactions[0].dictify()), + { + "aamt": asset_in.amount, + "arcv": decode_address(self.pool.address), + "fee": ANY, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(user_address), + "type": "axfer", + "xaid": self.asset_in.id, + }, + ) + self.assertDictEqual( + dict(txn_group.transactions[1].dictify()), + { + "apaa": [ + b"swap", + b"fixed-input", + quote.amount_out_with_slippage.amount.to_bytes(8, "big"), + ], + "apan": OnComplete.NoOpOC, + "apas": [self.asset_out.id, self.asset_in.id], + "apat": [decode_address(self.pool.address)], + "apid": self.VALIDATOR_APP_ID_V2, + "fee": ANY, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "note": ANY, + "snd": decode_address(user_address), + "type": "appl", + }, + ) + + def test_v1_direct_route_prepare_swap_router_transactions_from_quotes(self): + sp = self.get_suggested_params() + user_private_key, user_address = generate_account() + asset_in = AssetAmount(self.asset_in, 1_000_000) + asset_out = AssetAmount(self.asset_out, 2_000_000) + swap_type = "fixed-input" + + v1_pool = V1Pool( + client=self.get_tinyman_v1_client(), + asset_a=self.asset_in, + asset_b=self.asset_out, + validator_app_id=self.VALIDATOR_APP_ID_V1, + fetch=False, + ) + v1_pool.exists = True + v1_pool.liquidity_asset = Asset(id=99, name="TM", unit_name="TM", decimals=6) + + quote = V1SwapQuote( + swap_type=swap_type, + amount_in=asset_in, + amount_out=asset_out, + swap_fees=None, + slippage=0.05, + price_impact=None, + ) + route_pools_and_quotes = [ + (v1_pool, quote), + ] + + with patch( + "algosdk.v2client.algod.AlgodClient.suggested_params", + return_value=self.get_suggested_params(), + ): + txn_group = prepare_swap_router_transactions_from_quotes( + route_pools_and_quotes=route_pools_and_quotes, + swap_type=swap_type, + user_address=user_address, + suggested_params=sp, + slippage=0, + ) + + self.assertEqual(len(txn_group.transactions), 4) + self.assertEqual( + dict(txn_group.transactions[1].dictify())["apid"], self.VALIDATOR_APP_ID_V1 + ) diff --git a/tinyman/exceptions.py b/tinyman/exceptions.py new file mode 100644 index 0000000..8825ab4 --- /dev/null +++ b/tinyman/exceptions.py @@ -0,0 +1,18 @@ +class PoolIsNotBootstrapped(Exception): + pass + + +class PoolAlreadyBootstrapped(Exception): + pass + + +class PoolHasNoLiquidity(Exception): + pass + + +class PoolAlreadyInitialized(Exception): + pass + + +class InsufficientReserves(Exception): + pass diff --git a/tinyman/swap_router/routes.py b/tinyman/swap_router/routes.py index dc18965..558ad08 100644 --- a/tinyman/swap_router/routes.py +++ b/tinyman/swap_router/routes.py @@ -2,8 +2,8 @@ from typing import Union from tinyman.assets import Asset, AssetAmount +from tinyman.exceptions import PoolHasNoLiquidity, InsufficientReserves from tinyman.v1.pools import Pool as TinymanV1Pool -from tinyman.v2.exceptions import InsufficientReserves from tinyman.v2.pools import Pool as TinymanV2Pool @@ -11,12 +11,11 @@ class Route: asset_in: Asset asset_out: Asset - pools: list[Union[TinymanV1Pool, TinymanV2Pool]] + pools: Union[list[TinymanV1Pool], list[TinymanV2Pool]] def __str__(self): return "Route: " + "-> ".join(f"{pool}" for pool in self.pools) - # Fixed-Input def get_fixed_input_quotes(self, amount_in: int, slippage: float = 0.05): quotes = [] assert self.pools @@ -40,13 +39,12 @@ def get_fixed_input_quotes(self, amount_in: int, slippage: float = 0.05): def get_fixed_input_last_quote(self, amount_in: int, slippage: float = 0.05): try: quotes = self.get_fixed_input_quotes(amount_in=amount_in, slippage=slippage) - except InsufficientReserves: + except (InsufficientReserves, PoolHasNoLiquidity): return None last_quote = quotes[-1] return last_quote - # Fixed-Output def get_fixed_output_quotes(self, amount_out: int, slippage: float = 0.05): quotes = [] assert self.pools @@ -73,7 +71,7 @@ def get_fixed_output_first_quote(self, amount_out: int, slippage: float = 0.05): quotes = self.get_fixed_output_quotes( amount_out=amount_out, slippage=slippage ) - except InsufficientReserves: + except (InsufficientReserves, PoolHasNoLiquidity): return None first_quote = quotes[0] diff --git a/tinyman/swap_router/swap_router.py b/tinyman/swap_router/swap_router.py index 88c36c1..1d3429c 100644 --- a/tinyman/swap_router/swap_router.py +++ b/tinyman/swap_router/swap_router.py @@ -1,6 +1,6 @@ from typing import Union -from algosdk.future.transaction import ( +from tinyman.compat import ( AssetTransferTxn, ApplicationNoOpTxn, SuggestedParams, @@ -174,9 +174,9 @@ def prepare_swap_router_transactions_from_quotes( suggested_params=suggested_params, ) - algod_client = pools[0].client.algod_client + algod = pools[0].client.algod swap_router_app_address = get_application_address(router_app_id) - account_info = algod_client.account_info(swap_router_app_address) + account_info = algod.account_info(swap_router_app_address) opted_in_asset_ids = { int(asset["asset-id"]) for asset in account_info["assets"] } diff --git a/tinyman/swap_router/utils.py b/tinyman/swap_router/utils.py index 7582e63..a28ad43 100644 --- a/tinyman/swap_router/utils.py +++ b/tinyman/swap_router/utils.py @@ -1,7 +1,8 @@ -from tinyman.v2.exceptions import InsufficientReserves +from tinyman.swap_router.routes import Route +from tinyman.exceptions import InsufficientReserves, PoolHasNoLiquidity -def get_best_fixed_input_route(routes, amount_in): +def get_best_fixed_input_route(routes: list[Route], amount_in: int): best_route = None best_route_max_price_impact = None best_route_amount_out = None @@ -9,7 +10,7 @@ def get_best_fixed_input_route(routes, amount_in): for route in routes: try: quotes = route.get_fixed_input_quotes(amount_in=amount_in) - except (InsufficientReserves, AssertionError): + except (InsufficientReserves, PoolHasNoLiquidity): continue last_quote = quotes[-1] @@ -26,7 +27,7 @@ def get_best_fixed_input_route(routes, amount_in): return best_route -def get_best_fixed_output_route(routes, amount_out): +def get_best_fixed_output_route(routes: list[Route], amount_out: int): best_route = None best_route_max_price_impact = None best_route_amount_in = None @@ -34,7 +35,7 @@ def get_best_fixed_output_route(routes, amount_out): for route in routes: try: quotes = route.get_fixed_output_quotes(amount_out=amount_out) - except (InsufficientReserves, AssertionError): + except (InsufficientReserves, PoolHasNoLiquidity): continue first_quote = quotes[0] diff --git a/tinyman/v1/pools.py b/tinyman/v1/pools.py index 49b33fb..ba1f602 100644 --- a/tinyman/v1/pools.py +++ b/tinyman/v1/pools.py @@ -1,19 +1,22 @@ import math -from dataclasses import dataclass from base64 import b64encode -from algosdk.v2client.algod import AlgodClient +from dataclasses import dataclass + from algosdk.encoding import decode_address -from .contracts import get_pool_logicsig -from tinyman.utils import get_state_int, calculate_price_impact +from algosdk.v2client.algod import AlgodClient + from tinyman.assets import Asset, AssetAmount -from .swap import prepare_swap_transactions -from .bootstrap import prepare_bootstrap_transactions -from .mint import prepare_mint_transactions -from .burn import prepare_burn_transactions -from .redeem import prepare_redeem_transactions +from tinyman.exceptions import InsufficientReserves from tinyman.optin import prepare_asset_optin_transactions -from .fees import prepare_redeem_fees_transactions -from .client import TinymanClient +from tinyman.utils import get_state_int, calculate_price_impact +from tinyman.v1.bootstrap import prepare_bootstrap_transactions +from tinyman.v1.burn import prepare_burn_transactions +from tinyman.v1.client import TinymanClient +from tinyman.v1.fees import prepare_redeem_fees_transactions +from tinyman.v1.mint import prepare_mint_transactions +from tinyman.v1.redeem import prepare_redeem_transactions +from tinyman.v1.swap import prepare_swap_transactions +from .contracts import get_pool_logicsig def get_pool_info(client: AlgodClient, validator_app_id, asset1_id, asset2_id): @@ -414,7 +417,8 @@ def fetch_fixed_output_swap_quote( input_supply = self.asset1_reserves output_supply = self.asset2_reserves - assert output_supply > amount_out.amount, "InsufficientReserves" + if output_supply <= amount_out.amount: + raise InsufficientReserves() # k = input_supply * output_supply # ignoring fees, k must remain constant diff --git a/tinyman/v2/exceptions.py b/tinyman/v2/exceptions.py index 8825ab4..0ee2ad6 100644 --- a/tinyman/v2/exceptions.py +++ b/tinyman/v2/exceptions.py @@ -1,18 +1,9 @@ -class PoolIsNotBootstrapped(Exception): - pass - - -class PoolAlreadyBootstrapped(Exception): - pass - - -class PoolHasNoLiquidity(Exception): - pass - - -class PoolAlreadyInitialized(Exception): - pass - - -class InsufficientReserves(Exception): - pass +# flake8: noqa, for backward compatibility. + +from tinyman.exceptions import ( + PoolIsNotBootstrapped, + PoolAlreadyBootstrapped, + PoolHasNoLiquidity, + PoolAlreadyInitialized, + InsufficientReserves, +) diff --git a/tinyman/v2/formulas.py b/tinyman/v2/formulas.py index 258c9df..9b10f76 100644 --- a/tinyman/v2/formulas.py +++ b/tinyman/v2/formulas.py @@ -2,7 +2,7 @@ from tinyman.utils import calculate_price_impact from tinyman.v2.constants import LOCKED_POOL_TOKENS -from tinyman.v2.exceptions import InsufficientReserves +from tinyman.exceptions import InsufficientReserves def calculate_protocol_fee_amount( From 4aca603e4c5c160a3df0ab950e867f5e1d71fe83 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Mon, 13 Feb 2023 13:48:42 +0300 Subject: [PATCH 08/32] refactor swap router --- examples/swap_router/swap_fixed_input.py | 42 ---- tests/swap_router/test.py | 281 +++++++++++++---------- tinyman/swap_router/constants.py | 2 +- tinyman/swap_router/routes.py | 149 +++++++++++- tinyman/swap_router/swap_router.py | 275 ++++------------------ tinyman/swap_router/utils.py | 52 ----- tinyman/utils.py | 2 +- tinyman/v1/pools.py | 4 +- tinyman/v2/client.py | 6 +- 9 files changed, 357 insertions(+), 456 deletions(-) delete mode 100644 examples/swap_router/swap_fixed_input.py delete mode 100644 tinyman/swap_router/utils.py diff --git a/examples/swap_router/swap_fixed_input.py b/examples/swap_router/swap_fixed_input.py deleted file mode 100644 index 927f09c..0000000 --- a/examples/swap_router/swap_fixed_input.py +++ /dev/null @@ -1,42 +0,0 @@ -from algosdk.account import generate_account -from algosdk.v2client.algod import AlgodClient - -from tinyman.swap_router.swap_router import ( - fetch_swap_route_quotes, - prepare_swap_router_transactions_from_quotes, -) -from tinyman.v1.client import TinymanTestnetClient -from tinyman.v2.client import TinymanV2TestnetClient - -ALGO_ASSET_ID = 0 -USDC_ASSET_ID = 10458941 -private_key, address = generate_account() - -algod_client = AlgodClient("", "https://testnet-api.algonode.network") - -tinyman_v1_client = TinymanTestnetClient(algod_client=algod_client) -tinyman_v2_client = TinymanV2TestnetClient(algod_client=algod_client) - -swap_type = "fixed-input" -amount = 1_000_000 - -route_pools_and_quotes = fetch_swap_route_quotes( - tinyman_v1_client=tinyman_v1_client, - tinyman_v2_client=tinyman_v2_client, - asset_in_id=ALGO_ASSET_ID, - asset_out_id=USDC_ASSET_ID, - swap_type=swap_type, - amount=amount, -) -print(route_pools_and_quotes) - -txn_group = prepare_swap_router_transactions_from_quotes( - route_pools_and_quotes=route_pools_and_quotes, - swap_type=swap_type, - user_address=address, - suggested_params=algod_client.suggested_params(), -) - -for txn in txn_group.transactions: - print() - print(txn.dictify()) diff --git a/tests/swap_router/test.py b/tests/swap_router/test.py index 01e2c30..f3ce23d 100644 --- a/tests/swap_router/test.py +++ b/tests/swap_router/test.py @@ -10,16 +10,15 @@ from tests import get_suggested_params from tinyman.assets import Asset, AssetAmount from tinyman.compat import OnComplete -from tinyman.swap_router.routes import Route -from tinyman.swap_router.swap_router import ( - prepare_swap_router_asset_opt_in_transaction, - prepare_swap_router_transactions, - prepare_swap_router_transactions_from_quotes, -) -from tinyman.swap_router.utils import ( +from tinyman.swap_router.routes import ( + Route, get_best_fixed_input_route, get_best_fixed_output_route, ) +from tinyman.swap_router.swap_router import ( + prepare_swap_router_asset_opt_in_transaction, + prepare_swap_router_transactions, get_swap_router_app_opt_in_required_asset_ids, +) from tinyman.v1.client import TinymanClient from tinyman.v1.constants import TESTNET_VALIDATOR_APP_ID from tinyman.v1.pools import Pool as V1Pool @@ -508,7 +507,7 @@ def test_algo_input_prepare_swap_router_transactions(self): ) self.assertEqual(txn_group.transactions[1].dictify()["type"], "appl") - def test_indirect_route_prepare_swap_router_transactions_from_quotes(self): + def test_indirect_route_prepare_swap_transactions_from_quotes(self): sp = self.get_suggested_params() user_private_key, user_address = generate_account() router_app_address = get_application_address(self.ROUTER_APP_ID) @@ -517,12 +516,18 @@ def test_indirect_route_prepare_swap_router_transactions_from_quotes(self): asset_intermediary = AssetAmount(self.intermediary_asset, 9_999_999) swap_type = "fixed-input" + route = Route( + asset_in=self.asset_in, + asset_out=self.asset_out, + pools=[self.pool_1, self.pool_2] + ) + quote_1 = V2SwapQuote( swap_type=swap_type, amount_in=asset_in, amount_out=asset_intermediary, swap_fees=None, - slippage=None, + slippage=0, price_impact=None, ) quote_2 = V2SwapQuote( @@ -530,11 +535,10 @@ def test_indirect_route_prepare_swap_router_transactions_from_quotes(self): amount_in=asset_intermediary, amount_out=asset_out, swap_fees=None, - slippage=None, + slippage=0, price_impact=None, ) - - route_pools_and_quotes = [(self.pool_1, quote_1), (self.pool_2, quote_2)] + quotes = [quote_1, quote_2] opt_in_app_call_txn = { "apaa": [b"asset_opt_in"], @@ -580,115 +584,32 @@ def test_indirect_route_prepare_swap_router_transactions_from_quotes(self): "type": "appl", } - # 3 Opt-in - account_info = self.get_mock_account_info( - address=router_app_address, - assets=[], + txn_group = route.prepare_swap_transactions_from_quotes( + quotes=quotes, + user_address=user_address, + suggested_params=sp, ) - with patch( - "algosdk.v2client.algod.AlgodClient.account_info", return_value=account_info - ): - txn_group = prepare_swap_router_transactions_from_quotes( - route_pools_and_quotes=route_pools_and_quotes, - swap_type=swap_type, - user_address=user_address, - suggested_params=sp, - slippage=0, - ) - self.assertEqual(len(txn_group.transactions), 3) - opt_in_app_call_txn["apas"] = [ - self.asset_in.id, - self.asset_out.id, - self.intermediary_asset.id, - ] - opt_in_app_call_txn["fee"] = 4 * 1000 - self.assertDictEqual( - dict(txn_group.transactions[0].dictify()), opt_in_app_call_txn - ) - self.assertDictEqual( - dict(txn_group.transactions[1].dictify()), transfer_input_txn - ) - self.assertDictEqual( - dict(txn_group.transactions[2].dictify()), swap_app_call_txn - ) - - # 2 Opt-in - account_info = self.get_mock_account_info( - address=router_app_address, - assets=[ - {"amount": 0, "asset-id": self.asset_in.id, "is-frozen": False}, - ], + self.assertEqual(len(txn_group.transactions), 2) + self.assertDictEqual( + dict(txn_group.transactions[0].dictify()), transfer_input_txn ) - with patch( - "algosdk.v2client.algod.AlgodClient.account_info", return_value=account_info - ): - txn_group = prepare_swap_router_transactions_from_quotes( - route_pools_and_quotes=route_pools_and_quotes, - swap_type=swap_type, - user_address=user_address, - suggested_params=sp, - slippage=0, - ) - self.assertEqual(len(txn_group.transactions), 3) - opt_in_app_call_txn["apas"] = [ - self.asset_out.id, - self.intermediary_asset.id, - ] - opt_in_app_call_txn["fee"] = 3 * 1000 - self.assertDictEqual( - dict(txn_group.transactions[0].dictify()), opt_in_app_call_txn - ) - self.assertDictEqual( - dict(txn_group.transactions[1].dictify()), transfer_input_txn - ) - self.assertDictEqual( - dict(txn_group.transactions[2].dictify()), swap_app_call_txn - ) - - # No opt-in + Slippage - account_info = self.get_mock_account_info( - address=router_app_address, - assets=[ - {"amount": 0, "asset-id": self.asset_in.id, "is-frozen": False}, - { - "amount": 0, - "asset-id": self.intermediary_asset.id, - "is-frozen": False, - }, - {"amount": 0, "asset-id": self.asset_out.id, "is-frozen": False}, - ], + self.assertDictEqual( + dict(txn_group.transactions[1].dictify()), swap_app_call_txn ) - with patch( - "algosdk.v2client.algod.AlgodClient.account_info", return_value=account_info - ): - txn_group = prepare_swap_router_transactions_from_quotes( - route_pools_and_quotes=route_pools_and_quotes, - swap_type=swap_type, - user_address=user_address, - suggested_params=sp, - slippage=0.05, - ) - self.assertEqual(len(txn_group.transactions), 2) - self.assertDictEqual( - dict(txn_group.transactions[0].dictify()), transfer_input_txn - ) - quote_2.slippage = 0.05 - swap_app_call_txn["apaa"] = [ - b"swap", - b"fixed-input", - quote_2.amount_out_with_slippage.amount.to_bytes(8, "big"), - ] - self.assertDictEqual( - dict(txn_group.transactions[1].dictify()), swap_app_call_txn - ) - def test_direct_route_prepare_swap_router_transactions_from_quotes(self): + def test_direct_route_prepare_swap_transactions_from_quotes(self): sp = self.get_suggested_params() user_private_key, user_address = generate_account() asset_in = AssetAmount(self.asset_in, 1_000_000) asset_out = AssetAmount(self.asset_out, 2_000_000) swap_type = "fixed-input" + route = Route( + asset_in=self.asset_in, + asset_out=self.asset_out, + pools=[self.pool] + ) + quote = V2SwapQuote( swap_type=swap_type, amount_in=asset_in, @@ -697,16 +618,12 @@ def test_direct_route_prepare_swap_router_transactions_from_quotes(self): slippage=0.05, price_impact=None, ) - route_pools_and_quotes = [ - (self.pool, quote), - ] + quotes = [quote] - txn_group = prepare_swap_router_transactions_from_quotes( - route_pools_and_quotes=route_pools_and_quotes, - swap_type=swap_type, + txn_group = route.prepare_swap_transactions_from_quotes( + quotes=quotes, user_address=user_address, suggested_params=sp, - slippage=0, ) self.assertEqual(len(txn_group.transactions), 2) self.assertDictEqual( @@ -747,7 +664,7 @@ def test_direct_route_prepare_swap_router_transactions_from_quotes(self): }, ) - def test_v1_direct_route_prepare_swap_router_transactions_from_quotes(self): + def test_v1_direct_route_prepare_swap_transactions_from_quotes(self): sp = self.get_suggested_params() user_private_key, user_address = generate_account() asset_in = AssetAmount(self.asset_in, 1_000_000) @@ -764,6 +681,12 @@ def test_v1_direct_route_prepare_swap_router_transactions_from_quotes(self): v1_pool.exists = True v1_pool.liquidity_asset = Asset(id=99, name="TM", unit_name="TM", decimals=6) + route = Route( + asset_in=self.asset_in, + asset_out=self.asset_out, + pools=[v1_pool] + ) + quote = V1SwapQuote( swap_type=swap_type, amount_in=asset_in, @@ -772,23 +695,129 @@ def test_v1_direct_route_prepare_swap_router_transactions_from_quotes(self): slippage=0.05, price_impact=None, ) - route_pools_and_quotes = [ - (v1_pool, quote), - ] + quotes = [quote] with patch( "algosdk.v2client.algod.AlgodClient.suggested_params", return_value=self.get_suggested_params(), ): - txn_group = prepare_swap_router_transactions_from_quotes( - route_pools_and_quotes=route_pools_and_quotes, - swap_type=swap_type, + txn_group = route.prepare_swap_transactions_from_quotes( + quotes=quotes, user_address=user_address, suggested_params=sp, - slippage=0, ) self.assertEqual(len(txn_group.transactions), 4) self.assertEqual( dict(txn_group.transactions[1].dictify())["apid"], self.VALIDATOR_APP_ID_V1 ) + + def test_swap_route_opt_in(self): + sp = self.get_suggested_params() + user_private_key, user_address = generate_account() + router_app_address = get_application_address(self.ROUTER_APP_ID) + algod = AlgodClient("TEST", "https://test.test.network") + + route = Route( + asset_in=self.asset_in, + asset_out=self.asset_out, + pools=[self.pool_1, self.pool_2] + ) + opt_in_app_call_txn = { + "apaa": [b"asset_opt_in"], + "apan": OnComplete.NoOpOC, + "apas": ANY, + "apid": self.ROUTER_APP_ID, + "fee": ANY, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(user_address), + "type": "appl", + } + + # 3 Opt-in + account_info = self.get_mock_account_info( + address=router_app_address, + assets=[], + ) + with patch( + "algosdk.v2client.algod.AlgodClient.account_info", return_value=account_info + ): + opt_in_required_asset_ids = get_swap_router_app_opt_in_required_asset_ids( + algod_client=algod, + router_app_id=self.ROUTER_APP_ID, + asset_ids=route.asset_ids, + ) + self.assertEqual(opt_in_required_asset_ids, opt_in_app_call_txn["apas"]) + + opt_in_txn_group = prepare_swap_router_asset_opt_in_transaction( + router_app_id=self.ROUTER_APP_ID, + asset_ids=opt_in_required_asset_ids, + user_address=user_address, + suggested_params=sp + ) + + self.assertEqual(len(opt_in_txn_group.transactions), 1) + self.assertDictEqual( + dict(opt_in_txn_group.transactions[0].dictify()), opt_in_app_call_txn + ) + + # 2 Opt-in + account_info = self.get_mock_account_info( + address=router_app_address, + assets=[ + {"amount": 0, "asset-id": self.asset_in.id, "is-frozen": False}, + ], + ) + with patch( + "algosdk.v2client.algod.AlgodClient.account_info", return_value=account_info + ): + opt_in_app_call_txn["apas"] = [ + self.asset_out.id, + self.intermediary_asset.id, + ] + opt_in_app_call_txn["fee"] = 3 * 1000 + + opt_in_required_asset_ids = get_swap_router_app_opt_in_required_asset_ids( + algod_client=algod, + router_app_id=self.ROUTER_APP_ID, + asset_ids=route.asset_ids, + ) + self.assertEqual(opt_in_required_asset_ids, opt_in_app_call_txn["apas"]) + + opt_in_txn_group = prepare_swap_router_asset_opt_in_transaction( + router_app_id=self.ROUTER_APP_ID, + asset_ids=opt_in_required_asset_ids, + user_address=user_address, + suggested_params=sp + ) + + self.assertEqual(len(opt_in_txn_group.transactions), 1) + self.assertDictEqual( + dict(opt_in_txn_group.transactions[0].dictify()), opt_in_app_call_txn + ) + + # No opt-in + Slippage + account_info = self.get_mock_account_info( + address=router_app_address, + assets=[ + {"amount": 0, "asset-id": self.asset_in.id, "is-frozen": False}, + { + "amount": 0, + "asset-id": self.intermediary_asset.id, + "is-frozen": False, + }, + {"amount": 0, "asset-id": self.asset_out.id, "is-frozen": False}, + ], + ) + with patch( + "algosdk.v2client.algod.AlgodClient.account_info", return_value=account_info + ): + opt_in_required_asset_ids = get_swap_router_app_opt_in_required_asset_ids( + algod_client=algod, + router_app_id=self.ROUTER_APP_ID, + asset_ids=route.asset_ids, + ) + self.assertEqual(opt_in_required_asset_ids, []) diff --git a/tinyman/swap_router/constants.py b/tinyman/swap_router/constants.py index db60d6b..82bf365 100644 --- a/tinyman/swap_router/constants.py +++ b/tinyman/swap_router/constants.py @@ -1,4 +1,4 @@ -TESTNET_SWAP_ROUTER_APP_ID_V1 = 0 # TODO +TESTNET_SWAP_ROUTER_APP_ID_V1 = 157589674 # TODO: temp app for testing. MAINNET_SWAP_ROUTER_APP_ID_V1 = 0 # TODO FIXED_INPUT_SWAP_TYPE = "fixed-input" diff --git a/tinyman/swap_router/routes.py b/tinyman/swap_router/routes.py index 558ad08..153dd7b 100644 --- a/tinyman/swap_router/routes.py +++ b/tinyman/swap_router/routes.py @@ -1,17 +1,23 @@ +import math from dataclasses import dataclass +from typing import Optional from typing import Union from tinyman.assets import Asset, AssetAmount +from tinyman.compat import SuggestedParams from tinyman.exceptions import PoolHasNoLiquidity, InsufficientReserves +from tinyman.utils import TransactionGroup from tinyman.v1.pools import Pool as TinymanV1Pool +from tinyman.v1.pools import SwapQuote as TinymanV1SwapQuote from tinyman.v2.pools import Pool as TinymanV2Pool +from tinyman.v2.quotes import SwapQuote as TinymanV2SwapQuote @dataclass class Route: asset_in: Asset asset_out: Asset - pools: Union[list[TinymanV1Pool], list[TinymanV2Pool]] + pools: Union[list[TinymanV2Pool], list[TinymanV1Pool]] def __str__(self): return "Route: " + "-> ".join(f"{pool}" for pool in self.pools) @@ -76,3 +82,144 @@ def get_fixed_output_first_quote(self, amount_out: int, slippage: float = 0.05): first_quote = quotes[0] return first_quote + + def prepare_swap_transactions_from_quotes( + self, + quotes: list[Union[TinymanV2SwapQuote, TinymanV1SwapQuote]], + user_address: Optional[str] = None, + suggested_params: Optional[SuggestedParams] = None, + ) -> TransactionGroup: + from tinyman.swap_router.swap_router import prepare_swap_router_transactions + + quote_count = len(quotes) + if quote_count == 1: + pool = self.pools[0] + quote = quotes[0] + + if isinstance(pool, TinymanV1Pool) and isinstance(quote, TinymanV1SwapQuote): + txn_group = pool.prepare_swap_transactions_from_quote( + quote=quote, + swapper_address=user_address, + # suggested_params=suggested_params, + ) + return txn_group + + elif isinstance(pool, TinymanV2Pool) and isinstance(quote, TinymanV2SwapQuote): + txn_group = pool.prepare_swap_transactions_from_quote( + quote=quote, + user_address=user_address, + suggested_params=suggested_params, + ) + return txn_group + else: + raise NotImplementedError() + + elif quote_count == 2: + pools = self.pools + swap_type = quotes[0].swap_type + tinyman_client = pools[0].client + + router_app_id = tinyman_client.router_app_id + validator_app_id = tinyman_client.validator_app_id + + input_asset_id = quotes[0].amount_in.asset.id + intermediary_asset_id = quotes[0].amount_out.asset.id + output_asset_id = quotes[-1].amount_out.asset.id + + asset_in_amount = quotes[0].amount_in_with_slippage.amount + asset_out_amount = quotes[-1].amount_out_with_slippage.amount + + txn_group = prepare_swap_router_transactions( + router_app_id=router_app_id, + validator_app_id=validator_app_id, + input_asset_id=input_asset_id, + intermediary_asset_id=intermediary_asset_id, + output_asset_id=output_asset_id, + asset_in_amount=asset_in_amount, + asset_out_amount=asset_out_amount, + swap_type=swap_type, + user_address=user_address, + suggested_params=suggested_params, + ) + return txn_group + + else: + raise NotImplementedError() + + @property + def asset_ids(self) -> list[int]: + asset_ids = [self.asset_in.id] + + for pool in self.pools: + if isinstance(pool, TinymanV2Pool): + asset_1_id = pool.asset_1.id + asset_2_id = pool.asset_2.id + elif isinstance(pool, TinymanV1Pool): + asset_1_id = pool.asset1.id + asset_2_id = pool.asset2.id + else: + raise NotImplementedError() + + if asset_ids[-1] == asset_1_id: + asset_ids.append(asset_2_id) + else: + asset_ids.append(asset_1_id) + + return asset_ids + + @classmethod + def calculate_price_impact_from_quotes(cls, quotes): + swap_price = math.prod([quote.price for quote in quotes]) + pool_price = math.prod([quote.price / (1 - quote.price_impact) for quote in quotes]) + price_impact = round(1 - (swap_price / pool_price), 5) + return price_impact + + +def get_best_fixed_input_route(routes: list[Route], amount_in: int) -> Optional[Route]: + best_route = None + best_route_price_impact = None + best_route_amount_out = None + + for route in routes: + try: + quotes = route.get_fixed_input_quotes(amount_in=amount_in) + except (InsufficientReserves, PoolHasNoLiquidity): + continue + + last_quote = quotes[-1] + price_impact = route.calculate_price_impact_from_quotes(quotes) + + if (not best_route) or ( + (best_route_amount_out, -best_route_price_impact) + < (last_quote.amount_out, -price_impact) + ): + best_route = route + best_route_amount_out = last_quote.amount_out + best_route_price_impact = price_impact + + return best_route + + +def get_best_fixed_output_route(routes: list[Route], amount_out: int): + best_route = None + best_route_price_impact = None + best_route_amount_in = None + + for route in routes: + try: + quotes = route.get_fixed_output_quotes(amount_out=amount_out) + except (InsufficientReserves, PoolHasNoLiquidity): + continue + + first_quote = quotes[0] + price_impact = route.calculate_price_impact_from_quotes(quotes) + + if (not best_route) or ( + (best_route_amount_in, best_route_price_impact) + > (first_quote.amount_in, price_impact) + ): + best_route = route + best_route_amount_in = first_quote.amount_in + best_route_price_impact = price_impact + + return best_route diff --git a/tinyman/swap_router/swap_router.py b/tinyman/swap_router/swap_router.py index 1d3429c..28ecf1b 100644 --- a/tinyman/swap_router/swap_router.py +++ b/tinyman/swap_router/swap_router.py @@ -1,27 +1,24 @@ -from typing import Union +from algosdk.logic import get_application_address +from algosdk.v2client.algod import AlgodClient +from requests import request, HTTPError +from tinyman.assets import Asset from tinyman.compat import ( AssetTransferTxn, ApplicationNoOpTxn, SuggestedParams, PaymentTxn, ) -from algosdk.logic import get_application_address -from requests import request, HTTPError - -from tinyman.assets import AssetAmount from tinyman.swap_router.constants import ( FIXED_INPUT_SWAP_TYPE, FIXED_OUTPUT_SWAP_TYPE, ) +from tinyman.swap_router.routes import Route from tinyman.utils import TransactionGroup -from tinyman.v1.client import TinymanClient -from tinyman.v1.pools import Pool as TinymanV1Pool, SwapQuote as TinymanV1SwapQuote from tinyman.v2.client import TinymanV2Client from tinyman.v2.constants import FIXED_INPUT_APP_ARGUMENT, FIXED_OUTPUT_APP_ARGUMENT from tinyman.v2.contracts import get_pool_logicsig from tinyman.v2.pools import Pool as TinymanV2Pool -from tinyman.v2.quotes import SwapQuote as TinymanV2SwapQuote def prepare_swap_router_asset_opt_in_transaction( @@ -116,122 +113,38 @@ def prepare_swap_router_transactions( return txn_group -def prepare_swap_router_transactions_from_quotes( - route_pools_and_quotes: list[ - Union[ - tuple[TinymanV1Pool, TinymanV1SwapQuote], - tuple[TinymanV2Pool, TinymanV2SwapQuote], - ] - ], - swap_type: str, - slippage: float = 0.05, - user_address: str = None, - suggested_params: SuggestedParams = None, -) -> TransactionGroup: - # override slippage - for i in range(len(route_pools_and_quotes)): - route_pools_and_quotes[i][1].slippage = slippage - - swap_count = len(route_pools_and_quotes) - if swap_count == 1: - pool, quote = route_pools_and_quotes[0] - quote.slippage = slippage - - if isinstance(pool, TinymanV1Pool): - return pool.prepare_swap_transactions_from_quote( - quote=quote, - swapper_address=user_address, - # suggested_params=suggested_params, - ) - elif isinstance(pool, TinymanV2Pool): - return pool.prepare_swap_transactions_from_quote( - quote=quote, - user_address=user_address, - suggested_params=suggested_params, - ) - else: - raise NotImplementedError() - - elif swap_count == 2: - pools, quotes = zip(*route_pools_and_quotes) - router_app_id = pools[0].client.router_app_id - validator_app_id = pools[0].client.validator_app_id - - input_asset_id = quotes[0].amount_in.asset.id - intermediary_asset_id = quotes[0].amount_out.asset.id - output_asset_id = quotes[-1].amount_out.asset.id - - txn_group = prepare_swap_router_transactions( - router_app_id=router_app_id, - validator_app_id=validator_app_id, - input_asset_id=input_asset_id, - intermediary_asset_id=intermediary_asset_id, - output_asset_id=output_asset_id, - asset_in_amount=quotes[0].amount_in_with_slippage.amount, - asset_out_amount=quotes[-1].amount_out_with_slippage.amount, - swap_type=swap_type, - user_address=user_address, - suggested_params=suggested_params, - ) - - algod = pools[0].client.algod - swap_router_app_address = get_application_address(router_app_id) - account_info = algod.account_info(swap_router_app_address) - opted_in_asset_ids = { - int(asset["asset-id"]) for asset in account_info["assets"] - } - asset_ids = ( - {input_asset_id, intermediary_asset_id, output_asset_id} - - {0} - - opted_in_asset_ids - ) - - if asset_ids: - opt_in_txn_group = prepare_swap_router_asset_opt_in_transaction( - router_app_id=router_app_id, - asset_ids=list(asset_ids), - user_address=user_address, - suggested_params=suggested_params, - ) - txn_group = opt_in_txn_group + txn_group +def get_swap_router_app_opt_in_required_asset_ids(algod_client: AlgodClient, router_app_id: int, asset_ids=list[int]) -> list[int]: + swap_router_app_address = get_application_address(router_app_id) + account_info = algod_client.account_info(swap_router_app_address) - return txn_group + app_opted_in_asset_ids = { + int(asset["asset-id"]) for asset in account_info["assets"] + } - else: - raise NotImplementedError() + app_opt_in_required_asset_ids = list(set(asset_ids) - {0} - app_opted_in_asset_ids) + return app_opt_in_required_asset_ids -def fetch_swap_route_quotes( - tinyman_v1_client: TinymanClient, - tinyman_v2_client: TinymanV2Client, - asset_in_id: int, - asset_out_id: int, +def fetch_best_route_suggestion( + tinyman_client: TinymanV2Client, + asset_in: Asset, + asset_out: Asset, swap_type: str, amount: int, -) -> list[ - Union[ - tuple[TinymanV1Pool, TinymanV1SwapQuote], - tuple[TinymanV2Pool, TinymanV2SwapQuote], - ] -]: +) -> Route: assert swap_type in (FIXED_INPUT_SWAP_TYPE, FIXED_OUTPUT_SWAP_TYPE) assert amount > 0 - assert asset_in_id >= 0 - assert asset_out_id >= 0 - assert isinstance(tinyman_v1_client, TinymanClient) - assert isinstance(tinyman_v2_client, TinymanV2Client) payload = { - "asset_in_id": str(asset_in_id), - "asset_out_id": str(asset_out_id), + "asset_in_id": str(asset_in.id), + "asset_out_id": str(asset_out.id), "swap_type": swap_type, "amount": str(amount), } raw_response = request( method="POST", - url="http://dev.analytics.tinyman.org/api/v1/swap-router/", - # TODO: url=client.api_base_url + "v1/swap-router/", "v1/swap-router/quotes/", + url=tinyman_client.api_base_url + "v1/swap-router/quotes/", json=payload, ) @@ -242,125 +155,29 @@ def fetch_swap_route_quotes( response = raw_response.json() print(response) - route_pools_and_quotes = [] - for swap in response["route"]: - if swap["pool"]["version"] == "1.1": - client = tinyman_v1_client - pool = TinymanV1Pool( - client=client, - asset_a=client.fetch_asset(int(swap["pool"]["asset_1_id"])), - asset_b=client.fetch_asset(int(swap["pool"]["asset_2_id"])), - fetch=True, - ) - - asset_in = client.fetch_asset(int(swap["quote"]["amount_in"]["asset_id"])) - amount_out = client.fetch_asset( - int(swap["quote"]["amount_out"]["asset_id"]) - ) - - quote = TinymanV1SwapQuote( - swap_type=swap_type, - amount_in=AssetAmount( - asset_in, int(swap["quote"]["amount_in"]["amount"]) - ), - amount_out=AssetAmount( - amount_out, int(swap["quote"]["amount_out"]["amount"]) - ), - swap_fees=AssetAmount( - asset_in, int(swap["quote"]["amount_in"]["amount"]) - ), - slippage=0, - price_impact=swap["quote"]["price_impact"], - ) - - elif swap["pool"]["version"] == "2.0": - client = tinyman_v2_client - - pool = TinymanV2Pool( - client=client, - asset_a=client.fetch_asset(int(swap["pool"]["asset_1_id"])), - asset_b=client.fetch_asset(int(swap["pool"]["asset_2_id"])), - fetch=True, - ) - - asset_in = client.fetch_asset(int(swap["quote"]["amount_in"]["asset_id"])) - amount_out = client.fetch_asset( - int(swap["quote"]["amount_out"]["asset_id"]) - ) - - quote = TinymanV2SwapQuote( - swap_type=swap_type, - amount_in=AssetAmount( - asset_in, int(swap["quote"]["amount_in"]["amount"]) - ), - amount_out=AssetAmount( - amount_out, int(swap["quote"]["amount_out"]["amount"]) - ), - swap_fees=AssetAmount( - asset_in, int(swap["quote"]["amount_in"]["amount"]) - ), - slippage=0, - price_impact=swap["quote"]["price_impact"], - ) - else: - raise NotImplementedError() - route_pools_and_quotes.append((pool, quote)) - - return route_pools_and_quotes - + pools = [] + for pool in response["pools"]: + pool = TinymanV2Pool( + client=tinyman_client, + asset_a=Asset( + id=pool["asset_1_id"]["id"], + name=pool["asset_1_id"]["name"], + unit_name=pool["asset_1_id"]["unit_name"], + decimals=pool["asset_1_id"]["decimals"], + ), + asset_b=Asset( + id=pool["asset_2_id"]["id"], + name=pool["asset_2_id"]["name"], + unit_name=pool["asset_2_id"]["unit_name"], + decimals=pool["asset_2_id"]["decimals"], + ), + fetch=True, + ) + pools.append(pool) -# def fetch_best_route( -# tinyman_v1_client: TinymanClient, -# tinyman_v2_client: TinymanV2Client, -# asset_in_id: int, -# asset_out_id: int, -# swap_type: str, -# amount: int, -# ): -# asset_in = tinyman_v2_client.fetch_asset(asset_in_id) -# asset_out = tinyman_v2_client.fetch_asset(asset_out_id) -# routes = [] -# -# v1_pool = TinymanV1Pool( -# client=tinyman_v1_client, -# asset_a=asset_in, -# asset_b=asset_out, -# fetch=True, -# ) -# if v1_pool.exists: -# direct_v1_route = Route(asset_in=asset_in, asset_out=asset_out, pools=[v1_pool]) -# routes.append(direct_v1_route) -# -# v2_pool = TinymanV2Pool( -# client=tinyman_v2_client, -# asset_a=asset_in, -# asset_b=asset_out, -# fetch=True, -# ) -# if v2_pool.exists: -# direct_v2_route = Route(asset_in=asset_in, asset_out=asset_out, pools=[v2_pool]) -# routes.append(direct_v2_route) -# -# try: -# smart_route = fetch_swap_route( -# tinyman_v1_client=tinyman_v1_client, -# tinyman_v2_client=tinyman_v2_client, -# asset_in_id=asset_in_id, -# asset_out_id=asset_out_id, -# swap_type=swap_type, -# amount=amount, -# ) -# except Exception: # TODO: Handle the exception properly. -# smart_swap_route = None -# -# if smart_swap_route is not None: -# routes.append(smart_swap_route) -# -# if swap_type == FIXED_INPUT_SWAP_TYPE: -# best_route = get_best_fixed_input_route(routes=routes, amount_in=amount) -# elif swap_type == FIXED_OUTPUT_SWAP_TYPE: -# best_route = get_best_fixed_output_route(routes=routes, amount_out=amount) -# else: -# raise NotImplementedError() -# -# return best_route + route = Route( + asset_in=asset_in, + asset_out=asset_out, + pools=pools + ) + return route diff --git a/tinyman/swap_router/utils.py b/tinyman/swap_router/utils.py deleted file mode 100644 index a28ad43..0000000 --- a/tinyman/swap_router/utils.py +++ /dev/null @@ -1,52 +0,0 @@ -from tinyman.swap_router.routes import Route -from tinyman.exceptions import InsufficientReserves, PoolHasNoLiquidity - - -def get_best_fixed_input_route(routes: list[Route], amount_in: int): - best_route = None - best_route_max_price_impact = None - best_route_amount_out = None - - for route in routes: - try: - quotes = route.get_fixed_input_quotes(amount_in=amount_in) - except (InsufficientReserves, PoolHasNoLiquidity): - continue - - last_quote = quotes[-1] - max_price_impact = max(quote.price_impact for quote in quotes) - - if (not best_route) or ( - (best_route_amount_out, -best_route_max_price_impact) - < (last_quote.amount_out, -max_price_impact) - ): - best_route = route - best_route_amount_out = last_quote.amount_out - best_route_max_price_impact = max_price_impact - - return best_route - - -def get_best_fixed_output_route(routes: list[Route], amount_out: int): - best_route = None - best_route_max_price_impact = None - best_route_amount_in = None - - for route in routes: - try: - quotes = route.get_fixed_output_quotes(amount_out=amount_out) - except (InsufficientReserves, PoolHasNoLiquidity): - continue - - first_quote = quotes[0] - max_price_impact = max(quote.price_impact for quote in quotes) - - if (not best_route) or ( - (best_route_amount_in, best_route_max_price_impact) - > (first_quote.amount_in, max_price_impact) - ): - best_route = route - best_route_amount_in = first_quote.amount_in - best_route_max_price_impact = max_price_impact - - return best_route diff --git a/tinyman/utils.py b/tinyman/utils.py index 5e43659..576d766 100644 --- a/tinyman/utils.py +++ b/tinyman/utils.py @@ -114,7 +114,7 @@ def calculate_price_impact( ): swap_price = swap_output_amount / swap_input_amount pool_price = output_supply / input_supply - price_impact = abs(round((swap_price / pool_price) - 1, 5)) + price_impact = abs(round(1 - (swap_price / pool_price), 5)) return price_impact diff --git a/tinyman/v1/pools.py b/tinyman/v1/pools.py index ba1f602..692cf22 100644 --- a/tinyman/v1/pools.py +++ b/tinyman/v1/pools.py @@ -6,7 +6,7 @@ from algosdk.v2client.algod import AlgodClient from tinyman.assets import Asset, AssetAmount -from tinyman.exceptions import InsufficientReserves +from tinyman.exceptions import InsufficientReserves, PoolHasNoLiquidity from tinyman.optin import prepare_asset_optin_transactions from tinyman.utils import get_state_int, calculate_price_impact from tinyman.v1.bootstrap import prepare_bootstrap_transactions @@ -371,7 +371,7 @@ def fetch_fixed_input_swap_quote( output_supply = self.asset1_reserves if not input_supply or not output_supply: - raise Exception("Pool has no liquidity!") + raise PoolHasNoLiquidity() # k = input_supply * output_supply # ignoring fees, k must remain constant diff --git a/tinyman/v2/client.py b/tinyman/v2/client.py index 60f1fe1..d8b6c23 100644 --- a/tinyman/v2/client.py +++ b/tinyman/v2/client.py @@ -46,12 +46,13 @@ def __init__( algod_client: AlgodClient, user_address: Optional[str] = None, client_name: Optional[str] = None, + api_base_url: Optional[str] = None ): super().__init__( algod_client, validator_app_id=TESTNET_VALIDATOR_APP_ID, router_app_id=TESTNET_SWAP_ROUTER_APP_ID_V1, - api_base_url="https://testnet.analytics.tinyman.org/api/", + api_base_url=api_base_url or "https://testnet.analytics.tinyman.org/api/", user_address=user_address, staking_app_id=TESTNET_STAKING_APP_ID, client_name=client_name, @@ -64,12 +65,13 @@ def __init__( algod_client: AlgodClient, user_address: Optional[str] = None, client_name: Optional[str] = None, + api_base_url: Optional[str] = None ): super().__init__( algod_client, validator_app_id=MAINNET_VALIDATOR_APP_ID, router_app_id=MAINNET_SWAP_ROUTER_APP_ID_V1, - api_base_url="https://mainnet.analytics.tinyman.org/api/", + api_base_url=api_base_url or "https://mainnet.analytics.tinyman.org/api/", user_address=user_address, staking_app_id=MAINNET_STAKING_APP_ID, client_name=client_name, From fcf6b1bb7d94396bc0df051bdd887ca33e14049b Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 16 Feb 2023 11:20:32 +0300 Subject: [PATCH 09/32] add swap router event log parser --- tests/swap_router/test.py | 50 ++++++++++++++++++++++-------- tinyman/swap_router/constants.py | 5 ++- tinyman/swap_router/routes.py | 15 ++++++--- tinyman/swap_router/swap_router.py | 27 +++++++--------- tinyman/swap_router/utils.py | 21 +++++++++++++ tinyman/v2/client.py | 4 +-- 6 files changed, 87 insertions(+), 35 deletions(-) create mode 100644 tinyman/swap_router/utils.py diff --git a/tests/swap_router/test.py b/tests/swap_router/test.py index f3ce23d..86d0adc 100644 --- a/tests/swap_router/test.py +++ b/tests/swap_router/test.py @@ -17,8 +17,10 @@ ) from tinyman.swap_router.swap_router import ( prepare_swap_router_asset_opt_in_transaction, - prepare_swap_router_transactions, get_swap_router_app_opt_in_required_asset_ids, + prepare_swap_router_transactions, + get_swap_router_app_opt_in_required_asset_ids, ) +from tinyman.swap_router.utils import parse_swap_router_event_log from tinyman.v1.client import TinymanClient from tinyman.v1.constants import TESTNET_VALIDATOR_APP_ID from tinyman.v1.pools import Pool as V1Pool @@ -519,7 +521,7 @@ def test_indirect_route_prepare_swap_transactions_from_quotes(self): route = Route( asset_in=self.asset_in, asset_out=self.asset_out, - pools=[self.pool_1, self.pool_2] + pools=[self.pool_1, self.pool_2], ) quote_1 = V2SwapQuote( @@ -605,9 +607,7 @@ def test_direct_route_prepare_swap_transactions_from_quotes(self): swap_type = "fixed-input" route = Route( - asset_in=self.asset_in, - asset_out=self.asset_out, - pools=[self.pool] + asset_in=self.asset_in, asset_out=self.asset_out, pools=[self.pool] ) quote = V2SwapQuote( @@ -681,11 +681,7 @@ def test_v1_direct_route_prepare_swap_transactions_from_quotes(self): v1_pool.exists = True v1_pool.liquidity_asset = Asset(id=99, name="TM", unit_name="TM", decimals=6) - route = Route( - asset_in=self.asset_in, - asset_out=self.asset_out, - pools=[v1_pool] - ) + route = Route(asset_in=self.asset_in, asset_out=self.asset_out, pools=[v1_pool]) quote = V1SwapQuote( swap_type=swap_type, @@ -721,7 +717,7 @@ def test_swap_route_opt_in(self): route = Route( asset_in=self.asset_in, asset_out=self.asset_out, - pools=[self.pool_1, self.pool_2] + pools=[self.pool_1, self.pool_2], ) opt_in_app_call_txn = { "apaa": [b"asset_opt_in"], @@ -756,7 +752,7 @@ def test_swap_route_opt_in(self): router_app_id=self.ROUTER_APP_ID, asset_ids=opt_in_required_asset_ids, user_address=user_address, - suggested_params=sp + suggested_params=sp, ) self.assertEqual(len(opt_in_txn_group.transactions), 1) @@ -791,7 +787,7 @@ def test_swap_route_opt_in(self): router_app_id=self.ROUTER_APP_ID, asset_ids=opt_in_required_asset_ids, user_address=user_address, - suggested_params=sp + suggested_params=sp, ) self.assertEqual(len(opt_in_txn_group.transactions), 1) @@ -821,3 +817,31 @@ def test_swap_route_opt_in(self): asset_ids=route.asset_ids, ) self.assertEqual(opt_in_required_asset_ids, []) + + +class EventLogParserTestCase(TestCase): + def test_bytes_log(self): + raw_log = b"\x81b\xda\x9e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x03\xe8\x00\x00\x00\x00\x00\x00&\xbb" + result = parse_swap_router_event_log(log=raw_log) + self.assertDictEqual( + result, + { + "input_asset_id": 0, + "output_asset_id": 5, + "input_amount": 1000, + "output_amount": 9915, + }, + ) + + def test_string_log(self): + raw_log = "gWLangAAAAAAn5c9AAAAAAQwcrUAAAAAAAGGoAAAAAAAA5u2" + result = parse_swap_router_event_log(log=raw_log) + self.assertDictEqual( + result, + { + "input_asset_id": 10458941, + "output_asset_id": 70283957, + "input_amount": 100000, + "output_amount": 236470, + }, + ) diff --git a/tinyman/swap_router/constants.py b/tinyman/swap_router/constants.py index 82bf365..64af07c 100644 --- a/tinyman/swap_router/constants.py +++ b/tinyman/swap_router/constants.py @@ -1,5 +1,8 @@ -TESTNET_SWAP_ROUTER_APP_ID_V1 = 157589674 # TODO: temp app for testing. +TESTNET_SWAP_ROUTER_APP_ID_V1 = 159521633 # TODO: temp app for testing. MAINNET_SWAP_ROUTER_APP_ID_V1 = 0 # TODO FIXED_INPUT_SWAP_TYPE = "fixed-input" FIXED_OUTPUT_SWAP_TYPE = "fixed-output" + +# Event Log Selectors +SWAP_EVENT_LOG_SELECTOR = b"\x81b\xda\x9e" # "swap(uint64,uint64,uint64,uint64)" diff --git a/tinyman/swap_router/routes.py b/tinyman/swap_router/routes.py index 153dd7b..b59fecd 100644 --- a/tinyman/swap_router/routes.py +++ b/tinyman/swap_router/routes.py @@ -20,7 +20,7 @@ class Route: pools: Union[list[TinymanV2Pool], list[TinymanV1Pool]] def __str__(self): - return "Route: " + "-> ".join(f"{pool}" for pool in self.pools) + return "Route: " + " -> ".join(f"{pool}" for pool in self.pools) def get_fixed_input_quotes(self, amount_in: int, slippage: float = 0.05): quotes = [] @@ -96,7 +96,9 @@ def prepare_swap_transactions_from_quotes( pool = self.pools[0] quote = quotes[0] - if isinstance(pool, TinymanV1Pool) and isinstance(quote, TinymanV1SwapQuote): + if isinstance(pool, TinymanV1Pool) and isinstance( + quote, TinymanV1SwapQuote + ): txn_group = pool.prepare_swap_transactions_from_quote( quote=quote, swapper_address=user_address, @@ -104,7 +106,9 @@ def prepare_swap_transactions_from_quotes( ) return txn_group - elif isinstance(pool, TinymanV2Pool) and isinstance(quote, TinymanV2SwapQuote): + elif isinstance(pool, TinymanV2Pool) and isinstance( + quote, TinymanV2SwapQuote + ): txn_group = pool.prepare_swap_transactions_from_quote( quote=quote, user_address=user_address, @@ -118,6 +122,7 @@ def prepare_swap_transactions_from_quotes( pools = self.pools swap_type = quotes[0].swap_type tinyman_client = pools[0].client + user_address = user_address or tinyman_client.user_address router_app_id = tinyman_client.router_app_id validator_app_id = tinyman_client.validator_app_id @@ -170,7 +175,9 @@ def asset_ids(self) -> list[int]: @classmethod def calculate_price_impact_from_quotes(cls, quotes): swap_price = math.prod([quote.price for quote in quotes]) - pool_price = math.prod([quote.price / (1 - quote.price_impact) for quote in quotes]) + pool_price = math.prod( + [quote.price / (1 - quote.price_impact) for quote in quotes] + ) price_impact = round(1 - (swap_price / pool_price), 5) return price_impact diff --git a/tinyman/swap_router/swap_router.py b/tinyman/swap_router/swap_router.py index 28ecf1b..9a7a2f4 100644 --- a/tinyman/swap_router/swap_router.py +++ b/tinyman/swap_router/swap_router.py @@ -113,7 +113,9 @@ def prepare_swap_router_transactions( return txn_group -def get_swap_router_app_opt_in_required_asset_ids(algod_client: AlgodClient, router_app_id: int, asset_ids=list[int]) -> list[int]: +def get_swap_router_app_opt_in_required_asset_ids( + algod_client: AlgodClient, router_app_id: int, asset_ids=list[int] +) -> list[int]: swap_router_app_address = get_application_address(router_app_id) account_info = algod_client.account_info(swap_router_app_address) @@ -153,31 +155,26 @@ def fetch_best_route_suggestion( raise HTTPError(response=raw_response) response = raw_response.json() - print(response) pools = [] for pool in response["pools"]: pool = TinymanV2Pool( client=tinyman_client, asset_a=Asset( - id=pool["asset_1_id"]["id"], - name=pool["asset_1_id"]["name"], - unit_name=pool["asset_1_id"]["unit_name"], - decimals=pool["asset_1_id"]["decimals"], + id=int(pool["asset_1"]["id"]), + name=pool["asset_1"]["name"], + unit_name=pool["asset_1"]["unit_name"], + decimals=pool["asset_1"]["decimals"], ), asset_b=Asset( - id=pool["asset_2_id"]["id"], - name=pool["asset_2_id"]["name"], - unit_name=pool["asset_2_id"]["unit_name"], - decimals=pool["asset_2_id"]["decimals"], + id=int(pool["asset_2"]["id"]), + name=pool["asset_2"]["name"], + unit_name=pool["asset_2"]["unit_name"], + decimals=pool["asset_2"]["decimals"], ), fetch=True, ) pools.append(pool) - route = Route( - asset_in=asset_in, - asset_out=asset_out, - pools=pools - ) + route = Route(asset_in=asset_in, asset_out=asset_out, pools=pools) return route diff --git a/tinyman/swap_router/utils.py b/tinyman/swap_router/utils.py new file mode 100644 index 0000000..a2e321e --- /dev/null +++ b/tinyman/swap_router/utils.py @@ -0,0 +1,21 @@ +from base64 import b64decode +from typing import Union, Optional + + +def parse_swap_router_event_log(log: Union[bytes, str]) -> Optional[dict]: + # Signature is "swap(uint64,uint64,uint64,uint64)" + swap_event_selector = b"\x81b\xda\x9e" + + if isinstance(log, str): + # Indexer returns logs as b64 encoded. + log = b64decode(log) + + if log[:4] == swap_event_selector and len(log) >= 36: + return dict( + input_asset_id=int.from_bytes(log[4:12], "big"), + output_asset_id=int.from_bytes(log[12:20], "big"), + input_amount=int.from_bytes(log[20:28], "big"), + output_amount=int.from_bytes(log[28:36], "big"), + ) + + return None diff --git a/tinyman/v2/client.py b/tinyman/v2/client.py index d8b6c23..2516863 100644 --- a/tinyman/v2/client.py +++ b/tinyman/v2/client.py @@ -46,7 +46,7 @@ def __init__( algod_client: AlgodClient, user_address: Optional[str] = None, client_name: Optional[str] = None, - api_base_url: Optional[str] = None + api_base_url: Optional[str] = None, ): super().__init__( algod_client, @@ -65,7 +65,7 @@ def __init__( algod_client: AlgodClient, user_address: Optional[str] = None, client_name: Optional[str] = None, - api_base_url: Optional[str] = None + api_base_url: Optional[str] = None, ): super().__init__( algod_client, From dc7a71d08be2cccb0739d20fa91e852d84110991 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 16 Feb 2023 15:57:22 +0300 Subject: [PATCH 10/32] add swap router example --- examples/swap_router/swap.py | 182 +++++++++++++++++++++++++++++ tinyman/swap_router/swap_router.py | 1 - tinyman/v1/pools.py | 2 +- tinyman/v2/pools.py | 2 +- 4 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 examples/swap_router/swap.py diff --git a/examples/swap_router/swap.py b/examples/swap_router/swap.py new file mode 100644 index 0000000..a910f29 --- /dev/null +++ b/examples/swap_router/swap.py @@ -0,0 +1,182 @@ +# This sample is provided for demonstration purposes only. +# It is not intended for production use. +# This example does not constitute trading advice. +from typing import Optional +from urllib.parse import quote_plus + +from examples.v2.utils import get_algod +from tinyman.assets import Asset +from tinyman.assets import AssetAmount +from tinyman.optin import prepare_asset_optin_transactions +from tinyman.swap_router.routes import Route +from tinyman.swap_router.routes import get_best_fixed_input_route +from tinyman.swap_router.swap_router import fetch_best_route_suggestion +from tinyman.swap_router.swap_router import get_swap_router_app_opt_in_required_asset_ids, prepare_swap_router_asset_opt_in_transaction +from tinyman.v1.client import TinymanClient +from tinyman.v1.pools import Pool as TinymanV1Pool +from tinyman.v2.client import TinymanV2Client +from tinyman.v2.client import TinymanV2TestnetClient +from tinyman.v2.pools import Pool as TinymanV2Pool + + +def fetch_routes( + tinyman_v1_client: Optional[TinymanClient], + tinyman_v2_client: TinymanV2Client, + asset_in: Asset, + asset_out: Asset, + swap_type: str, + amount: int, +) -> list[Route]: + """ + This is an example route list preparation. + You can build yor own route list according to your needs. + + The list contains; + V1 Direct Route if it exists and has liquidity, + V2 Direct Route if it exists and has liquidity, + V2 Indirect (2 swap) Route provided by Tinyman API. + """ + routes = [] + + if tinyman_v1_client is not None: + # Don't check V1 pool if the client is not provided + v1_pool = TinymanV1Pool( + client=tinyman_v1_client, + asset_a=asset_in, + asset_b=asset_out, + fetch=True, + ) + if v1_pool.exists and v1_pool.issued_liquidity: + v1_direct_route = Route(asset_in=asset_in, asset_out=asset_out, pools=[v1_pool]) + routes.append(v1_direct_route) + + v2_pool = TinymanV2Pool( + client=tinyman_v2_client, + asset_a=asset_in, + asset_b=asset_out, + fetch=True, + ) + if v2_pool.exists and v2_pool.issued_pool_tokens: + v2_direct_route = Route(asset_in=asset_in, asset_out=asset_out, pools=[v2_pool]) + routes.append(v2_direct_route) + + route = fetch_best_route_suggestion( + tinyman_client=tinyman_v2_client, + asset_in=asset_in, + asset_out=asset_out, + swap_type=swap_type, + amount=amount, + ) + if len(route.pools) > 1: + routes.append(route) + + return routes + + +def swap(asset_in_id, asset_out_id, account): + algod = get_algod() + + # tinyman_v1_client = TinymanTestnetClient(algod_client=algod, user_address=account["address"]) + tinyman_v2_client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) + + asset_in = tinyman_v2_client.fetch_asset(asset_in_id) + asset_out = tinyman_v2_client.fetch_asset(asset_out_id) + + asset_amount_in = AssetAmount(asset_in, 100_000) + routes = fetch_routes( + tinyman_v1_client=None, + tinyman_v2_client=tinyman_v2_client, + asset_in=asset_in, + asset_out=asset_out, + swap_type="fixed-input", + amount=asset_amount_in.amount + ) + + if routes: + print("Routes:") + for index, route in enumerate(routes, start=1): + print(index, route) + else: + print("There is no route available.") + return + + print("Best Route:") + best_route = get_best_fixed_input_route(routes=routes, amount_in=asset_amount_in.amount) + print(best_route) + + if best_route: + print("Quotes:") + quotes = best_route.get_fixed_input_quotes(amount_in=asset_amount_in.amount) + for index, quote in enumerate(quotes, start=1): + print(index, quote) + else: + print("Couldn't calculate the quotes.") + return + + suggested_params = tinyman_v2_client.algod.suggested_params() + + # 1 - Transfer input to swap router app account + # 2 - Swap router app call + txn_group = best_route.prepare_swap_transactions_from_quotes(quotes=quotes, suggested_params=suggested_params) + + if len(quotes) > 1: + print("The best route is single hop swap. Prepare swap router transactions.") + + # Swap router app account may require to opt in to some assets. + opt_in_required_asset_ids = get_swap_router_app_opt_in_required_asset_ids( + algod_client=tinyman_v2_client.algod, + router_app_id=tinyman_v2_client.router_app_id, + asset_ids=best_route.asset_ids, + ) + if opt_in_required_asset_ids: + opt_in_txn_group = prepare_swap_router_asset_opt_in_transaction( + router_app_id=tinyman_v2_client.router_app_id, + asset_ids=opt_in_required_asset_ids, + user_address=account["address"], + suggested_params=suggested_params + ) + txn_group = opt_in_txn_group + txn_group + + # User account may require to opt in to output asset. + if asset_out_id: + account_info = algod.account_info(account["address"]) + opted_in_asset_ids = { + int(asset["asset-id"]) for asset in account_info["assets"] + } + if asset_out_id not in opted_in_asset_ids: + user_opt_in_txn_group = prepare_asset_optin_transactions( + asset_id=asset_out_id, + sender=account["address"], + suggested_params=suggested_params, + ) + txn_group = user_opt_in_txn_group + txn_group + + else: + print() + print("The best route is direct swap.") + return + + # Sign + txn_group.sign_with_private_key(account["address"], account["private_key"]) + + # Submit transactions to the network and wait for confirmation + txn_info = tinyman_v2_client.submit(txn_group, wait=True) + print("Transaction Info") + print(txn_info) + + print( + f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" + ) + + +if __name__ == '__main__': + # TODO: Set Account + account = { + "address": "ALGORAND_ADDRESS_HERE", + "private_key": "base64_private_key_here", + } + + # TODO: Set asset ids + asset_in_id, asset_out_id = 0, 21582668 + + swap(asset_in_id, asset_out_id, account) diff --git a/tinyman/swap_router/swap_router.py b/tinyman/swap_router/swap_router.py index 9a7a2f4..966d99c 100644 --- a/tinyman/swap_router/swap_router.py +++ b/tinyman/swap_router/swap_router.py @@ -150,7 +150,6 @@ def fetch_best_route_suggestion( json=payload, ) - # TODO: Handle all errors properly. if raw_response.status_code != 200: raise HTTPError(response=raw_response) diff --git a/tinyman/v1/pools.py b/tinyman/v1/pools.py index 692cf22..c6a90c9 100644 --- a/tinyman/v1/pools.py +++ b/tinyman/v1/pools.py @@ -193,7 +193,7 @@ def __init__( self.update_from_info(info) def __repr__(self): - return f"Pool {self.asset1.unit_name}({self.asset1.id})-{self.asset2.unit_name}({self.asset2.id}) {self.address}" + return f"Pool V1 {self.asset1.unit_name}({self.asset1.id})-{self.asset2.unit_name}({self.asset2.id}) {self.address}" @classmethod def from_account_info(cls, account_info, client=None): diff --git a/tinyman/v2/pools.py b/tinyman/v2/pools.py index 607d8ec..f5cfe0e 100644 --- a/tinyman/v2/pools.py +++ b/tinyman/v2/pools.py @@ -143,7 +143,7 @@ def __init__( self.update_from_info(info, fetch) def __repr__(self): - return f"Pool {self.asset_1.unit_name}({self.asset_1.id})-{self.asset_2.unit_name}({self.asset_2.id}) {self.address}" + return f"Pool V2 {self.asset_1.unit_name}({self.asset_1.id})-{self.asset_2.unit_name}({self.asset_2.id}) {self.address}" @classmethod def from_account_info( From 5818c5586cc1ff5f96837db93600c0b877fb74ca Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Fri, 17 Feb 2023 18:15:18 +0300 Subject: [PATCH 11/32] fix route price impact --- tinyman/swap_router/routes.py | 39 +++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/tinyman/swap_router/routes.py b/tinyman/swap_router/routes.py index b59fecd..88c5f96 100644 --- a/tinyman/swap_router/routes.py +++ b/tinyman/swap_router/routes.py @@ -172,13 +172,40 @@ def asset_ids(self) -> list[int]: return asset_ids - @classmethod - def calculate_price_impact_from_quotes(cls, quotes): + @property + def price(self): + input_asset_id = self.asset_in.id + + pool_prices = [] + for pool in self.pools: + if isinstance(pool, TinymanV2Pool): + asset_1_id = pool.asset_1.id + asset_2_id = pool.asset_2.id + pool_asset_1_price = pool.asset_1_price + pool_asset_2_price = pool.asset_2_price + + elif isinstance(pool, TinymanV1Pool): + asset_1_id = pool.asset1.id + asset_2_id = pool.asset2.id + pool_asset_1_price = pool.asset1_price + pool_asset_2_price = pool.asset2_price + + else: + raise NotImplementedError() + + if input_asset_id == asset_1_id: + pool_prices.append(pool_asset_1_price) + input_asset_id = asset_2_id + else: + pool_prices.append(pool_asset_2_price) + input_asset_id = asset_1_id + + return math.prod(pool_prices) + + def calculate_price_impact_from_quotes(self, quotes): swap_price = math.prod([quote.price for quote in quotes]) - pool_price = math.prod( - [quote.price / (1 - quote.price_impact) for quote in quotes] - ) - price_impact = round(1 - (swap_price / pool_price), 5) + route_price = self.price + price_impact = round(1 - (swap_price / route_price), 5) return price_impact From ea5296d90b2fbe337d04e75628390c6a2b62caa2 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 1 Mar 2023 11:14:26 +0300 Subject: [PATCH 12/32] add app call note to swap router --- tinyman/swap_router/routes.py | 1 + tinyman/swap_router/swap_router.py | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/tinyman/swap_router/routes.py b/tinyman/swap_router/routes.py index 88c5f96..5995c65 100644 --- a/tinyman/swap_router/routes.py +++ b/tinyman/swap_router/routes.py @@ -145,6 +145,7 @@ def prepare_swap_transactions_from_quotes( swap_type=swap_type, user_address=user_address, suggested_params=suggested_params, + app_call_note=tinyman_client.generate_app_call_note(), ) return txn_group diff --git a/tinyman/swap_router/swap_router.py b/tinyman/swap_router/swap_router.py index 966d99c..69c30f1 100644 --- a/tinyman/swap_router/swap_router.py +++ b/tinyman/swap_router/swap_router.py @@ -1,3 +1,5 @@ +from typing import Optional + from algosdk.logic import get_application_address from algosdk.v2client.algod import AlgodClient from requests import request, HTTPError @@ -54,6 +56,7 @@ def prepare_swap_router_transactions( swap_type: [str, bytes], user_address: str, suggested_params: SuggestedParams, + app_call_note: Optional[str] = None, ) -> TransactionGroup: pool_1_logicsig = get_pool_logicsig( validator_app_id, input_asset_id, intermediary_asset_id @@ -88,6 +91,7 @@ def prepare_swap_router_transactions( accounts=[pool_1_address, pool_2_address], foreign_apps=[validator_app_id], foreign_assets=[input_asset_id, intermediary_asset_id, output_asset_id], + note=app_call_note, ), ] @@ -156,20 +160,20 @@ def fetch_best_route_suggestion( response = raw_response.json() pools = [] - for pool in response["pools"]: + for quote in response["route"]: pool = TinymanV2Pool( client=tinyman_client, asset_a=Asset( - id=int(pool["asset_1"]["id"]), - name=pool["asset_1"]["name"], - unit_name=pool["asset_1"]["unit_name"], - decimals=pool["asset_1"]["decimals"], + id=int(quote["pool"]["asset_1"]["id"]), + name=quote["pool"]["asset_1"]["name"], + unit_name=quote["pool"]["asset_1"]["unit_name"], + decimals=quote["pool"]["asset_1"]["decimals"], ), asset_b=Asset( - id=int(pool["asset_2"]["id"]), - name=pool["asset_2"]["name"], - unit_name=pool["asset_2"]["unit_name"], - decimals=pool["asset_2"]["decimals"], + id=int(quote["pool"]["asset_2"]["id"]), + name=quote["pool"]["asset_2"]["name"], + unit_name=quote["pool"]["asset_2"]["unit_name"], + decimals=quote["pool"]["asset_2"]["decimals"], ), fetch=True, ) From 93c8f04fd51324d9c2082d00afdb3c645361814a Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 1 Mar 2023 15:20:09 +0300 Subject: [PATCH 13/32] make swap router txn fee aware --- tinyman/swap_router/routes.py | 65 ++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/tinyman/swap_router/routes.py b/tinyman/swap_router/routes.py index 5995c65..39c31d3 100644 --- a/tinyman/swap_router/routes.py +++ b/tinyman/swap_router/routes.py @@ -3,6 +3,8 @@ from typing import Optional from typing import Union +from algosdk.constants import MIN_TXN_FEE + from tinyman.assets import Asset, AssetAmount from tinyman.compat import SuggestedParams from tinyman.exceptions import PoolHasNoLiquidity, InsufficientReserves @@ -203,17 +205,48 @@ def price(self): return math.prod(pool_prices) - def calculate_price_impact_from_quotes(self, quotes): - swap_price = math.prod([quote.price for quote in quotes]) + @classmethod + def get_swap_price_from_quotes(cls, quotes, asset_in_algo_price: Optional[int] = None): + amount_in = quotes[0].amount_in.amount + amount_out = quotes[-1].amount_out.amount + + if asset_in_algo_price and asset_in_algo_price > 0: + transaction_count = cls.get_transaction_count(quotes) + + txn_fee_in_algo = MIN_TXN_FEE * transaction_count + txn_fee_in_asset_in = txn_fee_in_algo / asset_in_algo_price + amount_in += txn_fee_in_asset_in + + swap_price = amount_out / amount_in + return swap_price + + def get_price_impact_from_quotes(self, quotes): + swap_price = self.get_swap_price_from_quotes(quotes) route_price = self.price price_impact = round(1 - (swap_price / route_price), 5) return price_impact + @classmethod + def get_transaction_count(cls, quotes) -> int: + if len(quotes) == 2: + transaction_count = 10 + elif len(quotes) == 1: + if quotes[0].swap_type == "fixed-input": + transaction_count = 3 + elif quotes[0].swap_type == "fixed-output": + transaction_count = 4 + else: + raise NotImplementedError() + else: + raise NotImplementedError() + + return transaction_count -def get_best_fixed_input_route(routes: list[Route], amount_in: int) -> Optional[Route]: + +def get_best_fixed_input_route(routes: list[Route], amount_in: int, asset_in_algo_price: Optional[float] = None) -> Optional[Route]: best_route = None best_route_price_impact = None - best_route_amount_out = None + best_route_swap_price = None for route in routes: try: @@ -221,24 +254,24 @@ def get_best_fixed_input_route(routes: list[Route], amount_in: int) -> Optional[ except (InsufficientReserves, PoolHasNoLiquidity): continue - last_quote = quotes[-1] - price_impact = route.calculate_price_impact_from_quotes(quotes) + swap_price = route.get_swap_price_from_quotes(quotes, asset_in_algo_price) + price_impact = route.get_price_impact_from_quotes(quotes) if (not best_route) or ( - (best_route_amount_out, -best_route_price_impact) - < (last_quote.amount_out, -price_impact) + (best_route_swap_price, -best_route_price_impact) + < (swap_price, -price_impact) ): best_route = route - best_route_amount_out = last_quote.amount_out + best_route_swap_price = swap_price best_route_price_impact = price_impact return best_route -def get_best_fixed_output_route(routes: list[Route], amount_out: int): +def get_best_fixed_output_route(routes: list[Route], amount_out: int, asset_in_algo_price: Optional[float] = None): best_route = None best_route_price_impact = None - best_route_amount_in = None + best_route_swap_price = None for route in routes: try: @@ -246,15 +279,15 @@ def get_best_fixed_output_route(routes: list[Route], amount_out: int): except (InsufficientReserves, PoolHasNoLiquidity): continue - first_quote = quotes[0] - price_impact = route.calculate_price_impact_from_quotes(quotes) + swap_price = route.get_swap_price_from_quotes(quotes, asset_in_algo_price) + price_impact = route.get_price_impact_from_quotes(quotes) if (not best_route) or ( - (best_route_amount_in, best_route_price_impact) - > (first_quote.amount_in, price_impact) + (best_route_swap_price, -best_route_price_impact) + < (swap_price, -price_impact) ): best_route = route - best_route_amount_in = first_quote.amount_in + best_route_swap_price = swap_price best_route_price_impact = price_impact return best_route From 68ef972702f8e510032384027dfb8b8c0f953bab Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 1 Mar 2023 18:12:36 +0300 Subject: [PATCH 14/32] minor refactor swap router transaction preperation --- examples/swap_router/swap.py | 61 +++++++++++++++++++++-------------- tinyman/client.py | 5 +++ tinyman/swap_router/routes.py | 61 ++++------------------------------- 3 files changed, 48 insertions(+), 79 deletions(-) diff --git a/examples/swap_router/swap.py b/examples/swap_router/swap.py index a910f29..b039fca 100644 --- a/examples/swap_router/swap.py +++ b/examples/swap_router/swap.py @@ -73,7 +73,7 @@ def fetch_routes( return routes -def swap(asset_in_id, asset_out_id, account): +def swap(asset_in_id, asset_out_id, amount, account): algod = get_algod() # tinyman_v1_client = TinymanTestnetClient(algod_client=algod, user_address=account["address"]) @@ -82,7 +82,7 @@ def swap(asset_in_id, asset_out_id, account): asset_in = tinyman_v2_client.fetch_asset(asset_in_id) asset_out = tinyman_v2_client.fetch_asset(asset_out_id) - asset_amount_in = AssetAmount(asset_in, 100_000) + asset_amount_in = AssetAmount(asset_in, amount) routes = fetch_routes( tinyman_v1_client=None, tinyman_v2_client=tinyman_v2_client, @@ -115,13 +115,14 @@ def swap(asset_in_id, asset_out_id, account): suggested_params = tinyman_v2_client.algod.suggested_params() - # 1 - Transfer input to swap router app account - # 2 - Swap router app call - txn_group = best_route.prepare_swap_transactions_from_quotes(quotes=quotes, suggested_params=suggested_params) - if len(quotes) > 1: + # Swap Router Flow print("The best route is single hop swap. Prepare swap router transactions.") + # 1 - Transfer input to swap router app account + # 2 - Swap router app call + txn_group = best_route.prepare_swap_router_transactions_from_quotes(quotes=quotes, suggested_params=suggested_params) + # Swap router app account may require to opt in to some assets. opt_in_required_asset_ids = get_swap_router_app_opt_in_required_asset_ids( algod_client=tinyman_v2_client.algod, @@ -137,24 +138,34 @@ def swap(asset_in_id, asset_out_id, account): ) txn_group = opt_in_txn_group + txn_group - # User account may require to opt in to output asset. - if asset_out_id: - account_info = algod.account_info(account["address"]) - opted_in_asset_ids = { - int(asset["asset-id"]) for asset in account_info["assets"] - } - if asset_out_id not in opted_in_asset_ids: - user_opt_in_txn_group = prepare_asset_optin_transactions( - asset_id=asset_out_id, - sender=account["address"], - suggested_params=suggested_params, - ) - txn_group = user_opt_in_txn_group + txn_group - else: - print() - print("The best route is direct swap.") - return + # Direct Swap Flow + pool = best_route.pools[0] + quote = quotes[0] + + if pool.client.version == "v1": + print(f"The best route is direct swap using {pool}.") + print("V1 swap flow is not handled in this example.") + return + + elif pool.client.version == "v2": + print(f"The best route is direct swap using {pool}.") + txn_group = pool.prepare_swap_transactions_from_quote( + quote=quote, + user_address=account["address"], + suggested_params=suggested_params, + ) + else: + raise NotImplementedError() + + # User account may require to opt in to output asset. + if not tinyman_v2_client.asset_is_opted_in(asset_id=asset_out_id, user_address=account["address"]): + user_opt_in_txn_group = prepare_asset_optin_transactions( + asset_id=asset_out_id, + sender=account["address"], + suggested_params=suggested_params, + ) + txn_group = user_opt_in_txn_group + txn_group # Sign txn_group.sign_with_private_key(account["address"], account["private_key"]) @@ -165,7 +176,7 @@ def swap(asset_in_id, asset_out_id, account): print(txn_info) print( - f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" + f"\nCheck the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" ) @@ -179,4 +190,4 @@ def swap(asset_in_id, asset_out_id, account): # TODO: Set asset ids asset_in_id, asset_out_id = 0, 21582668 - swap(asset_in_id, asset_out_id, account) + swap(asset_in_id, asset_out_id, 1_000_000, account) diff --git a/tinyman/client.py b/tinyman/client.py index 1a82709..682e782 100644 --- a/tinyman/client.py +++ b/tinyman/client.py @@ -80,6 +80,11 @@ def is_opted_in(self, user_address=None): def asset_is_opted_in(self, asset_id, user_address=None): user_address = user_address or self.user_address + + if asset_id == 0: + # ALGO + return True + account_info = self.algod.account_info(user_address) for a in account_info.get("assets", []): if a["asset-id"] == asset_id: diff --git a/tinyman/swap_router/routes.py b/tinyman/swap_router/routes.py index 39c31d3..2e4245c 100644 --- a/tinyman/swap_router/routes.py +++ b/tinyman/swap_router/routes.py @@ -10,7 +10,6 @@ from tinyman.exceptions import PoolHasNoLiquidity, InsufficientReserves from tinyman.utils import TransactionGroup from tinyman.v1.pools import Pool as TinymanV1Pool -from tinyman.v1.pools import SwapQuote as TinymanV1SwapQuote from tinyman.v2.pools import Pool as TinymanV2Pool from tinyman.v2.quotes import SwapQuote as TinymanV2SwapQuote @@ -40,19 +39,9 @@ def get_fixed_input_quotes(self, amount_in: int, slippage: float = 0.05): quotes.append(quote) current_asset_in_amount = quote.amount_out - last_quote = quotes[-1] - assert last_quote.amount_out.asset.id == self.asset_out.id + assert quotes[-1].amount_out.asset.id == self.asset_out.id return quotes - def get_fixed_input_last_quote(self, amount_in: int, slippage: float = 0.05): - try: - quotes = self.get_fixed_input_quotes(amount_in=amount_in, slippage=slippage) - except (InsufficientReserves, PoolHasNoLiquidity): - return None - - last_quote = quotes[-1] - return last_quote - def get_fixed_output_quotes(self, amount_out: int, slippage: float = 0.05): quotes = [] assert self.pools @@ -70,57 +59,19 @@ def get_fixed_output_quotes(self, amount_out: int, slippage: float = 0.05): current_asset_out_amount = quote.amount_in quotes.reverse() - first_quote = quotes[0] - assert first_quote.amount_in.asset.id == self.asset_in.id + assert quotes[0].amount_in.asset.id == self.asset_in.id return quotes - def get_fixed_output_first_quote(self, amount_out: int, slippage: float = 0.05): - try: - quotes = self.get_fixed_output_quotes( - amount_out=amount_out, slippage=slippage - ) - except (InsufficientReserves, PoolHasNoLiquidity): - return None - - first_quote = quotes[0] - return first_quote - - def prepare_swap_transactions_from_quotes( + def prepare_swap_router_transactions_from_quotes( self, - quotes: list[Union[TinymanV2SwapQuote, TinymanV1SwapQuote]], + quotes: list[TinymanV2SwapQuote], user_address: Optional[str] = None, suggested_params: Optional[SuggestedParams] = None, ) -> TransactionGroup: from tinyman.swap_router.swap_router import prepare_swap_router_transactions quote_count = len(quotes) - if quote_count == 1: - pool = self.pools[0] - quote = quotes[0] - - if isinstance(pool, TinymanV1Pool) and isinstance( - quote, TinymanV1SwapQuote - ): - txn_group = pool.prepare_swap_transactions_from_quote( - quote=quote, - swapper_address=user_address, - # suggested_params=suggested_params, - ) - return txn_group - - elif isinstance(pool, TinymanV2Pool) and isinstance( - quote, TinymanV2SwapQuote - ): - txn_group = pool.prepare_swap_transactions_from_quote( - quote=quote, - user_address=user_address, - suggested_params=suggested_params, - ) - return txn_group - else: - raise NotImplementedError() - - elif quote_count == 2: + if quote_count == 2: pools = self.pools swap_type = quotes[0].swap_type tinyman_client = pools[0].client @@ -151,6 +102,8 @@ def prepare_swap_transactions_from_quotes( ) return txn_group + elif quote_count == 1: + raise NotImplementedError("Use prepare_swap_transactions function of the pool directly.") else: raise NotImplementedError() From a495a9cb140877926d03adf98864606f9ee3ebb4 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 1 Mar 2023 18:21:46 +0300 Subject: [PATCH 15/32] fix test and linter errors --- examples/swap_router/swap.py | 31 +++++-- tests/swap_router/test.py | 149 +++------------------------------- tinyman/swap_router/routes.py | 16 +++- 3 files changed, 45 insertions(+), 151 deletions(-) diff --git a/examples/swap_router/swap.py b/examples/swap_router/swap.py index b039fca..ed69be2 100644 --- a/examples/swap_router/swap.py +++ b/examples/swap_router/swap.py @@ -11,7 +11,10 @@ from tinyman.swap_router.routes import Route from tinyman.swap_router.routes import get_best_fixed_input_route from tinyman.swap_router.swap_router import fetch_best_route_suggestion -from tinyman.swap_router.swap_router import get_swap_router_app_opt_in_required_asset_ids, prepare_swap_router_asset_opt_in_transaction +from tinyman.swap_router.swap_router import ( + get_swap_router_app_opt_in_required_asset_ids, + prepare_swap_router_asset_opt_in_transaction, +) from tinyman.v1.client import TinymanClient from tinyman.v1.pools import Pool as TinymanV1Pool from tinyman.v2.client import TinymanV2Client @@ -47,7 +50,9 @@ def fetch_routes( fetch=True, ) if v1_pool.exists and v1_pool.issued_liquidity: - v1_direct_route = Route(asset_in=asset_in, asset_out=asset_out, pools=[v1_pool]) + v1_direct_route = Route( + asset_in=asset_in, asset_out=asset_out, pools=[v1_pool] + ) routes.append(v1_direct_route) v2_pool = TinymanV2Pool( @@ -77,7 +82,9 @@ def swap(asset_in_id, asset_out_id, amount, account): algod = get_algod() # tinyman_v1_client = TinymanTestnetClient(algod_client=algod, user_address=account["address"]) - tinyman_v2_client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) + tinyman_v2_client = TinymanV2TestnetClient( + algod_client=algod, user_address=account["address"] + ) asset_in = tinyman_v2_client.fetch_asset(asset_in_id) asset_out = tinyman_v2_client.fetch_asset(asset_out_id) @@ -89,7 +96,7 @@ def swap(asset_in_id, asset_out_id, amount, account): asset_in=asset_in, asset_out=asset_out, swap_type="fixed-input", - amount=asset_amount_in.amount + amount=asset_amount_in.amount, ) if routes: @@ -101,7 +108,9 @@ def swap(asset_in_id, asset_out_id, amount, account): return print("Best Route:") - best_route = get_best_fixed_input_route(routes=routes, amount_in=asset_amount_in.amount) + best_route = get_best_fixed_input_route( + routes=routes, amount_in=asset_amount_in.amount + ) print(best_route) if best_route: @@ -121,7 +130,9 @@ def swap(asset_in_id, asset_out_id, amount, account): # 1 - Transfer input to swap router app account # 2 - Swap router app call - txn_group = best_route.prepare_swap_router_transactions_from_quotes(quotes=quotes, suggested_params=suggested_params) + txn_group = best_route.prepare_swap_router_transactions_from_quotes( + quotes=quotes, suggested_params=suggested_params + ) # Swap router app account may require to opt in to some assets. opt_in_required_asset_ids = get_swap_router_app_opt_in_required_asset_ids( @@ -134,7 +145,7 @@ def swap(asset_in_id, asset_out_id, amount, account): router_app_id=tinyman_v2_client.router_app_id, asset_ids=opt_in_required_asset_ids, user_address=account["address"], - suggested_params=suggested_params + suggested_params=suggested_params, ) txn_group = opt_in_txn_group + txn_group @@ -159,7 +170,9 @@ def swap(asset_in_id, asset_out_id, amount, account): raise NotImplementedError() # User account may require to opt in to output asset. - if not tinyman_v2_client.asset_is_opted_in(asset_id=asset_out_id, user_address=account["address"]): + if not tinyman_v2_client.asset_is_opted_in( + asset_id=asset_out_id, user_address=account["address"] + ): user_opt_in_txn_group = prepare_asset_optin_transactions( asset_id=asset_out_id, sender=account["address"], @@ -180,7 +193,7 @@ def swap(asset_in_id, asset_out_id, amount, account): ) -if __name__ == '__main__': +if __name__ == "__main__": # TODO: Set Account account = { "address": "ALGORAND_ADDRESS_HERE", diff --git a/tests/swap_router/test.py b/tests/swap_router/test.py index 86d0adc..dd8b43b 100644 --- a/tests/swap_router/test.py +++ b/tests/swap_router/test.py @@ -23,8 +23,6 @@ from tinyman.swap_router.utils import parse_swap_router_event_log from tinyman.v1.client import TinymanClient from tinyman.v1.constants import TESTNET_VALIDATOR_APP_ID -from tinyman.v1.pools import Pool as V1Pool -from tinyman.v1.pools import SwapQuote as V1SwapQuote from tinyman.v2.client import TinymanV2Client from tinyman.v2.constants import TESTNET_VALIDATOR_APP_ID_V2 from tinyman.v2.contracts import get_pool_logicsig as v2_get_pool_logicsig @@ -258,9 +256,8 @@ def test_fixed_input_direct_best_route(self): best_route = get_best_fixed_input_route(routes=routes, amount_in=amount_in) self.assertEqual(best_route, self.direct_route) - last_quote = best_route.get_fixed_input_last_quote( - amount_in=amount_in, slippage=0.05 - ) + quotes = best_route.get_fixed_input_quotes(amount_in=amount_in) + last_quote = quotes[-1] self.assertEqual(last_quote.amount_out.amount, 3323) def test_fixed_input_indirect_best_route(self): @@ -270,9 +267,8 @@ def test_fixed_input_indirect_best_route(self): best_route = get_best_fixed_input_route(routes=routes, amount_in=amount_in) self.assertEqual(best_route, self.indirect_route) - last_quote = best_route.get_fixed_input_last_quote( - amount_in=amount_in, slippage=0.05 - ) + quotes = best_route.get_fixed_input_quotes(amount_in=amount_in) + last_quote = quotes[-1] self.assertEqual(last_quote.amount_out.amount, 2484) def test_fixed_output_direct_best_route(self): @@ -291,9 +287,8 @@ def test_fixed_output_direct_best_route(self): best_route = get_best_fixed_output_route(routes=routes, amount_out=amount_out) self.assertEqual(best_route, self.direct_route) - first_quote = best_route.get_fixed_output_first_quote( - amount_out=amount_out, slippage=0.05 - ) + quotes = best_route.get_fixed_output_quotes(amount_out=amount_out) + first_quote = quotes[0] self.assertEqual(first_quote.amount_in.amount, 30091) self.assertEqual(first_quote.amount_out.amount, amount_out) @@ -304,9 +299,8 @@ def test_fixed_output_indirect_best_route(self): best_route = get_best_fixed_output_route(routes=routes, amount_out=amount_out) self.assertEqual(best_route, self.indirect_route) - first_quote = best_route.get_fixed_output_first_quote( - amount_out=amount_out, slippage=0.05 - ) + quotes = best_route.get_fixed_output_quotes(amount_out=amount_out) + first_quote = quotes[0] self.assertEqual(first_quote.amount_in.amount, 40243) @@ -509,7 +503,7 @@ def test_algo_input_prepare_swap_router_transactions(self): ) self.assertEqual(txn_group.transactions[1].dictify()["type"], "appl") - def test_indirect_route_prepare_swap_transactions_from_quotes(self): + def test_indirect_route_prepare_swap_router_transactions_from_quotes(self): sp = self.get_suggested_params() user_private_key, user_address = generate_account() router_app_address = get_application_address(self.ROUTER_APP_ID) @@ -542,19 +536,6 @@ def test_indirect_route_prepare_swap_transactions_from_quotes(self): ) quotes = [quote_1, quote_2] - opt_in_app_call_txn = { - "apaa": [b"asset_opt_in"], - "apan": OnComplete.NoOpOC, - "apas": ANY, - "apid": self.ROUTER_APP_ID, - "fee": ANY, - "fv": ANY, - "gh": ANY, - "grp": ANY, - "lv": ANY, - "snd": decode_address(user_address), - "type": "appl", - } transfer_input_txn = { "aamt": asset_in.amount, "arcv": b"\xd4\xb4\xce\xaa\xc35V\xffg\xfa\xae\xcbz\xd0\x8a\xb3\x8f\x85\x1a\x9e\x06b\x8a\xf4X:\x0b\xae[\x93i\xde", @@ -584,9 +565,10 @@ def test_indirect_route_prepare_swap_transactions_from_quotes(self): "lv": ANY, "snd": decode_address(user_address), "type": "appl", + "note": b'tinyman/v2:j{"origin":"tinyman-py-sdk"}', } - txn_group = route.prepare_swap_transactions_from_quotes( + txn_group = route.prepare_swap_router_transactions_from_quotes( quotes=quotes, user_address=user_address, suggested_params=sp, @@ -599,115 +581,6 @@ def test_indirect_route_prepare_swap_transactions_from_quotes(self): dict(txn_group.transactions[1].dictify()), swap_app_call_txn ) - def test_direct_route_prepare_swap_transactions_from_quotes(self): - sp = self.get_suggested_params() - user_private_key, user_address = generate_account() - asset_in = AssetAmount(self.asset_in, 1_000_000) - asset_out = AssetAmount(self.asset_out, 2_000_000) - swap_type = "fixed-input" - - route = Route( - asset_in=self.asset_in, asset_out=self.asset_out, pools=[self.pool] - ) - - quote = V2SwapQuote( - swap_type=swap_type, - amount_in=asset_in, - amount_out=asset_out, - swap_fees=None, - slippage=0.05, - price_impact=None, - ) - quotes = [quote] - - txn_group = route.prepare_swap_transactions_from_quotes( - quotes=quotes, - user_address=user_address, - suggested_params=sp, - ) - self.assertEqual(len(txn_group.transactions), 2) - self.assertDictEqual( - dict(txn_group.transactions[0].dictify()), - { - "aamt": asset_in.amount, - "arcv": decode_address(self.pool.address), - "fee": ANY, - "fv": ANY, - "gh": ANY, - "grp": ANY, - "lv": ANY, - "snd": decode_address(user_address), - "type": "axfer", - "xaid": self.asset_in.id, - }, - ) - self.assertDictEqual( - dict(txn_group.transactions[1].dictify()), - { - "apaa": [ - b"swap", - b"fixed-input", - quote.amount_out_with_slippage.amount.to_bytes(8, "big"), - ], - "apan": OnComplete.NoOpOC, - "apas": [self.asset_out.id, self.asset_in.id], - "apat": [decode_address(self.pool.address)], - "apid": self.VALIDATOR_APP_ID_V2, - "fee": ANY, - "fv": ANY, - "gh": ANY, - "grp": ANY, - "lv": ANY, - "note": ANY, - "snd": decode_address(user_address), - "type": "appl", - }, - ) - - def test_v1_direct_route_prepare_swap_transactions_from_quotes(self): - sp = self.get_suggested_params() - user_private_key, user_address = generate_account() - asset_in = AssetAmount(self.asset_in, 1_000_000) - asset_out = AssetAmount(self.asset_out, 2_000_000) - swap_type = "fixed-input" - - v1_pool = V1Pool( - client=self.get_tinyman_v1_client(), - asset_a=self.asset_in, - asset_b=self.asset_out, - validator_app_id=self.VALIDATOR_APP_ID_V1, - fetch=False, - ) - v1_pool.exists = True - v1_pool.liquidity_asset = Asset(id=99, name="TM", unit_name="TM", decimals=6) - - route = Route(asset_in=self.asset_in, asset_out=self.asset_out, pools=[v1_pool]) - - quote = V1SwapQuote( - swap_type=swap_type, - amount_in=asset_in, - amount_out=asset_out, - swap_fees=None, - slippage=0.05, - price_impact=None, - ) - quotes = [quote] - - with patch( - "algosdk.v2client.algod.AlgodClient.suggested_params", - return_value=self.get_suggested_params(), - ): - txn_group = route.prepare_swap_transactions_from_quotes( - quotes=quotes, - user_address=user_address, - suggested_params=sp, - ) - - self.assertEqual(len(txn_group.transactions), 4) - self.assertEqual( - dict(txn_group.transactions[1].dictify())["apid"], self.VALIDATOR_APP_ID_V1 - ) - def test_swap_route_opt_in(self): sp = self.get_suggested_params() user_private_key, user_address = generate_account() diff --git a/tinyman/swap_router/routes.py b/tinyman/swap_router/routes.py index 2e4245c..07dae95 100644 --- a/tinyman/swap_router/routes.py +++ b/tinyman/swap_router/routes.py @@ -103,7 +103,9 @@ def prepare_swap_router_transactions_from_quotes( return txn_group elif quote_count == 1: - raise NotImplementedError("Use prepare_swap_transactions function of the pool directly.") + raise NotImplementedError( + "Use prepare_swap_transactions function of the pool directly." + ) else: raise NotImplementedError() @@ -159,7 +161,9 @@ def price(self): return math.prod(pool_prices) @classmethod - def get_swap_price_from_quotes(cls, quotes, asset_in_algo_price: Optional[int] = None): + def get_swap_price_from_quotes( + cls, quotes, asset_in_algo_price: Optional[int] = None + ): amount_in = quotes[0].amount_in.amount amount_out = quotes[-1].amount_out.amount @@ -196,7 +200,9 @@ def get_transaction_count(cls, quotes) -> int: return transaction_count -def get_best_fixed_input_route(routes: list[Route], amount_in: int, asset_in_algo_price: Optional[float] = None) -> Optional[Route]: +def get_best_fixed_input_route( + routes: list[Route], amount_in: int, asset_in_algo_price: Optional[float] = None +) -> Optional[Route]: best_route = None best_route_price_impact = None best_route_swap_price = None @@ -221,7 +227,9 @@ def get_best_fixed_input_route(routes: list[Route], amount_in: int, asset_in_alg return best_route -def get_best_fixed_output_route(routes: list[Route], amount_out: int, asset_in_algo_price: Optional[float] = None): +def get_best_fixed_output_route( + routes: list[Route], amount_out: int, asset_in_algo_price: Optional[float] = None +): best_route = None best_route_price_impact = None best_route_swap_price = None From 4747b43840ecbe99504a305b17c5df707765f082 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 1 Mar 2023 18:43:32 +0300 Subject: [PATCH 16/32] improve api request and error handling --- tinyman/swap_router/swap_router.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/tinyman/swap_router/swap_router.py b/tinyman/swap_router/swap_router.py index 69c30f1..6e40e86 100644 --- a/tinyman/swap_router/swap_router.py +++ b/tinyman/swap_router/swap_router.py @@ -1,8 +1,8 @@ from typing import Optional +import requests from algosdk.logic import get_application_address from algosdk.v2client.algod import AlgodClient -from requests import request, HTTPError from tinyman.assets import Asset from tinyman.compat import ( @@ -148,16 +148,11 @@ def fetch_best_route_suggestion( "amount": str(amount), } - raw_response = request( - method="POST", - url=tinyman_client.api_base_url + "v1/swap-router/quotes/", - json=payload, + r = requests.post( + tinyman_client.api_base_url + "v1/swap-router/quotes/", json=payload ) - - if raw_response.status_code != 200: - raise HTTPError(response=raw_response) - - response = raw_response.json() + r.raise_for_status() + response = r.json() pools = [] for quote in response["route"]: From 4042c84983a7863bce06b7e002fdae17f0d00972 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Mon, 6 Mar 2023 14:22:30 +0300 Subject: [PATCH 17/32] add low swap amount error --- tinyman/exceptions.py | 4 ++++ tinyman/swap_router/routes.py | 6 +++--- tinyman/v2/pools.py | 9 ++++++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tinyman/exceptions.py b/tinyman/exceptions.py index 8825ab4..31a17bf 100644 --- a/tinyman/exceptions.py +++ b/tinyman/exceptions.py @@ -16,3 +16,7 @@ class PoolAlreadyInitialized(Exception): class InsufficientReserves(Exception): pass + + +class LowSwapAmountError(Exception): + pass diff --git a/tinyman/swap_router/routes.py b/tinyman/swap_router/routes.py index 07dae95..9fa12a5 100644 --- a/tinyman/swap_router/routes.py +++ b/tinyman/swap_router/routes.py @@ -7,7 +7,7 @@ from tinyman.assets import Asset, AssetAmount from tinyman.compat import SuggestedParams -from tinyman.exceptions import PoolHasNoLiquidity, InsufficientReserves +from tinyman.exceptions import PoolHasNoLiquidity, InsufficientReserves, LowSwapAmountError from tinyman.utils import TransactionGroup from tinyman.v1.pools import Pool as TinymanV1Pool from tinyman.v2.pools import Pool as TinymanV2Pool @@ -210,7 +210,7 @@ def get_best_fixed_input_route( for route in routes: try: quotes = route.get_fixed_input_quotes(amount_in=amount_in) - except (InsufficientReserves, PoolHasNoLiquidity): + except (InsufficientReserves, LowSwapAmountError, PoolHasNoLiquidity): continue swap_price = route.get_swap_price_from_quotes(quotes, asset_in_algo_price) @@ -237,7 +237,7 @@ def get_best_fixed_output_route( for route in routes: try: quotes = route.get_fixed_output_quotes(amount_out=amount_out) - except (InsufficientReserves, PoolHasNoLiquidity): + except (InsufficientReserves, LowSwapAmountError, PoolHasNoLiquidity): continue swap_price = route.get_swap_price_from_quotes(quotes, asset_in_algo_price) diff --git a/tinyman/v2/pools.py b/tinyman/v2/pools.py index f5cfe0e..813fdb4 100644 --- a/tinyman/v2/pools.py +++ b/tinyman/v2/pools.py @@ -1,9 +1,10 @@ from typing import Optional -from tinyman.compat import LogicSigAccount, Transaction, SuggestedParams from algosdk.v2client.algod import AlgodClient from tinyman.assets import Asset, AssetAmount +from tinyman.compat import LogicSigAccount, Transaction, SuggestedParams +from tinyman.exceptions import LowSwapAmountError from tinyman.optin import prepare_asset_optin_transactions from tinyman.utils import TransactionGroup from .add_liquidity import ( @@ -913,6 +914,9 @@ def fetch_fixed_input_swap_quote( ) amount_out = AssetAmount(asset_out, swap_output_amount) + if not total_fee_amount: + raise LowSwapAmountError() + quote = SwapQuote( swap_type="fixed-input", amount_in=amount_in, @@ -954,6 +958,9 @@ def fetch_fixed_output_swap_quote( ) amount_in = AssetAmount(asset_in, swap_input_amount) + if not total_fee_amount: + raise LowSwapAmountError() + quote = SwapQuote( swap_type="fixed-output", amount_out=amount_out, From 50bda403436668c9b1300924daf379f3583ad086 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Fri, 24 Mar 2023 12:19:44 +0300 Subject: [PATCH 18/32] fix swap router transaction counts --- tinyman/swap_router/routes.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/tinyman/swap_router/routes.py b/tinyman/swap_router/routes.py index 9fa12a5..a1be2a3 100644 --- a/tinyman/swap_router/routes.py +++ b/tinyman/swap_router/routes.py @@ -9,6 +9,7 @@ from tinyman.compat import SuggestedParams from tinyman.exceptions import PoolHasNoLiquidity, InsufficientReserves, LowSwapAmountError from tinyman.utils import TransactionGroup +from tinyman.swap_router.constants import FIXED_INPUT_SWAP_TYPE, FIXED_OUTPUT_SWAP_TYPE from tinyman.v1.pools import Pool as TinymanV1Pool from tinyman.v2.pools import Pool as TinymanV2Pool from tinyman.v2.quotes import SwapQuote as TinymanV2SwapQuote @@ -185,18 +186,22 @@ def get_price_impact_from_quotes(self, quotes): @classmethod def get_transaction_count(cls, quotes) -> int: - if len(quotes) == 2: - transaction_count = 10 - elif len(quotes) == 1: - if quotes[0].swap_type == "fixed-input": - transaction_count = 3 - elif quotes[0].swap_type == "fixed-output": - transaction_count = 4 - else: - raise NotImplementedError() - else: - raise NotImplementedError() - + transaction_count_mapping = { + # Single Swap + 1: { + FIXED_INPUT_SWAP_TYPE: 3, + FIXED_OUTPUT_SWAP_TYPE: 4, + }, + # Swap Router (1 hop) + 2: { + FIXED_INPUT_SWAP_TYPE: 8, + FIXED_OUTPUT_SWAP_TYPE: 9, + } + } + + swap_count = len(quotes) + swap_type = quotes[0].swap_type + transaction_count = transaction_count_mapping[swap_count][swap_type] return transaction_count From 50dd7b22994c94f201ef517ab8e9fb691b38a75b Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Tue, 28 Mar 2023 13:23:57 +0300 Subject: [PATCH 19/32] add temp mainnet app id --- tinyman/swap_router/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tinyman/swap_router/constants.py b/tinyman/swap_router/constants.py index 64af07c..a9fc3a9 100644 --- a/tinyman/swap_router/constants.py +++ b/tinyman/swap_router/constants.py @@ -1,5 +1,5 @@ TESTNET_SWAP_ROUTER_APP_ID_V1 = 159521633 # TODO: temp app for testing. -MAINNET_SWAP_ROUTER_APP_ID_V1 = 0 # TODO +MAINNET_SWAP_ROUTER_APP_ID_V1 = 1071281873 # TODO: temp app for testing. FIXED_INPUT_SWAP_TYPE = "fixed-input" FIXED_OUTPUT_SWAP_TYPE = "fixed-output" From b1f08367edc40fe962c1a12b059e53532f6300e8 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 6 Apr 2023 18:53:14 +0300 Subject: [PATCH 20/32] remove misleading insufficient reserves error --- tinyman/v2/formulas.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tinyman/v2/formulas.py b/tinyman/v2/formulas.py index 9b10f76..9dcb0fe 100644 --- a/tinyman/v2/formulas.py +++ b/tinyman/v2/formulas.py @@ -2,7 +2,7 @@ from tinyman.utils import calculate_price_impact from tinyman.v2.constants import LOCKED_POOL_TOKENS -from tinyman.exceptions import InsufficientReserves +from tinyman.exceptions import InsufficientReserves, LowSwapAmountError def calculate_protocol_fee_amount( @@ -239,7 +239,9 @@ def calculate_output_amount_of_fixed_input_swap( ) -> int: k = input_supply * output_supply output_amount = output_supply - int(k / (input_supply + swap_amount)) - output_amount -= 1 + + # On-chain app raises an error if output_amount is less than zero. + output_amount = max(output_amount, 0) return output_amount @@ -257,6 +259,9 @@ def calculate_swap_amount_of_fixed_output_swap( def calculate_fixed_input_swap( input_supply: int, output_supply: int, swap_input_amount: int, total_fee_share: int ) -> (int, int, int, float): + if not swap_input_amount: + raise LowSwapAmountError() + total_fee_amount = calculate_fixed_input_fee_amount( input_amount=swap_input_amount, total_fee_share=total_fee_share ) @@ -265,9 +270,6 @@ def calculate_fixed_input_swap( input_supply, output_supply, swap_amount ) - if swap_output_amount <= 0: - raise InsufficientReserves() - price_impact = calculate_price_impact( input_supply=input_supply, output_supply=output_supply, From 0f3f0f5c5c33b68dd23899872b4c344b39611e68 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Fri, 7 Apr 2023 12:50:28 +0300 Subject: [PATCH 21/32] fix fixed-input swap calculation --- tinyman/utils.py | 2 +- tinyman/v2/formulas.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tinyman/utils.py b/tinyman/utils.py index 576d766..1296966 100644 --- a/tinyman/utils.py +++ b/tinyman/utils.py @@ -114,7 +114,7 @@ def calculate_price_impact( ): swap_price = swap_output_amount / swap_input_amount pool_price = output_supply / input_supply - price_impact = abs(round(1 - (swap_price / pool_price), 5)) + price_impact = round(1 - (swap_price / pool_price), 5) return price_impact diff --git a/tinyman/v2/formulas.py b/tinyman/v2/formulas.py index 9dcb0fe..48530cd 100644 --- a/tinyman/v2/formulas.py +++ b/tinyman/v2/formulas.py @@ -239,6 +239,7 @@ def calculate_output_amount_of_fixed_input_swap( ) -> int: k = input_supply * output_supply output_amount = output_supply - int(k / (input_supply + swap_amount)) + output_amount -= 1 # On-chain app raises an error if output_amount is less than zero. output_amount = max(output_amount, 0) From 7cdb2d655cce6d90252c67005a737fdaaebd92e3 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Tue, 11 Apr 2023 16:28:54 +0300 Subject: [PATCH 22/32] add swap router management txns --- tinyman/swap_router/constants.py | 8 ++++ tinyman/swap_router/management.py | 76 ++++++++++++++++++++++++++++++ tinyman/swap_router/swap_router.py | 6 ++- 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 tinyman/swap_router/management.py diff --git a/tinyman/swap_router/constants.py b/tinyman/swap_router/constants.py index a9fc3a9..1f29126 100644 --- a/tinyman/swap_router/constants.py +++ b/tinyman/swap_router/constants.py @@ -4,5 +4,13 @@ FIXED_INPUT_SWAP_TYPE = "fixed-input" FIXED_OUTPUT_SWAP_TYPE = "fixed-output" +SWAP_APP_ARGUMENT = b"swap" +FIXED_INPUT_APP_ARGUMENT = b"fixed-input" +FIXED_OUTPUT_APP_ARGUMENT = b"fixed-output" +ASSET_OPT_IN_APP_ARGUMENT = b"asset_opt_in" +CLAIM_EXTRA_APP_ARGUMENT = b"claim_extra" +SET_MANAGER_APP_ARGUMENT = b"set_manager" +SET_EXTRA_COLLECTOR_APP_ARGUMENT = b"set_extra_collector" + # Event Log Selectors SWAP_EVENT_LOG_SELECTOR = b"\x81b\xda\x9e" # "swap(uint64,uint64,uint64,uint64)" diff --git a/tinyman/swap_router/management.py b/tinyman/swap_router/management.py new file mode 100644 index 0000000..e13c131 --- /dev/null +++ b/tinyman/swap_router/management.py @@ -0,0 +1,76 @@ +from typing import Optional + +from tinyman.compat import ( + ApplicationNoOpTxn, + SuggestedParams, +) +from tinyman.swap_router.constants import CLAIM_EXTRA_APP_ARGUMENT, SET_MANAGER_APP_ARGUMENT, SET_EXTRA_COLLECTOR_APP_ARGUMENT +from tinyman.utils import TransactionGroup + + +def prepare_claim_extra_transactions( + router_app_id: int, + asset_ids: [int], + sender: str, + suggested_params: SuggestedParams, + app_call_note: Optional[str] = None, +) -> TransactionGroup: + + claim_extra_app_call = ApplicationNoOpTxn( + sender=sender, + sp=suggested_params, + index=router_app_id, + app_args=[CLAIM_EXTRA_APP_ARGUMENT], + foreign_assets=asset_ids, + note=app_call_note + ) + min_fee = suggested_params.min_fee + inner_transaction_count = len(asset_ids) + claim_extra_app_call.fee = min_fee * (1 + inner_transaction_count) + + txn_group = TransactionGroup([claim_extra_app_call]) + return txn_group + + +def prepare_set_set_manager_transactions( + router_app_id: int, + manager: str, + new_manager: str, + suggested_params: SuggestedParams, + app_call_note: Optional[str] = None, +) -> TransactionGroup: + txns = [ + ApplicationNoOpTxn( + sender=manager, + sp=suggested_params, + index=router_app_id, + app_args=[SET_MANAGER_APP_ARGUMENT], + accounts=[new_manager], + note=app_call_note, + ), + ] + txn_group = TransactionGroup(txns) + return txn_group + + +def prepare_set_extra_collector_transactions( + router_app_id: int, + manager: str, + new_extra_collector: str, + suggested_params: SuggestedParams, + app_call_note: Optional[str] = None, +) -> TransactionGroup: + txns = [ + ApplicationNoOpTxn( + sender=manager, + sp=suggested_params, + index=router_app_id, + app_args=[SET_EXTRA_COLLECTOR_APP_ARGUMENT], + accounts=[new_extra_collector], + note=app_call_note, + ), + ] + txn_group = TransactionGroup(txns) + return txn_group + + diff --git a/tinyman/swap_router/swap_router.py b/tinyman/swap_router/swap_router.py index 6e40e86..c3ab228 100644 --- a/tinyman/swap_router/swap_router.py +++ b/tinyman/swap_router/swap_router.py @@ -14,6 +14,8 @@ from tinyman.swap_router.constants import ( FIXED_INPUT_SWAP_TYPE, FIXED_OUTPUT_SWAP_TYPE, + SWAP_APP_ARGUMENT, + ASSET_OPT_IN_APP_ARGUMENT, ) from tinyman.swap_router.routes import Route from tinyman.utils import TransactionGroup @@ -34,7 +36,7 @@ def prepare_swap_router_asset_opt_in_transaction( sender=user_address, sp=suggested_params, index=router_app_id, - app_args=["asset_opt_in"], + app_args=[ASSET_OPT_IN_APP_ARGUMENT], foreign_assets=asset_ids, ) min_fee = suggested_params.min_fee @@ -87,7 +89,7 @@ def prepare_swap_router_transactions( sender=user_address, sp=suggested_params, index=router_app_id, - app_args=["swap", swap_type, asset_out_amount], + app_args=[SWAP_APP_ARGUMENT, swap_type, asset_out_amount], accounts=[pool_1_address, pool_2_address], foreign_apps=[validator_app_id], foreign_assets=[input_asset_id, intermediary_asset_id, output_asset_id], From c4aa676ea4d2feca5212af8dc8fe1a8db81e0b4c Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Tue, 11 Apr 2023 18:41:55 +0300 Subject: [PATCH 23/32] add swap router testnet app id --- tinyman/swap_router/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tinyman/swap_router/constants.py b/tinyman/swap_router/constants.py index 1f29126..cd43a75 100644 --- a/tinyman/swap_router/constants.py +++ b/tinyman/swap_router/constants.py @@ -1,4 +1,4 @@ -TESTNET_SWAP_ROUTER_APP_ID_V1 = 159521633 # TODO: temp app for testing. +TESTNET_SWAP_ROUTER_APP_ID_V1 = 184778019 MAINNET_SWAP_ROUTER_APP_ID_V1 = 1071281873 # TODO: temp app for testing. FIXED_INPUT_SWAP_TYPE = "fixed-input" From a65784d63d40120ff672137f3fbd2592fd28d985 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Mon, 6 Mar 2023 12:36:00 +0300 Subject: [PATCH 24/32] add swap router error mapping --- tinyman/v2/client.py | 8 +++-- tinyman/v2/swap_router_approval.map.json | 37 ++++++++++++++++++++++++ tinyman/v2/utils.py | 30 +++++++++++++++++-- 3 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 tinyman/v2/swap_router_approval.map.json diff --git a/tinyman/v2/client.py b/tinyman/v2/client.py index 2516863..247ff7f 100644 --- a/tinyman/v2/client.py +++ b/tinyman/v2/client.py @@ -17,7 +17,7 @@ TESTNET_VALIDATOR_APP_ID, MAINNET_VALIDATOR_APP_ID, ) -from tinyman.v2.utils import lookup_error +from tinyman.v2.utils import lookup_error, get_tealishmap class TinymanV2Client(BaseTinymanClient): @@ -34,9 +34,11 @@ def handle_error(self, exception, txn_group): error = parse_error(exception) if isinstance(error, LogicError): app_id = find_app_id_from_txn_id(txn_group, error.txn_id) - if app_id in (TESTNET_VALIDATOR_APP_ID, MAINNET_VALIDATOR_APP_ID): + tealishmap = get_tealishmap(app_id) + if tealishmap: error.app_id = app_id - error.message = lookup_error(error.pc, error.message) + error.message = lookup_error(error.pc, error.message, tealishmap) + raise error from None diff --git a/tinyman/v2/swap_router_approval.map.json b/tinyman/v2/swap_router_approval.map.json new file mode 100644 index 0000000..1414893 --- /dev/null +++ b/tinyman/v2/swap_router_approval.map.json @@ -0,0 +1,37 @@ +{ + "pc_teal": [0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 5, 6, 6, 6, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 12, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 16, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 20, 22, 22, 23, 27, 27, 28, 28, 29, 30, 30, 30, 31, 31, 32, 32, 33, 34, 34, 34, 35, 35, 36, 36, 37, 38, 38, 38, 39, 39, 40, 40, 41, 42, 42, 42, 43, 43, 44, 44, 45, 46, 46, 46, 47, 52, 52, 53, 58, 58, 58, 59, 59, 59, 59, 59, 59, 60, 61, 61, 61, 62, 62, 62, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 64, 65, 65, 65, 66, 66, 66, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 68, 69, 69, 69, 70, 70, 70, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 72, 73, 73, 73, 74, 74, 74, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 76, 77, 77, 77, 78, 86, 86, 87, 87, 89, 89, 90, 90, 92, 92, 93, 93, 94, 95, 95, 95, 97, 97, 98, 98, 99, 99, 99, 100, 100, 101, 101, 102, 103, 103, 104, 104, 104, 107, 107, 108, 114, 114, 114, 114, 114, 114, 114, 114, 114, 114, 114, 114, 114, 114, 114, 114, 115, 116, 116, 116, 117, 118, 121, 121, 122, 122, 124, 124, 124, 125, 125, 127, 127, 127, 128, 128, 132, 132, 132, 133, 133, 135, 135, 135, 136, 136, 138, 138, 138, 139, 139, 144, 144, 145, 145, 145, 148, 148, 149, 149, 150, 150, 151, 151, 152, 154, 154, 155, 158, 158, 159, 159, 159, 162, 162, 163, 163, 164, 164, 165, 165, 166, 168, 168, 169, 175, 175, 176, 178, 178, 179, 179, 180, 181, 181, 183, 183, 184, 184, 185, 185, 186, 187, 190, 190, 191, 191, 192, 192, 193, 194, 194, 194, 197, 197, 198, 198, 199, 199, 200, 201, 203, 203, 204, 205, 207, 207, 208, 208, 209, 209, 210, 210, 210, 213, 213, 214, 214, 215, 215, 216, 217, 217, 217, 219, 219, 220, 220, 221, 221, 222, 223, 225, 225, 226, 226, 227, 227, 228, 229, 231, 231, 232, 232, 233, 233, 234, 234, 234, 238, 241, 241, 242, 246, 246, 246, 247, 247, 247, 247, 247, 247, 247, 247, 247, 247, 247, 247, 247, 248, 249, 249, 249, 250, 250, 250, 251, 251, 251, 251, 251, 251, 251, 251, 251, 251, 251, 251, 251, 251, 252, 253, 253, 253, 254, 259, 259, 259, 260, 261, 261, 263, 263, 264, 264, 270, 270, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 271, 272, 272, 273, 273, 274, 274, 275, 275, 276, 276, 276, 277, 277, 278, 280, 280, 281, 286, 286, 287, 287, 287, 287, 287, 287, 287, 287, 287, 287, 287, 287, 287, 288, 288, 289, 289, 290, 290, 291, 291, 292, 292, 292, 293, 293, 294, 296, 296, 297, 297, 298, 299, 303, 303, 304, 304, 305, 305, 306, 306, 307, 307, 307, 310, 311, 311, 312, 313, 314, 314, 315, 316, 317, 317, 318, 319, 320, 320, 321, 322, 323, 325, 325, 326, 331, 331, 331, 332, 333, 333, 337, 337, 338, 338, 339, 339, 339, 340, 340, 342, 342, 343, 343, 344, 344, 344, 345, 345, 347, 347, 348, 348, 349, 349, 349, 350, 350, 352, 352, 353, 353, 354, 354, 354, 355, 355, 364, 364, 365, 365, 366, 366, 367, 367, 367, 368, 368, 370, 370, 371, 371, 372, 372, 372, 373, 373, 375, 375, 376, 376, 377, 378, 378, 382, 382, 383, 383, 384, 384, 385, 385, 385, 386, 386, 388, 388, 389, 389, 390, 390, 390, 391, 391, 393, 393, 394, 394, 395, 396, 396, 402, 402, 403, 403, 403, 403, 403, 403, 403, 403, 403, 403, 403, 403, 403, 403, 404, 404, 405, 405, 406, 406, 407, 407, 408, 408, 408, 409, 409, 410, 410, 412, 412, 413, 413, 414, 415, 417, 417, 418, 419, 425, 425, 426, 426, 426, 426, 426, 426, 426, 426, 426, 426, 426, 426, 426, 426, 427, 427, 428, 428, 429, 429, 430, 430, 431, 431, 431, 432, 432, 433, 433, 435, 435, 436, 436, 437, 438, 440, 440, 441, 442, 446, 446, 447, 447, 448, 449, 449, 451, 451, 452, 452, 452, 455, 455, 456, 456, 457, 457, 458, 458, 459, 459, 459, 464, 464, 465, 465, 466, 466, 467, 467, 468, 468, 468, 471, 472, 472, 473, 474, 475, 475, 476, 477, 478, 478, 479, 479, 480, 481, 482, 483, 483, 484, 485, 486, 488, 488, 489, 491, 491, 492, 500, 500, 501, 501, 501, 501, 501, 501, 501, 501, 501, 502, 503, 504, 508, 508, 508, 508, 508, 508, 508, 508, 508, 509, 509, 509, 510, 513, 513, 514, 522, 522, 523, 523, 523, 523, 523, 523, 523, 523, 523, 524, 525, 526, 530, 530, 530, 530, 530, 530, 530, 530, 530, 530, 530, 530, 530, 530, 530, 530, 530, 531, 531, 531, 532, 535, 535, 536, 544, 544, 545, 545, 546, 547, 552, 552, 553, 553, 556, 556, 557, 557, 559, 559, 560, 560, 561, 562, 562, 562, 564, 564, 565, 565, 566, 566, 568, 568, 569, 569, 570, 570, 570, 571, 571, 573, 573, 574, 574, 574, 577, 577, 578, 578, 579, 579, 580, 580, 580, 580, 580, 580, 580, 580, 580, 580, 580, 580, 580, 580, 580, 580, 580, 581, 582, 582, 582, 584, 584, 585, 585, 586, 587, 587, 588, 588, 588, 591, 591, 592, 594, 594, 595, 599, 599, 600, 600, 601, 601, 602, 602, 603, 603, 604, 604, 606, 606, 607, 607, 608, 608, 608, 609, 609, 611, 611, 612, 612, 613, 613, 613, 614, 614, 617, 617, 618, 618, 618, 621, 624, 624, 625, 625, 627, 627, 628, 628, 630, 630, 631, 631, 633, 633, 634, 634, 636, 636, 637, 637, 640, 642, 642, 643, 643, 645, 645, 646, 646, 648, 648, 648, 648, 648, 648, 648, 648, 648, 648, 648, 648, 648, 648, 648, 648, 649, 650, 650, 652, 652, 652, 652, 652, 652, 653, 653, 655, 655, 656, 656, 658, 658, 659, 660, 660, 662, 662, 663, 663, 665, 665, 666, 666, 668, 668, 669, 669, 671, 671, 672, 672, 674, 676, 676, 676, 680, 683, 683, 684, 684, 686, 686, 687, 687, 689, 689, 690, 690, 692, 692, 693, 693, 696, 698, 698, 699, 699, 701, 701, 702, 702, 704, 704, 704, 704, 704, 704, 704, 704, 704, 704, 704, 704, 704, 704, 704, 704, 705, 706, 706, 708, 708, 708, 708, 708, 708, 709, 709, 711, 711, 712, 712, 714, 714, 715, 716, 716, 718, 718, 719, 719, 721, 721, 722, 722, 724, 724, 725, 725, 727, 727, 728, 728, 730, 735, 735, 736, 736, 737, 737, 737, 738, 738, 740, 740, 741, 741, 742, 742, 742, 743, 743, 745, 745, 746, 746, 747, 748, 748, 750, 750, 751, 751, 752, 752, 753, 754, 755, 755, 757, 757, 758, 758, 759, 763, 763, 764, 764, 770, 770, 770, 770, 770, 770, 770, 770, 770, 770, 770, 770, 770, 770, 770, 770, 771, 772, 772, 775, 775, 776, 776, 777, 777, 777, 777, 777, 777, 777, 777, 777, 777, 777, 777, 778, 779, 779, 780, 780, 782, 782, 783, 785, 785, 786, 786, 787, 788, 788, 788, 791, 791, 792, 792, 793, 793, 793, 793, 793, 793, 793, 793, 793, 793, 793, 793, 793, 793, 793, 793, 793, 793, 794, 795, 796, 796, 798, 798, 799, 803, 803, 804, 804, 805, 805, 805, 805, 805, 805, 805, 805, 805, 805, 805, 805, 806, 807, 808, 808, 810, 810, 811, 811, 812, 813, 813, 813, 816, 816, 817, 817, 818, 818, 818, 818, 818, 818, 818, 818, 818, 818, 818, 818, 818, 818, 818, 818, 818, 818, 819, 820, 821, 821, 823, 823, 824, 828, 830, 834, 834, 836, 836, 837, 837, 837, 841, 841, 842, 842, 843, 843, 844, 844, 845, 848, 848, 849, 849, 850, 851, 851, 851, 854, 854, 855, 855, 856, 856, 857, 857, 858, 858, 858, 862, 866, 866, 867, 867, 873, 873, 874, 874, 876, 876, 877, 878, 878, 878, 881, 881, 882, 883, 883, 884, 885, 886, 886, 887, 887, 887, 891, 891, 892, 892, 893, 893, 894, 895, 895, 898, 898, 899, 903, 903, 904, 904, 905, 905, 913, 913, 914, 915, 915, 916, 917, 918, 918, 921, 921, 922, 922, 923, 923, 924, 925, 926, 927, 928, 928, 929, 930, 930, 931, 932, 932, 934, 934, 935, 939, 939, 940, 940, 944, 944, 944, 944, 944, 944, 944, 944, 944, 944, 944, 944, 944, 944, 944, 944, 945, 946, 946, 949, 949, 950, 950, 951, 951, 951, 951, 951, 951, 951, 951, 951, 951, 951, 951, 951, 951, 951, 951, 951, 952, 953, 953, 954, 954, 956, 956, 957, 960, 960, 961, 961, 961, 962, 963, 963, 963, 964, 964, 965, 966, 967, 967, 969, 969, 970, 970, 971, 972, 972, 974, 974, 975, 979, 979, 980, 980, 981, 981, 982, 982, 988, 988, 989, 990, 990, 990, 993, 995, 995, 996, 996, 998, 998, 999, 999, 1001, 1001, 1002, 1002, 1004, 1004, 1005, 1005, 1007, 1007, 1008, 1008, 1009, 1011, 1011, 1011, 1015, 1017, 1017, 1018, 1018, 1020, 1020, 1021, 1021, 1023, 1023, 1024, 1024, 1026, 1026, 1027, 1027, 1029, 1029, 1030, 1030, 1032, 1032, 1033, 1033, 1034, 1038], + "teal_tealish": [0, 1, 2, 3, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 4, 11, 12, 12, 13, 12, 12, 12, 14, 12, 12, 12, 15, 12, 12, 12, 16, 12, 12, 12, 17, 12, 12, 12, 19, 20, 20, 21, 21, 21, 23, 24, 24, 25, 25, 26, 25, 25, 25, 27, 25, 25, 25, 28, 25, 25, 25, 29, 25, 25, 25, 30, 25, 25, 25, 32, 33, 33, 34, 35, 36, 37, 37, 37, 38, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 39, 38, 38, 38, 38, 38, 38, 41, 41, 41, 43, 44, 44, 45, 46, 46, 46, 46, 46, 46, 47, 48, 48, 48, 49, 49, 49, 50, 50, 50, 51, 52, 53, 53, 53, 54, 54, 54, 55, 55, 55, 56, 57, 58, 59, 59, 59, 59, 60, 60, 60, 60, 60, 60, 61, 61, 61, 59, 63, 63, 63, 63, 64, 64, 64, 64, 64, 64, 65, 65, 65, 63, 67, 68, 69, 70, 70, 70, 71, 71, 71, 71, 71, 72, 72, 72, 72, 72, 72, 73, 74, 74, 74, 74, 74, 74, 74, 75, 75, 75, 75, 75, 75, 76, 76, 76, 76, 77, 77, 77, 77, 74, 74, 78, 78, 78, 78, 78, 78, 79, 79, 79, 79, 79, 79, 80, 80, 80, 80, 80, 80, 81, 81, 81, 81, 74, 74, 82, 83, 83, 74, 85, 85, 85, 86, 87, 88, 88, 89, 88, 88, 88, 90, 88, 88, 88, 92, 93, 93, 94, 94, 94, 94, 95, 95, 95, 96, 97, 98, 99, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 101, 101, 101, 102, 103, 104, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 106, 106, 106, 106, 106, 107, 108, 109, 109, 109, 109, 109, 109, 110, 111, 111, 111, 111, 111, 111, 111, 111, 111, 111, 111, 111, 111, 111, 111, 112, 112, 112, 114, 115, 115, 116, 116, 116, 116, 117, 118, 119, 119, 119, 119, 119, 120, 120, 120, 120, 120, 121, 121, 121, 121, 121, 122, 122, 122, 122, 122, 123, 124, 125, 126, 127, 128, 129, 130, 130, 130, 130, 130, 130, 131, 131, 131, 131, 131, 132, 132, 132, 132, 132, 133, 134, 135, 135, 135, 135, 135, 135, 136, 136, 136, 136, 136, 137, 137, 137, 137, 137, 138, 139, 140, 141, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 143, 143, 143, 143, 143, 144, 144, 144, 144, 145, 146, 147, 148, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 150, 150, 150, 150, 150, 151, 151, 151, 151, 152, 153, 154, 154, 154, 154, 154, 155, 155, 155, 155, 156, 156, 156, 156, 156, 156, 155, 158, 159, 160, 160, 160, 160, 160, 160, 161, 162, 162, 162, 162, 162, 162, 162, 162, 162, 162, 162, 162, 162, 162, 162, 162, 162, 163, 163, 163, 165, 165, 165, 167, 168, 168, 169, 170, 171, 172, 172, 172, 172, 172, 172, 173, 174, 175, 175, 175, 175, 176, 177, 177, 177, 179, 180, 180, 181, 182, 183, 184, 184, 184, 184, 184, 184, 185, 186, 187, 187, 187, 187, 188, 189, 189, 189, 191, 192, 192, 193, 194, 195, 196, 196, 196, 196, 196, 197, 198, 199, 200, 200, 200, 201, 202, 202, 202, 202, 202, 202, 202, 202, 203, 203, 203, 203, 204, 204, 204, 204, 204, 205, 205, 205, 205, 206, 206, 206, 206, 206, 206, 206, 205, 202, 202, 202, 202, 202, 202, 209, 209, 209, 211, 211, 211, 213, 214, 214, 214, 214, 214, 214, 214, 214, 215, 215, 215, 215, 215, 216, 216, 216, 216, 216, 217, 218, 218, 218, 218, 219, 219, 220, 221, 221, 221, 222, 222, 222, 223, 223, 223, 224, 224, 224, 225, 225, 225, 220, 227, 227, 228, 228, 228, 229, 229, 229, 230, 230, 230, 230, 231, 231, 231, 232, 232, 232, 233, 233, 233, 233, 234, 234, 234, 235, 235, 235, 236, 236, 236, 237, 237, 237, 227, 219, 219, 218, 218, 240, 241, 241, 242, 243, 243, 243, 244, 244, 244, 245, 245, 245, 246, 246, 246, 242, 248, 248, 249, 249, 249, 250, 250, 250, 251, 251, 251, 251, 252, 252, 252, 253, 253, 253, 254, 254, 254, 254, 255, 255, 255, 256, 256, 256, 257, 257, 257, 258, 258, 258, 248, 241, 241, 218, 262, 263, 263, 263, 263, 263, 264, 264, 264, 264, 264, 265, 265, 265, 265, 265, 266, 266, 266, 266, 266, 266, 266, 267, 214, 214, 267, 269, 270, 270, 270, 270, 271, 272, 273, 274, 275, 275, 275, 275, 276, 277, 277, 277, 277, 277, 277, 277, 278, 278, 278, 279, 279, 279, 279, 279, 279, 280, 280, 280, 280, 280, 280, 280, 281, 279, 281, 279, 283, 284, 284, 284, 284, 284, 284, 284, 285, 285, 285, 285, 285, 285, 286, 286, 286, 286, 286, 286, 286, 287, 285, 287, 285, 289, 290, 290, 291, 291, 293, 294, 294, 294, 295, 295, 295, 295, 296, 297, 297, 297, 297, 297, 297, 298, 299, 299, 299, 299, 299, 299, 300, 300, 300, 300, 300, 300, 299, 295, 303, 303, 305, 306, 306, 306, 306, 307, 308, 309, 310, 311, 311, 311, 312, 312, 312, 312, 312, 313, 313, 313, 313, 313, 313, 313, 312, 312, 314, 315, 315, 315, 315, 315, 315, 312, 317, 306, 317, 319, 320, 320, 320, 320, 320, 321, 322, 323, 324, 325, 326, 327, 327, 327, 327, 327, 327, 327, 328, 329, 329, 329, 329, 329, 329, 329, 329, 329, 329, 329, 329, 329, 330, 320, 330, 332, 333, 333, 333, 333, 334, 335, 336, 336, 336, 336, 337, 338, 338, 338, 338, 338, 338, 338, 339, 339, 339, 340, 341, 341, 341, 341, 341, 341, 341, 341, 341, 342, 342, 342, 342, 342, 343, 333, 343, 345, 346, 346, 346, 346, 346, 346, 347, 348, 349, 350, 351, 351, 351, 351, 351, 352, 352, 353, 353, 353, 354, 354, 354, 355, 355, 355, 356, 356, 356, 357, 357, 357, 352, 352, 351, 351, 359, 360, 360, 361, 361, 361, 362, 362, 362, 363, 363, 363, 364, 364, 364, 365, 365, 365, 366, 366, 366, 360, 360, 351, 369, 369], + "errors": { + "46": "Invalid transaction: The first foreign app reference must be Tinyman AMM.", + "49": "Invalid transaction: The first account reference must be address of the pool.", + "50": "Invalid transaction: The second account reference must be address of the pool.", + "53": "Invalid transaction: The first foreign asset reference must be input asset.", + "54": "Invalid transaction: The second foreign asset reference must be intermediary asset.", + "55": "Invalid transaction: The third foreign asset reference must be output asset.", + "61": "Swap Router must opt-in to intermediary asset. asset_opt_in method should be called.", + "65": "Swap Router must opt-in to output asset. asset_opt_in method should be called.", + "70": "Invalid transaction: Invalid transaction group structure.", + "72": "Invalid transaction: Sender of transactions must be the same.", + "75": "Invalid transaction: Receiver of transfer must be swap router account.", + "76": "Invalid transaction: The first foreign asset reference must be input asset.", + "79": "Invalid transaction: Receiver of transfer must be swap router account.", + "80": "Invalid transaction: The first foreign asset reference must be input asset.", + "85": "Input amount must be greater than 0.", + "94": "Invalid transaction: Invalid minimum output amount parameter.", + "101": "Contact Error: Unexpected swap output amount.", + "106": "Output amount is less than specified minimum output.", + "116": "Invalid transaction: Invalid output amount parameter.", + "143": "Contact Error: Unexpected swap output amount.", + "144": "Contact Error: Unexpected change amount.", + "150": "Contact Error: Unexpected swap output amount.", + "151": "Contact Error: Unexpected change amount.", + "278": "One of the pools has no liquidity.", + "329": "One of the pools has not enough liquidity.", + "339": "One of the pools has not bootstrapped.", + "220": "Swap failed, please try with new quotes.", + "227": "Swap failed, please try with new quotes.", + "242": "Swap failed, please try with new quotes.", + "248": "Swap failed, please try with new quotes.", + "360": "Asset transfer failed, please check opted in assets." + } +} \ No newline at end of file diff --git a/tinyman/v2/utils.py b/tinyman/v2/utils.py index fc3404a..022f8cc 100644 --- a/tinyman/v2/utils.py +++ b/tinyman/v2/utils.py @@ -4,21 +4,35 @@ from base64 import b64decode import tinyman.v2 +from tinyman.swap_router.constants import TESTNET_SWAP_ROUTER_APP_ID_V1, MAINNET_SWAP_ROUTER_APP_ID_V1 from tinyman.tealishmap import TealishMap from tinyman.utils import bytes_to_int +from tinyman.v2.constants import TESTNET_VALIDATOR_APP_ID, MAINNET_VALIDATOR_APP_ID if sys.version_info >= (3, 9): - tealishmap = TealishMap( + amm_tealishmap = TealishMap( json.loads( importlib.resources.files(tinyman.v2) .joinpath("amm_approval.map.json") .read_text() ) ) + swap_router_tealishmap = TealishMap( + json.loads( + importlib.resources.files(tinyman.v2) + .joinpath("swap_router_approval.map.json") + .read_text() + ) + ) + + else: - tealishmap = TealishMap( + amm_tealishmap = TealishMap( json.loads(importlib.resources.read_text(tinyman.v2, "amm_approval.map.json")) ) + swap_router_tealishmap = TealishMap( + json.loads(importlib.resources.read_text(tinyman.v2, "swap_router_approval.map.json")) + ) def decode_logs(logs: "list") -> dict: @@ -55,7 +69,17 @@ def get_state_from_account_info(account_info, app_id): return app_state -def lookup_error(pc, error_message): +def get_tealishmap(app_id): + maps = { + TESTNET_VALIDATOR_APP_ID: amm_tealishmap, + MAINNET_VALIDATOR_APP_ID: amm_tealishmap, + TESTNET_SWAP_ROUTER_APP_ID_V1: swap_router_tealishmap, + MAINNET_SWAP_ROUTER_APP_ID_V1: swap_router_tealishmap + } + return maps.get(app_id) + + +def lookup_error(pc, error_message, tealishmap): tealish_line_no = tealishmap.get_tealish_line_for_pc(int(pc)) if "assert failed" in error_message or "err opcode executed" in error_message: custom_error_message = tealishmap.get_error_for_pc(int(pc)) From e8c715dc4af72ee4f552c072278b25fba6597b50 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 12 Apr 2023 16:47:37 +0300 Subject: [PATCH 25/32] update swap router error mapping --- tinyman/v2/swap_router_approval.map.json | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tinyman/v2/swap_router_approval.map.json b/tinyman/v2/swap_router_approval.map.json index 1414893..0224519 100644 --- a/tinyman/v2/swap_router_approval.map.json +++ b/tinyman/v2/swap_router_approval.map.json @@ -3,11 +3,6 @@ "teal_tealish": [0, 1, 2, 3, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 4, 11, 12, 12, 13, 12, 12, 12, 14, 12, 12, 12, 15, 12, 12, 12, 16, 12, 12, 12, 17, 12, 12, 12, 19, 20, 20, 21, 21, 21, 23, 24, 24, 25, 25, 26, 25, 25, 25, 27, 25, 25, 25, 28, 25, 25, 25, 29, 25, 25, 25, 30, 25, 25, 25, 32, 33, 33, 34, 35, 36, 37, 37, 37, 38, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 39, 38, 38, 38, 38, 38, 38, 41, 41, 41, 43, 44, 44, 45, 46, 46, 46, 46, 46, 46, 47, 48, 48, 48, 49, 49, 49, 50, 50, 50, 51, 52, 53, 53, 53, 54, 54, 54, 55, 55, 55, 56, 57, 58, 59, 59, 59, 59, 60, 60, 60, 60, 60, 60, 61, 61, 61, 59, 63, 63, 63, 63, 64, 64, 64, 64, 64, 64, 65, 65, 65, 63, 67, 68, 69, 70, 70, 70, 71, 71, 71, 71, 71, 72, 72, 72, 72, 72, 72, 73, 74, 74, 74, 74, 74, 74, 74, 75, 75, 75, 75, 75, 75, 76, 76, 76, 76, 77, 77, 77, 77, 74, 74, 78, 78, 78, 78, 78, 78, 79, 79, 79, 79, 79, 79, 80, 80, 80, 80, 80, 80, 81, 81, 81, 81, 74, 74, 82, 83, 83, 74, 85, 85, 85, 86, 87, 88, 88, 89, 88, 88, 88, 90, 88, 88, 88, 92, 93, 93, 94, 94, 94, 94, 95, 95, 95, 96, 97, 98, 99, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 101, 101, 101, 102, 103, 104, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 106, 106, 106, 106, 106, 107, 108, 109, 109, 109, 109, 109, 109, 110, 111, 111, 111, 111, 111, 111, 111, 111, 111, 111, 111, 111, 111, 111, 111, 112, 112, 112, 114, 115, 115, 116, 116, 116, 116, 117, 118, 119, 119, 119, 119, 119, 120, 120, 120, 120, 120, 121, 121, 121, 121, 121, 122, 122, 122, 122, 122, 123, 124, 125, 126, 127, 128, 129, 130, 130, 130, 130, 130, 130, 131, 131, 131, 131, 131, 132, 132, 132, 132, 132, 133, 134, 135, 135, 135, 135, 135, 135, 136, 136, 136, 136, 136, 137, 137, 137, 137, 137, 138, 139, 140, 141, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 143, 143, 143, 143, 143, 144, 144, 144, 144, 145, 146, 147, 148, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 150, 150, 150, 150, 150, 151, 151, 151, 151, 152, 153, 154, 154, 154, 154, 154, 155, 155, 155, 155, 156, 156, 156, 156, 156, 156, 155, 158, 159, 160, 160, 160, 160, 160, 160, 161, 162, 162, 162, 162, 162, 162, 162, 162, 162, 162, 162, 162, 162, 162, 162, 162, 162, 163, 163, 163, 165, 165, 165, 167, 168, 168, 169, 170, 171, 172, 172, 172, 172, 172, 172, 173, 174, 175, 175, 175, 175, 176, 177, 177, 177, 179, 180, 180, 181, 182, 183, 184, 184, 184, 184, 184, 184, 185, 186, 187, 187, 187, 187, 188, 189, 189, 189, 191, 192, 192, 193, 194, 195, 196, 196, 196, 196, 196, 197, 198, 199, 200, 200, 200, 201, 202, 202, 202, 202, 202, 202, 202, 202, 203, 203, 203, 203, 204, 204, 204, 204, 204, 205, 205, 205, 205, 206, 206, 206, 206, 206, 206, 206, 205, 202, 202, 202, 202, 202, 202, 209, 209, 209, 211, 211, 211, 213, 214, 214, 214, 214, 214, 214, 214, 214, 215, 215, 215, 215, 215, 216, 216, 216, 216, 216, 217, 218, 218, 218, 218, 219, 219, 220, 221, 221, 221, 222, 222, 222, 223, 223, 223, 224, 224, 224, 225, 225, 225, 220, 227, 227, 228, 228, 228, 229, 229, 229, 230, 230, 230, 230, 231, 231, 231, 232, 232, 232, 233, 233, 233, 233, 234, 234, 234, 235, 235, 235, 236, 236, 236, 237, 237, 237, 227, 219, 219, 218, 218, 240, 241, 241, 242, 243, 243, 243, 244, 244, 244, 245, 245, 245, 246, 246, 246, 242, 248, 248, 249, 249, 249, 250, 250, 250, 251, 251, 251, 251, 252, 252, 252, 253, 253, 253, 254, 254, 254, 254, 255, 255, 255, 256, 256, 256, 257, 257, 257, 258, 258, 258, 248, 241, 241, 218, 262, 263, 263, 263, 263, 263, 264, 264, 264, 264, 264, 265, 265, 265, 265, 265, 266, 266, 266, 266, 266, 266, 266, 267, 214, 214, 267, 269, 270, 270, 270, 270, 271, 272, 273, 274, 275, 275, 275, 275, 276, 277, 277, 277, 277, 277, 277, 277, 278, 278, 278, 279, 279, 279, 279, 279, 279, 280, 280, 280, 280, 280, 280, 280, 281, 279, 281, 279, 283, 284, 284, 284, 284, 284, 284, 284, 285, 285, 285, 285, 285, 285, 286, 286, 286, 286, 286, 286, 286, 287, 285, 287, 285, 289, 290, 290, 291, 291, 293, 294, 294, 294, 295, 295, 295, 295, 296, 297, 297, 297, 297, 297, 297, 298, 299, 299, 299, 299, 299, 299, 300, 300, 300, 300, 300, 300, 299, 295, 303, 303, 305, 306, 306, 306, 306, 307, 308, 309, 310, 311, 311, 311, 312, 312, 312, 312, 312, 313, 313, 313, 313, 313, 313, 313, 312, 312, 314, 315, 315, 315, 315, 315, 315, 312, 317, 306, 317, 319, 320, 320, 320, 320, 320, 321, 322, 323, 324, 325, 326, 327, 327, 327, 327, 327, 327, 327, 328, 329, 329, 329, 329, 329, 329, 329, 329, 329, 329, 329, 329, 329, 330, 320, 330, 332, 333, 333, 333, 333, 334, 335, 336, 336, 336, 336, 337, 338, 338, 338, 338, 338, 338, 338, 339, 339, 339, 340, 341, 341, 341, 341, 341, 341, 341, 341, 341, 342, 342, 342, 342, 342, 343, 333, 343, 345, 346, 346, 346, 346, 346, 346, 347, 348, 349, 350, 351, 351, 351, 351, 351, 352, 352, 353, 353, 353, 354, 354, 354, 355, 355, 355, 356, 356, 356, 357, 357, 357, 352, 352, 351, 351, 359, 360, 360, 361, 361, 361, 362, 362, 362, 363, 363, 363, 364, 364, 364, 365, 365, 365, 366, 366, 366, 360, 360, 351, 369, 369], "errors": { "46": "Invalid transaction: The first foreign app reference must be Tinyman AMM.", - "49": "Invalid transaction: The first account reference must be address of the pool.", - "50": "Invalid transaction: The second account reference must be address of the pool.", - "53": "Invalid transaction: The first foreign asset reference must be input asset.", - "54": "Invalid transaction: The second foreign asset reference must be intermediary asset.", - "55": "Invalid transaction: The third foreign asset reference must be output asset.", "61": "Swap Router must opt-in to intermediary asset. asset_opt_in method should be called.", "65": "Swap Router must opt-in to output asset. asset_opt_in method should be called.", "70": "Invalid transaction: Invalid transaction group structure.", @@ -17,21 +12,9 @@ "79": "Invalid transaction: Receiver of transfer must be swap router account.", "80": "Invalid transaction: The first foreign asset reference must be input asset.", "85": "Input amount must be greater than 0.", - "94": "Invalid transaction: Invalid minimum output amount parameter.", - "101": "Contact Error: Unexpected swap output amount.", - "106": "Output amount is less than specified minimum output.", - "116": "Invalid transaction: Invalid output amount parameter.", - "143": "Contact Error: Unexpected swap output amount.", - "144": "Contact Error: Unexpected change amount.", - "150": "Contact Error: Unexpected swap output amount.", - "151": "Contact Error: Unexpected change amount.", "278": "One of the pools has no liquidity.", "329": "One of the pools has not enough liquidity.", "339": "One of the pools has not bootstrapped.", - "220": "Swap failed, please try with new quotes.", - "227": "Swap failed, please try with new quotes.", - "242": "Swap failed, please try with new quotes.", - "248": "Swap failed, please try with new quotes.", "360": "Asset transfer failed, please check opted in assets." } } \ No newline at end of file From e78815b792aa7e15077fdd596a036a5cc06b2acc Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 13 Apr 2023 16:13:12 +0300 Subject: [PATCH 26/32] add mainnet swap router app id --- tinyman/swap_router/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tinyman/swap_router/constants.py b/tinyman/swap_router/constants.py index cd43a75..3da0e82 100644 --- a/tinyman/swap_router/constants.py +++ b/tinyman/swap_router/constants.py @@ -1,5 +1,5 @@ TESTNET_SWAP_ROUTER_APP_ID_V1 = 184778019 -MAINNET_SWAP_ROUTER_APP_ID_V1 = 1071281873 # TODO: temp app for testing. +MAINNET_SWAP_ROUTER_APP_ID_V1 = 1083651166 FIXED_INPUT_SWAP_TYPE = "fixed-input" FIXED_OUTPUT_SWAP_TYPE = "fixed-output" From d4a1012c1f68f41dca00798d3016fd644083860f Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 13 Apr 2023 16:29:56 +0300 Subject: [PATCH 27/32] add error mapping file to package --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 92ae652..ead32d1 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,6 @@ install_requires=["py-algorand-sdk >= 1.10.0"], packages=setuptools.find_packages(), python_requires=">=3.8", - package_data={"tinyman.v1": ["asc.json"], "tinyman.v2": ["amm_approval.map.json"]}, + package_data={"tinyman.v1": ["asc.json"], "tinyman.v2": ["amm_approval.map.json", "swap_router_approval.map.json"]}, include_package_data=True, ) From bf89824fdc2cf9f4f76b1d27a9b91baa808ca4f0 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Mon, 17 Apr 2023 16:04:19 +0300 Subject: [PATCH 28/32] fix linter issues --- setup.py | 5 ++++- tests/swap_router/test.py | 1 - tinyman/swap_router/management.py | 10 ++++++---- tinyman/swap_router/routes.py | 8 ++++++-- tinyman/v2/utils.py | 11 ++++++++--- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/setup.py b/setup.py index ead32d1..43a9d20 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,9 @@ install_requires=["py-algorand-sdk >= 1.10.0"], packages=setuptools.find_packages(), python_requires=">=3.8", - package_data={"tinyman.v1": ["asc.json"], "tinyman.v2": ["amm_approval.map.json", "swap_router_approval.map.json"]}, + package_data={ + "tinyman.v1": ["asc.json"], + "tinyman.v2": ["amm_approval.map.json", "swap_router_approval.map.json"], + }, include_package_data=True, ) diff --git a/tests/swap_router/test.py b/tests/swap_router/test.py index dd8b43b..7a30ead 100644 --- a/tests/swap_router/test.py +++ b/tests/swap_router/test.py @@ -506,7 +506,6 @@ def test_algo_input_prepare_swap_router_transactions(self): def test_indirect_route_prepare_swap_router_transactions_from_quotes(self): sp = self.get_suggested_params() user_private_key, user_address = generate_account() - router_app_address = get_application_address(self.ROUTER_APP_ID) asset_in = AssetAmount(self.asset_in, 1_000_000) asset_out = AssetAmount(self.asset_out, 2_000_000) asset_intermediary = AssetAmount(self.intermediary_asset, 9_999_999) diff --git a/tinyman/swap_router/management.py b/tinyman/swap_router/management.py index e13c131..c5cfdbe 100644 --- a/tinyman/swap_router/management.py +++ b/tinyman/swap_router/management.py @@ -4,7 +4,11 @@ ApplicationNoOpTxn, SuggestedParams, ) -from tinyman.swap_router.constants import CLAIM_EXTRA_APP_ARGUMENT, SET_MANAGER_APP_ARGUMENT, SET_EXTRA_COLLECTOR_APP_ARGUMENT +from tinyman.swap_router.constants import ( + CLAIM_EXTRA_APP_ARGUMENT, + SET_MANAGER_APP_ARGUMENT, + SET_EXTRA_COLLECTOR_APP_ARGUMENT, +) from tinyman.utils import TransactionGroup @@ -22,7 +26,7 @@ def prepare_claim_extra_transactions( index=router_app_id, app_args=[CLAIM_EXTRA_APP_ARGUMENT], foreign_assets=asset_ids, - note=app_call_note + note=app_call_note, ) min_fee = suggested_params.min_fee inner_transaction_count = len(asset_ids) @@ -72,5 +76,3 @@ def prepare_set_extra_collector_transactions( ] txn_group = TransactionGroup(txns) return txn_group - - diff --git a/tinyman/swap_router/routes.py b/tinyman/swap_router/routes.py index a1be2a3..bb18d5a 100644 --- a/tinyman/swap_router/routes.py +++ b/tinyman/swap_router/routes.py @@ -7,7 +7,11 @@ from tinyman.assets import Asset, AssetAmount from tinyman.compat import SuggestedParams -from tinyman.exceptions import PoolHasNoLiquidity, InsufficientReserves, LowSwapAmountError +from tinyman.exceptions import ( + PoolHasNoLiquidity, + InsufficientReserves, + LowSwapAmountError, +) from tinyman.utils import TransactionGroup from tinyman.swap_router.constants import FIXED_INPUT_SWAP_TYPE, FIXED_OUTPUT_SWAP_TYPE from tinyman.v1.pools import Pool as TinymanV1Pool @@ -196,7 +200,7 @@ def get_transaction_count(cls, quotes) -> int: 2: { FIXED_INPUT_SWAP_TYPE: 8, FIXED_OUTPUT_SWAP_TYPE: 9, - } + }, } swap_count = len(quotes) diff --git a/tinyman/v2/utils.py b/tinyman/v2/utils.py index 022f8cc..a26600f 100644 --- a/tinyman/v2/utils.py +++ b/tinyman/v2/utils.py @@ -4,7 +4,10 @@ from base64 import b64decode import tinyman.v2 -from tinyman.swap_router.constants import TESTNET_SWAP_ROUTER_APP_ID_V1, MAINNET_SWAP_ROUTER_APP_ID_V1 +from tinyman.swap_router.constants import ( + TESTNET_SWAP_ROUTER_APP_ID_V1, + MAINNET_SWAP_ROUTER_APP_ID_V1, +) from tinyman.tealishmap import TealishMap from tinyman.utils import bytes_to_int from tinyman.v2.constants import TESTNET_VALIDATOR_APP_ID, MAINNET_VALIDATOR_APP_ID @@ -31,7 +34,9 @@ json.loads(importlib.resources.read_text(tinyman.v2, "amm_approval.map.json")) ) swap_router_tealishmap = TealishMap( - json.loads(importlib.resources.read_text(tinyman.v2, "swap_router_approval.map.json")) + json.loads( + importlib.resources.read_text(tinyman.v2, "swap_router_approval.map.json") + ) ) @@ -74,7 +79,7 @@ def get_tealishmap(app_id): TESTNET_VALIDATOR_APP_ID: amm_tealishmap, MAINNET_VALIDATOR_APP_ID: amm_tealishmap, TESTNET_SWAP_ROUTER_APP_ID_V1: swap_router_tealishmap, - MAINNET_SWAP_ROUTER_APP_ID_V1: swap_router_tealishmap + MAINNET_SWAP_ROUTER_APP_ID_V1: swap_router_tealishmap, } return maps.get(app_id) From 4978909bdc7594efec684d85019274a6564103ba Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Mon, 17 Apr 2023 16:38:48 +0300 Subject: [PATCH 29/32] pin Black version and fix issues --- .github/workflows/tests.yml | 2 +- .pre-commit-config.yaml | 2 +- tinyman/swap_router/management.py | 1 - tinyman/swap_router/swap_router.py | 1 - tinyman/v2/pools.py | 1 - 5 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6e2f1eb..4ff4e25 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 black py-algorand-sdk==${{ matrix.py-algorand-sdk-version }} + pip install flake8 black==23.3.0 py-algorand-sdk==${{ matrix.py-algorand-sdk-version }} - name: Run flake8 run: flake8 ${{ github.workspace }} --ignore=E501,F403,F405,E126,E121,W503,E203 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0b32a2e..ee83d68 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: exclude: ^(env|venv) - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 23.3.0 hooks: - id: black args: ['.', '--check'] diff --git a/tinyman/swap_router/management.py b/tinyman/swap_router/management.py index c5cfdbe..8ef2dd0 100644 --- a/tinyman/swap_router/management.py +++ b/tinyman/swap_router/management.py @@ -19,7 +19,6 @@ def prepare_claim_extra_transactions( suggested_params: SuggestedParams, app_call_note: Optional[str] = None, ) -> TransactionGroup: - claim_extra_app_call = ApplicationNoOpTxn( sender=sender, sp=suggested_params, diff --git a/tinyman/swap_router/swap_router.py b/tinyman/swap_router/swap_router.py index c3ab228..9a84ea4 100644 --- a/tinyman/swap_router/swap_router.py +++ b/tinyman/swap_router/swap_router.py @@ -31,7 +31,6 @@ def prepare_swap_router_asset_opt_in_transaction( user_address: str, suggested_params: SuggestedParams, ) -> TransactionGroup: - asset_opt_in_app_call = ApplicationNoOpTxn( sender=user_address, sp=suggested_params, diff --git a/tinyman/v2/pools.py b/tinyman/v2/pools.py index 813fdb4..0175c10 100644 --- a/tinyman/v2/pools.py +++ b/tinyman/v2/pools.py @@ -332,7 +332,6 @@ def prepare_bootstrap_transactions( refresh: bool = True, suggested_params: SuggestedParams = None, ) -> TransactionGroup: - user_address = user_address or self.client.user_address if refresh: From 3d42421ab73b977984b85fb184cd920f4b5ab343 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Mon, 17 Apr 2023 16:49:55 +0300 Subject: [PATCH 30/32] add requests to install_requires --- .github/workflows/tests.yml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4ff4e25..51fb77f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 black==23.3.0 py-algorand-sdk==${{ matrix.py-algorand-sdk-version }} + pip install flake8 black==23.3.0 py-algorand-sdk==${{ matrix.py-algorand-sdk-version }} requests - name: Run flake8 run: flake8 ${{ github.workspace }} --ignore=E501,F403,F405,E126,E121,W503,E203 diff --git a/setup.py b/setup.py index 43a9d20..e5446ad 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ project_urls={ "Source": "https://github.com/tinyman/tinyman-py-sdk", }, - install_requires=["py-algorand-sdk >= 1.10.0"], + install_requires=["py-algorand-sdk >= 1.10.0", "requests >= 2.0.0"], packages=setuptools.find_packages(), python_requires=">=3.8", package_data={ From 8f2dcaf3459bdc4a2a8ca0ac501ce7e98d2241ad Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Mon, 17 Apr 2023 17:18:33 +0300 Subject: [PATCH 31/32] fix type hints --- examples/swap_router/swap.py | 2 +- tinyman/swap_router/routes.py | 10 +++++----- tinyman/swap_router/swap_router.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/swap_router/swap.py b/examples/swap_router/swap.py index ed69be2..ae4d50d 100644 --- a/examples/swap_router/swap.py +++ b/examples/swap_router/swap.py @@ -29,7 +29,7 @@ def fetch_routes( asset_out: Asset, swap_type: str, amount: int, -) -> list[Route]: +) -> "list[Route]": """ This is an example route list preparation. You can build yor own route list according to your needs. diff --git a/tinyman/swap_router/routes.py b/tinyman/swap_router/routes.py index bb18d5a..1e58fdf 100644 --- a/tinyman/swap_router/routes.py +++ b/tinyman/swap_router/routes.py @@ -23,7 +23,7 @@ class Route: asset_in: Asset asset_out: Asset - pools: Union[list[TinymanV2Pool], list[TinymanV1Pool]] + pools: "Union[list[TinymanV2Pool], list[TinymanV1Pool]]" def __str__(self): return "Route: " + " -> ".join(f"{pool}" for pool in self.pools) @@ -69,7 +69,7 @@ def get_fixed_output_quotes(self, amount_out: int, slippage: float = 0.05): def prepare_swap_router_transactions_from_quotes( self, - quotes: list[TinymanV2SwapQuote], + quotes: "list[TinymanV2SwapQuote]", user_address: Optional[str] = None, suggested_params: Optional[SuggestedParams] = None, ) -> TransactionGroup: @@ -115,7 +115,7 @@ def prepare_swap_router_transactions_from_quotes( raise NotImplementedError() @property - def asset_ids(self) -> list[int]: + def asset_ids(self) -> "list[int]": asset_ids = [self.asset_in.id] for pool in self.pools: @@ -210,7 +210,7 @@ def get_transaction_count(cls, quotes) -> int: def get_best_fixed_input_route( - routes: list[Route], amount_in: int, asset_in_algo_price: Optional[float] = None + routes: "list[Route]", amount_in: int, asset_in_algo_price: Optional[float] = None ) -> Optional[Route]: best_route = None best_route_price_impact = None @@ -237,7 +237,7 @@ def get_best_fixed_input_route( def get_best_fixed_output_route( - routes: list[Route], amount_out: int, asset_in_algo_price: Optional[float] = None + routes: "list[Route]", amount_out: int, asset_in_algo_price: Optional[float] = None ): best_route = None best_route_price_impact = None diff --git a/tinyman/swap_router/swap_router.py b/tinyman/swap_router/swap_router.py index 9a84ea4..1ed9d3c 100644 --- a/tinyman/swap_router/swap_router.py +++ b/tinyman/swap_router/swap_router.py @@ -119,8 +119,8 @@ def prepare_swap_router_transactions( def get_swap_router_app_opt_in_required_asset_ids( - algod_client: AlgodClient, router_app_id: int, asset_ids=list[int] -) -> list[int]: + algod_client: AlgodClient, router_app_id: int, asset_ids: "list[int]" +) -> "list[int]": swap_router_app_address = get_application_address(router_app_id) account_info = algod_client.account_info(swap_router_app_address) From 619e98966903ec38aefa76f118f700b022203331 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Mon, 17 Apr 2023 17:19:08 +0300 Subject: [PATCH 32/32] add new Algorand SDK versions to test matrix --- .github/workflows/tests.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 51fb77f..4698d2c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,8 +9,11 @@ jobs: strategy: matrix: python-version: ["3.11", "3.10", "3.9", "3.8"] - py-algorand-sdk-version: ["1.10.0", "1.11.0", "1.12.0", "1.13.0", "1.13.1", "1.14.0", "1.15.0", "1.16.0", "1.16.1", - "1.17.0", "1.18.0", "1.19.0", "1.20.0", "1.20.1", "1.20.2", "2.0.0"] + py-algorand-sdk-version: [ + "1.10.0", "1.11.0", "1.12.0", "1.13.0", "1.13.1", "1.14.0", "1.15.0", "1.16.0", + "1.16.1","1.17.0", "1.18.0","1.19.0", "1.20.0", "1.20.1", "1.20.2", + "2.0.0", "2.1.0", "2.1.1", "2.1.2" + ] steps: - uses: actions/checkout@v3