From c637949d92e72ec86968458cba5977722edea23c Mon Sep 17 00:00:00 2001 From: etzellux Date: Wed, 2 Jul 2025 15:26:05 +0300 Subject: [PATCH 1/9] reorganize the repo --- .../contracts/{ => v1}/swap_router_approval.tl | 0 .../contracts/{ => v1}/swap_router_clear_state.tl | 0 .../contracts/{ => v2}/swap_router_v2_approval.tl | 0 .../contracts/{ => v2}/swap_router_v2_clear_state.tl | 0 swap_router/sdk/{ => v2}/client.py | 7 ++++--- swap_router/tests/{ => v1}/constants.py | 0 swap_router/tests/{ => v1}/test_swap_router.py | 6 +++--- swap_router/tests/{ => v2}/test_swap_router_v2.py | 11 ++++------- 8 files changed, 11 insertions(+), 13 deletions(-) rename swap_router/contracts/{ => v1}/swap_router_approval.tl (100%) rename swap_router/contracts/{ => v1}/swap_router_clear_state.tl (100%) rename swap_router/contracts/{ => v2}/swap_router_v2_approval.tl (100%) rename swap_router/contracts/{ => v2}/swap_router_v2_clear_state.tl (100%) rename swap_router/sdk/{ => v2}/client.py (98%) rename swap_router/tests/{ => v1}/constants.py (100%) rename swap_router/tests/{ => v1}/test_swap_router.py (99%) rename swap_router/tests/{ => v2}/test_swap_router_v2.py (98%) diff --git a/swap_router/contracts/swap_router_approval.tl b/swap_router/contracts/v1/swap_router_approval.tl similarity index 100% rename from swap_router/contracts/swap_router_approval.tl rename to swap_router/contracts/v1/swap_router_approval.tl diff --git a/swap_router/contracts/swap_router_clear_state.tl b/swap_router/contracts/v1/swap_router_clear_state.tl similarity index 100% rename from swap_router/contracts/swap_router_clear_state.tl rename to swap_router/contracts/v1/swap_router_clear_state.tl diff --git a/swap_router/contracts/swap_router_v2_approval.tl b/swap_router/contracts/v2/swap_router_v2_approval.tl similarity index 100% rename from swap_router/contracts/swap_router_v2_approval.tl rename to swap_router/contracts/v2/swap_router_v2_approval.tl diff --git a/swap_router/contracts/swap_router_v2_clear_state.tl b/swap_router/contracts/v2/swap_router_v2_clear_state.tl similarity index 100% rename from swap_router/contracts/swap_router_v2_clear_state.tl rename to swap_router/contracts/v2/swap_router_v2_clear_state.tl diff --git a/swap_router/sdk/client.py b/swap_router/sdk/v2/client.py similarity index 98% rename from swap_router/sdk/client.py rename to swap_router/sdk/v2/client.py index aa9bdaf..0f6c7e3 100644 --- a/swap_router/sdk/client.py +++ b/swap_router/sdk/v2/client.py @@ -2,8 +2,8 @@ from algosdk.encoding import decode_address, encode_address from algosdk.logic import get_application_address from algosdk.constants import ZERO_ADDRESS -from .base_client import BaseClient -from .utils import int_array, bytes_array +from ..base_client import BaseClient +from ..utils import int_array, bytes_array class SwapRouterClient(BaseClient): @@ -12,6 +12,7 @@ def __init__(self, algod, app_id, tinyman_amm_app_id, talgo_app_id, user_address super().__init__(algod, app_id, user_address, user_sk) self.amm_app_id = tinyman_amm_app_id self.talgo_app_id = talgo_app_id + state = self.get_globals(talgo_app_id) self.talgo_app_address = encode_address(state[b"account_0"]) self.talgo_asset_id = state[b"talgo_asset_id"] @@ -134,7 +135,7 @@ def swap(self, input_amount, output_amount, route, pools): )) inner_txns = sum(tx.get("inner_txns", 0) for tx in transactions) return self._submit(txns, additional_fees=inner_txns) - + def claim_extra(self, asset_id): sp = self.get_suggested_params() txns = [ diff --git a/swap_router/tests/constants.py b/swap_router/tests/v1/constants.py similarity index 100% rename from swap_router/tests/constants.py rename to swap_router/tests/v1/constants.py diff --git a/swap_router/tests/test_swap_router.py b/swap_router/tests/v1/test_swap_router.py similarity index 99% rename from swap_router/tests/test_swap_router.py rename to swap_router/tests/v1/test_swap_router.py index ad34c9c..4778625 100644 --- a/swap_router/tests/test_swap_router.py +++ b/swap_router/tests/v1/test_swap_router.py @@ -12,11 +12,11 @@ from algosdk.future import transaction from algosdk.logic import get_application_address -from tests.constants import MAX_ASSET_AMOUNT, APPLICATION_ID as AMM_APPLICATION_ID +from swap_router.tests.v1.constants import MAX_ASSET_AMOUNT, APPLICATION_ID as AMM_APPLICATION_ID from tests.core import BaseTestCase -swap_router_program = TealishProgram('contracts/swap_router/swap_router_approval.tl') -swap_clear_state_program = TealishProgram('contracts/swap_router/swap_router_clear_state.tl') +swap_router_program = TealishProgram('contracts/v1/swap_router/swap_router_approval.tl') +swap_clear_state_program = TealishProgram('contracts/v1swap_router/swap_router_clear_state.tl') SWAP_ROUTER_APP_ID = 20 SWAP_ROUTER_ADDRESS = get_application_address(SWAP_ROUTER_APP_ID) diff --git a/swap_router/tests/test_swap_router_v2.py b/swap_router/tests/v2/test_swap_router_v2.py similarity index 98% rename from swap_router/tests/test_swap_router_v2.py rename to swap_router/tests/v2/test_swap_router_v2.py index de65491..e76041a 100644 --- a/swap_router/tests/test_swap_router_v2.py +++ b/swap_router/tests/v2/test_swap_router_v2.py @@ -1,6 +1,3 @@ -from unittest import TestCase -from unittest.mock import ANY - from Cryptodome.Hash import SHA512 from algojig import TealishProgram from algojig import get_suggested_params @@ -13,14 +10,14 @@ from algosdk.logic import get_application_address from algosdk.constants import ZERO_ADDRESS -from tests.constants import MAX_ASSET_AMOUNT, APPLICATION_ID as AMM_APPLICATION_ID +from swap_router.tests.v1.constants import MAX_ASSET_AMOUNT, APPLICATION_ID as AMM_APPLICATION_ID from tests.core import BaseTestCase from tests.utils import int_array, bytes_array, JigAlgod -from sdk.client import SwapRouterClient +from swap_router.sdk.v2.client import SwapRouterClient -swap_router_program = TealishProgram('contracts/swap_router_v2_approval.tl') -swap_clear_state_program = TealishProgram('contracts/swap_router_v2_clear_state.tl') +swap_router_program = TealishProgram('contracts/v2/swap_router_v2_approval.tl') +swap_clear_state_program = TealishProgram('contracts/v2/swap_router_v2_clear_state.tl') SWAP_ROUTER_APP_ID = 2001 SWAP_ROUTER_ADDRESS = get_application_address(SWAP_ROUTER_APP_ID) From c89916ab4e7da2e3ba30c23676bc208224fb7b4d Mon Sep 17 00:00:00 2001 From: etzellux Date: Thu, 3 Jul 2025 15:53:39 +0300 Subject: [PATCH 2/9] refactor existing --- swap_router/sdk/base_client.py | 62 ++++++++++++------- .../tests/{v1/constants.py => common.py} | 8 ++- swap_router/tests/core.py | 36 +++++------ swap_router/tests/utils.py | 16 ++++- swap_router/tests/v1/test_swap_router.py | 2 +- swap_router/tests/v2/test_swap_router_v2.py | 26 ++------ 6 files changed, 83 insertions(+), 67 deletions(-) rename swap_router/tests/{v1/constants.py => common.py} (92%) diff --git a/swap_router/sdk/base_client.py b/swap_router/sdk/base_client.py index 6bf2a88..2c328a0 100644 --- a/swap_router/sdk/base_client.py +++ b/swap_router/sdk/base_client.py @@ -1,11 +1,10 @@ -from base64 import b64decode, b64encode import time -from algosdk.encoding import decode_address -from tinyman.utils import TransactionGroup, int_to_bytes +from base64 import b64decode, b64encode from algosdk import transaction -from algosdk.encoding import decode_address, encode_address from algosdk.logic import get_application_address -from algosdk.account import generate_account + +from tinyman.utils import TransactionGroup +from sdk.struct import get_struct, get_box_costs class BaseClient(): @@ -18,13 +17,13 @@ def __init__(self, algod, app_id, user_address, user_sk) -> None: self.add_key(user_address, user_sk) self.current_timestamp = None self.simulate = False - + def get_suggested_params(self): return self.algod.suggested_params() - + def get_current_timestamp(self): return self.current_timestamp or time.time() - + def _submit(self, transactions, additional_fees=0): transactions = self.flatten_transactions(transactions) fee = transactions[0].fee @@ -45,7 +44,7 @@ def _submit(self, transactions, additional_fees=0): else: txn_info = txn_group.submit(self.algod, wait=True) return txn_info - + def flatten_transactions(self, txns): result = [] if isinstance(txns, transaction.Transaction): @@ -54,23 +53,17 @@ def flatten_transactions(self, txns): for txn in txns: result += self.flatten_transactions(txn) return result - + + def calculate_min_balance(self, accounts=0, assets=0, boxes=None): + cost = 0 + cost += accounts * 100_000 + cost += assets * 100_000 + cost += get_box_costs(boxes or {}) + return cost + def add_key(self, address, key): self.keys[address] = key - def get_global(self, key, default=None, app_id=None): - app_id = app_id or self.app_id - global_state = {s["key"]: s["value"] for s in self.algod.application_info(app_id)["params"]["global-state"]} - key = b64encode(key).decode() - if key in global_state: - value = global_state[key] - if value["type"] == 2: - return value["uint"] - else: - return b64decode(value["bytes"]) - else: - return default - def get_globals(self, app_id=None): app_id = app_id or self.app_id gs = self.algod.application_info(app_id)["params"]["global-state"] @@ -86,6 +79,26 @@ def get_globals(self, app_id=None): state = dict(sorted(state.items(), key=lambda x: x[0])) return state + def get_global(self, key, default=None, app_id=None): + app_id = app_id or self.app_id + global_state = {s["key"]: s["value"] for s in self.algod.application_info(app_id)["params"]["global-state"]} + key = b64encode(key).decode() + if key in global_state: + value = global_state[key] + if value["type"] == 2: + return value["uint"] + else: + return b64decode(value["bytes"]) + else: + return default + + def get_box(self, box_name, struct_name, app_id=None): + app_id = app_id or self.app_id + box_value = b64decode(self.algod.application_box_by_name(app_id, box_name)["value"]) + struct_class = get_struct(struct_name) + struct = struct_class(box_value) + return struct + def box_exists(self, box_name, app_id=None): app_id = app_id or self.app_id try: @@ -95,6 +108,9 @@ def box_exists(self, box_name, app_id=None): return False def is_opted_in(self, address, asset_id): + if asset_id == 0: + return True + try: self.algod.account_asset_info(address, asset_id) return True diff --git a/swap_router/tests/v1/constants.py b/swap_router/tests/common.py similarity index 92% rename from swap_router/tests/v1/constants.py rename to swap_router/tests/common.py index 1140d56..9622d71 100644 --- a/swap_router/tests/v1/constants.py +++ b/swap_router/tests/common.py @@ -36,12 +36,13 @@ BLOCK_TIME_DELTA = 1000 BYTE_ZERO = b'\x00\x00\x00\x00\x00\x00\x00\x00' +MINIMUM_BALANCE = 100_000 MAX_UINT64 = 2**64 - 1 # 18446744073709551615 MAX_ASSET_AMOUNT = MAX_UINT64 POOL_TOKEN_TOTAL_SUPPLY = MAX_ASSET_AMOUNT ALGO_ASSET_ID = 0 -APPLICATION_ID = 1001 -APPLICATION_ADDRESS = get_application_address(APPLICATION_ID) +AMM_APPLICATION_ID = 1001 +AMM_APPLICATION_ADDRESS = get_application_address(AMM_APPLICATION_ID) # State APP_LOCAL_INTS = 12 @@ -59,3 +60,6 @@ talgo_approval_program = TealProgram(teal=requests.get("https://github.com/tinymanorg/tinyman-consensus-staking/blob/main/contracts/talgo/build/talgo_approval.teal?raw=True").text) + +SWAP_ROUTER_APP_ID = 2001 +SWAP_ROUTER_ADDRESS = get_application_address(SWAP_ROUTER_APP_ID) \ No newline at end of file diff --git a/swap_router/tests/core.py b/swap_router/tests/core.py index ba32fc4..18ef373 100644 --- a/swap_router/tests/core.py +++ b/swap_router/tests/core.py @@ -5,7 +5,7 @@ from algosdk.encoding import decode_address from algosdk import transaction -from .constants import * +from .common import * from .utils import get_pool_logicsig_bytecode @@ -73,7 +73,7 @@ def create_amm_app(self): self.ledger.set_account_balance(self.app_creator_address, 1_000_000) self.ledger.create_app( - app_id=APPLICATION_ID, + app_id=AMM_APPLICATION_ID, approval_program=amm_approval_program, creator=self.app_creator_address, local_ints=APP_LOCAL_INTS, @@ -83,9 +83,9 @@ def create_amm_app(self): ) # 100_000 for basic min balance requirement # + 100_000 for increase_cost_budget app creation min balance requirement - self.ledger.set_account_balance(APPLICATION_ADDRESS, 200_000) + self.ledger.set_account_balance(AMM_APPLICATION_ADDRESS, 200_000) self.ledger.set_global_state( - APPLICATION_ID, + AMM_APPLICATION_ID, { b'fee_collector': decode_address(self.app_creator_address), b'fee_manager': decode_address(self.app_creator_address), @@ -94,7 +94,7 @@ def create_amm_app(self): ) def bootstrap_pool(self, asset_1_id, asset_2_id): - lsig = get_pool_logicsig_bytecode(amm_pool_template, APPLICATION_ID, asset_1_id, asset_2_id) + lsig = get_pool_logicsig_bytecode(amm_pool_template, AMM_APPLICATION_ID, asset_1_id, asset_2_id) pool_address = lsig.address() if asset_2_id: @@ -110,7 +110,7 @@ def bootstrap_pool(self, asset_1_id, asset_2_id): self.ledger.set_account_balance(pool_address, minimum_balance + 100_000) # Rekey to application address - self.ledger.set_auth_addr(pool_address, APPLICATION_ADDRESS) + self.ledger.set_auth_addr(pool_address, AMM_APPLICATION_ADDRESS) # Opt-in to assets self.ledger.set_account_balance(pool_address, 0, asset_id=asset_1_id) @@ -118,18 +118,18 @@ def bootstrap_pool(self, asset_1_id, asset_2_id): self.ledger.set_account_balance(pool_address, 0, asset_id=asset_2_id) # Create pool token - pool_token_asset_id = self.ledger.create_asset(asset_id=None, params=dict(creator=APPLICATION_ADDRESS)) + pool_token_asset_id = self.ledger.create_asset(asset_id=None, params=dict(creator=AMM_APPLICATION_ADDRESS)) # Transfer Algo to application address - self.ledger.move(100_000, asset_id=0, sender=pool_address, receiver=APPLICATION_ADDRESS) + self.ledger.move(100_000, asset_id=0, sender=pool_address, receiver=AMM_APPLICATION_ADDRESS) # Transfer pool tokens from application adress to pool - self.ledger.set_account_balance(APPLICATION_ADDRESS, 0, asset_id=pool_token_asset_id) + self.ledger.set_account_balance(AMM_APPLICATION_ADDRESS, 0, asset_id=pool_token_asset_id) self.ledger.set_account_balance(pool_address, POOL_TOKEN_TOTAL_SUPPLY, asset_id=pool_token_asset_id) self.ledger.set_local_state( address=pool_address, - app_id=APPLICATION_ID, + app_id=AMM_APPLICATION_ID, state={ b'asset_1_id': asset_1_id, b'asset_2_id': asset_2_id, @@ -162,7 +162,7 @@ def set_initial_pool_liquidity(self, pool_address, asset_1_id, asset_2_id, pool_ self.ledger.update_local_state( address=pool_address, - app_id=APPLICATION_ID, + app_id=AMM_APPLICATION_ID, state_delta={ b'asset_1_reserves': asset_1_reserves, b'asset_2_reserves': asset_2_reserves, @@ -177,7 +177,7 @@ def set_initial_pool_liquidity(self, pool_address, asset_1_id, asset_2_id, pool_ def set_pool_protocol_fees(self, asset_1_protocol_fees, asset_2_protocol_fees): self.ledger.update_local_state( address=self.pool_address, - app_id=APPLICATION_ID, + app_id=AMM_APPLICATION_ID, state_delta={ b'asset_1_protocol_fees': asset_1_protocol_fees, b'asset_2_protocol_fees': asset_2_protocol_fees, @@ -216,7 +216,7 @@ def get_add_initial_liquidity_transactions(self, asset_1_amount, asset_2_amount, transaction.ApplicationNoOpTxn( sender=self.user_addr, sp=self.sp, - index=APPLICATION_ID, + index=AMM_APPLICATION_ID, app_args=[METHOD_ADD_INITIAL_LIQUIDITY], foreign_assets=[self.pool_token_asset_id], accounts=[self.pool_address], @@ -260,7 +260,7 @@ def get_add_liquidity_transactions(self, asset_1_amount, asset_2_amount, min_out transaction.ApplicationNoOpTxn( sender=self.user_addr, sp=self.sp, - index=APPLICATION_ID, + index=AMM_APPLICATION_ID, app_args=[METHOD_ADD_LIQUIDITY, mode, min_output], foreign_assets=[self.pool_token_asset_id], accounts=[self.pool_address], @@ -281,7 +281,7 @@ def get_remove_liquidity_transactions(self, liquidity_asset_amount, min_output_1 transaction.ApplicationNoOpTxn( sender=self.user_addr, sp=self.sp, - index=APPLICATION_ID, + index=AMM_APPLICATION_ID, app_args=[METHOD_REMOVE_LIQUIDITY, min_output_1, min_output_2], foreign_assets=[self.asset_1_id, self.asset_2_id], accounts=[self.pool_address], @@ -304,7 +304,7 @@ def get_remove_liquidity_single_transactions(self, liquidity_asset_amount, asset transaction.ApplicationNoOpTxn( sender=self.user_addr, sp=self.sp, - index=APPLICATION_ID, + index=AMM_APPLICATION_ID, app_args=[METHOD_REMOVE_LIQUIDITY, min_output_1, min_output_2], foreign_assets=[asset_id], accounts=[self.pool_address], @@ -318,7 +318,7 @@ def get_claim_fee_transactions(self, sender, fee_collector, app_call_fee=None): transaction.ApplicationNoOpTxn( sender=sender, sp=self.sp, - index=APPLICATION_ID, + index=AMM_APPLICATION_ID, app_args=[METHOD_CLAIM_FEES], foreign_assets=[self.asset_1_id, self.asset_2_id], accounts=[self.pool_address, fee_collector], @@ -332,7 +332,7 @@ def get_claim_extra_transactions(self, sender, asset_id, address, fee_collector, transaction.ApplicationNoOpTxn( sender=sender, sp=self.sp, - index=APPLICATION_ID, + index=AMM_APPLICATION_ID, app_args=[METHOD_CLAIM_EXTRA], foreign_assets=[asset_id], accounts=[address, fee_collector], diff --git a/swap_router/tests/utils.py b/swap_router/tests/utils.py index 584957a..f666698 100644 --- a/swap_router/tests/utils.py +++ b/swap_router/tests/utils.py @@ -1,8 +1,9 @@ -from base64 import b64encode from algojig import get_suggested_params from algosdk.v2client.algod import AlgodClient from algosdk import transaction +from base64 import b64encode +from Cryptodome.Hash import SHA512 def itob(value): @@ -103,3 +104,16 @@ def application_info(self, application_id): } } return result + + +def get_event_signature(event_name, event_args): + arg_string = ",".join(str(arg.type) for arg in event_args) + event_signature = "{}({})".format(event_name, arg_string) + return event_signature + + +def get_selector(signature): + sha_512_256_hash = SHA512.new(truncate="256") + sha_512_256_hash.update(signature.encode("utf-8")) + selector = sha_512_256_hash.digest()[:4] + return selector diff --git a/swap_router/tests/v1/test_swap_router.py b/swap_router/tests/v1/test_swap_router.py index 4778625..d93ec52 100644 --- a/swap_router/tests/v1/test_swap_router.py +++ b/swap_router/tests/v1/test_swap_router.py @@ -12,7 +12,7 @@ from algosdk.future import transaction from algosdk.logic import get_application_address -from swap_router.tests.v1.constants import MAX_ASSET_AMOUNT, APPLICATION_ID as AMM_APPLICATION_ID +from swap_router.tests.common import MAX_ASSET_AMOUNT, AMM_APPLICATION_ID as AMM_APPLICATION_ID from tests.core import BaseTestCase swap_router_program = TealishProgram('contracts/v1/swap_router/swap_router_approval.tl') diff --git a/swap_router/tests/v2/test_swap_router_v2.py b/swap_router/tests/v2/test_swap_router_v2.py index e76041a..152b66c 100644 --- a/swap_router/tests/v2/test_swap_router_v2.py +++ b/swap_router/tests/v2/test_swap_router_v2.py @@ -10,33 +10,15 @@ from algosdk.logic import get_application_address from algosdk.constants import ZERO_ADDRESS -from swap_router.tests.v1.constants import MAX_ASSET_AMOUNT, APPLICATION_ID as AMM_APPLICATION_ID -from tests.core import BaseTestCase -from tests.utils import int_array, bytes_array, JigAlgod - from swap_router.sdk.v2.client import SwapRouterClient +from swap_router.tests.common import MINIMUM_BALANCE, MAX_ASSET_AMOUNT, AMM_APPLICATION_ID, SWAP_ROUTER_APP_ID, SWAP_ROUTER_ADDRESS + +from tests.core import BaseTestCase +from tests.utils import bytes_array, int_array, get_event_signature, get_selector, JigAlgod swap_router_program = TealishProgram('contracts/v2/swap_router_v2_approval.tl') swap_clear_state_program = TealishProgram('contracts/v2/swap_router_v2_clear_state.tl') -SWAP_ROUTER_APP_ID = 2001 -SWAP_ROUTER_ADDRESS = get_application_address(SWAP_ROUTER_APP_ID) - -MINIMUM_BALANCE = 100_000 - - -def get_event_signature(event_name, event_args): - arg_string = ",".join(str(arg.type) for arg in event_args) - event_signature = "{}({})".format(event_name, arg_string) - return event_signature - - -def get_selector(signature): - sha_512_256_hash = SHA512.new(truncate="256") - sha_512_256_hash.update(signature.encode("utf-8")) - selector = sha_512_256_hash.digest()[:4] - return selector - class CreateAppTestCase(BaseTestCase): @classmethod From 608115e793a52891ed5014cc9fff5b7e56b7fb8e Mon Sep 17 00:00:00 2001 From: etzellux Date: Thu, 3 Jul 2025 15:54:44 +0300 Subject: [PATCH 3/9] add initial swap router v3 contract --- .../contracts/v3/swap_router_v3_approval.tl | 415 ++++++++++++++++++ .../v3/swap_router_v3_clear_state.tl | 4 + swap_router/sdk/__init__.py | 0 swap_router/sdk/struct.py | 162 +++++++ swap_router/sdk/v3/client.py | 263 +++++++++++ 5 files changed, 844 insertions(+) create mode 100644 swap_router/contracts/v3/swap_router_v3_approval.tl create mode 100644 swap_router/contracts/v3/swap_router_v3_clear_state.tl create mode 100644 swap_router/sdk/__init__.py create mode 100644 swap_router/sdk/struct.py create mode 100644 swap_router/sdk/v3/client.py diff --git a/swap_router/contracts/v3/swap_router_v3_approval.tl b/swap_router/contracts/v3/swap_router_v3_approval.tl new file mode 100644 index 0000000..1d3bc26 --- /dev/null +++ b/swap_router/contracts/v3/swap_router_v3_approval.tl @@ -0,0 +1,415 @@ +#pragma version 10 +#tealish version git+https://github.com/Hipo/tealish.git@d7441973671cf6b79dd55843016892f4b86ceeba + + +struct Address: + address: bytes[32] +end + + +# Global State +const bytes START_OUTPUT_ASSET_BALANCE_KEY = "start_output_asset_balance" + + +router: + create_application + update_application + propose_manager + accept_manager + set_extra_collector + claim_extra + asset_opt_in + noop + start_swap_group + end_swap_group + swap +end + + +# Permission: anyone +@public(OnCompletion=CreateApplication) +func create_application(tinyman_app_id: int, talgo_app_id: int, talgo_asset_id: int): + app_global_put("talgo_app_id", talgo_app_id) + bytes talgo_app_address + _, talgo_app_address = app_params_get(AppAddress, talgo_app_id) + app_global_put("talgo_app_address", talgo_app_address) + app_global_put("talgo_asset_id", talgo_asset_id) + app_global_put("tinyman_app_id", tinyman_app_id) + app_global_put("manager", Txn.Sender) + app_global_put("proposed_manager", "") + app_global_put("extra_collector", Txn.Sender) + return +end + + +# TODO: Remove function for mainnet +@public(OnCompletion=UpdateApplication) +func update_application(): + assert(Txn.Sender == app_global_get("manager")) + return +end + + +# Description: This function starts a swap group. +# It expects Gtxn[-1] to be an axfer/pay. +# It expects Gtxn[+index_diff] to be `end_swap_group` appcall. +# Checks transaction parameters within swap group. +@public() +func start_swap_group(input_asset_id: int, output_asset_id: int, total_input_amount: int, index_diff: int): + # Assert input transaction. + assert(Txn.GroupIndex) + int txn_index = Txn.GroupIndex + + int input_txn_index = txn_index - 1 + assert(Gtxn[input_txn_index].Sender == Txn.Sender) + + if Gtxn[input_txn_index].TypeEnum == Pay: + assert(!input_asset_id) + assert(Gtxn[input_txn_index].Receiver == Global.CurrentApplicationAddress) + assert(Gtxn[input_txn_index].Amount == input_amount) + elif Gtxn[input_txn_index].TypeEnum == Axfer: + assert(Gtxn[input_txn_index].XferAsset == input_asset_id) + assert(Gtxn[input_txn_index].AssetReceiver == Global.CurrentApplicationAddress) + assert(Gtxn[input_txn_index].AssetAmount == input_amount) + else: + Error() + end + + # Assert swap appcall parameters. + int tmp_total_input_amount = 0 + for i in 1:index_diff: + assert(Gtxn[txn_index + i].Sender == Txn.Sender) + assert(Gtxn[txn_index + i].TypeEnum == Appl) + assert(Gtxn[txn_index + i].OnCompletion == NoOp) + assert(Gtxn[txn_index + i].ApplicationID == Global.CurrentApplicationID) + + bytes apaa0 = Gtxn[txn_index + i].ApplicationArgs[0] + if apaa0 == "swap": + + tmp_total_input_amount = tmp_total_input_amount + btoi(Gtxn[txn_index].ApplicationArgs[1]) + + int[8] route = Cast(Gtxn[txn_index].ApplicationArgs[3], int[8]) + int swaps = btoi(Gtxn[txn_index].ApplicationArgs[5]) + + assert(route[0] == input_asset_id) + assert(route[swaps] == output_asset_id) + elif apaa0 == "noop": + # Do nothing. + else: + Error() + end + end + + assert(tmp_total_input_amount == total_input_amount) + + # Assert `end_swap_group` transaction. + assert(Gtxn[txn_index + index_diff].Sender == Txn.Sender) + assert(Gtxn[txn_index + index_diff].TypeEnum == Appl) + assert(Gtxn[txn_index + index_diff].OnCompletion == NoOp) + assert(Gtxn[txn_index + index_diff].ApplicationID == Global.CurrentApplicationID) + assert(Gtxn[txn_index + index_diff].ApplicationArgs[0] == "end_swap_group") + assert(Gtxn[txn_index + index_diff].ApplicationArgs[1] == input_asset_id) + assert(Gtxn[txn_index + index_diff].ApplicationArgs[2] == output_asset_id) + assert(Gtxn[txn_index + index_diff].ApplicationArgs[3] == total_input_amount) + + int current_output_asset_balance + if output_asset_id: + current_output_asset_balance, _ = asset_holding_get(AssetBalance, Global.CurrentApplicationAddress, output_asset_id) + else: + current_output_asset_balance = balance(Global.CurrentApplicationAddress) + end + app_global_put(START_OUTPUT_ASSET_BALANCE_KEY, current_output_asset_balance) + + return +end + + +# Description: This function ends a swap group. +@public() +func end_swap_group(input_asset_id: int, output_asset_id: int, total_input_amount: int, output_amount: int, index_diff: int): + int txn_index = Txn.GroupIndex + + # Assert the `start_swap_group` transaction. + assert(Gtxn[txn_index - index_diff].Sender == Txn.Sender) + assert(Gtxn[txn_index - index_diff].TypeEnum == Appl) + assert(Gtxn[txn_index - index_diff].OnCompletion == NoOp) + assert(Gtxn[txn_index - index_diff].ApplicationID == Global.CurrentApplicationID) + assert(Gtxn[txn_index - index_diff].ApplicationArgs[0] == "start_swap_group") + assert(Gtxn[txn_index - index_diff].ApplicationArgs[4] == index_diff) + + int current_output_asset_balance + if output_asset_id: + current_output_asset_balance, _ = asset_holding_get(AssetBalance, Global.CurrentApplicationAddress, output_asset_id) + else: + current_output_asset_balance = balance(Global.CurrentApplicationAddress) + end + + # Calculate final output amount. + int start_output_asset_balance = app_global_get(START_OUTPUT_ASSET_BALANCE_KEY) + int final_output_amount = current_output_asset_balance - start_output_asset_balance + assert(final_output_amount >= output_amount) + + # Transfer output to user. + transfer(output_asset_id, final_output_amount, Global.CurrentApplicationAddress, Txn.Sender) + + log(ARC28Event("swap_group(uint64,uint64,uint64,uint64)", itob(input_asset_id), itob(output_asset_id), itob(total_input_amount), itob(final_output_amount))) + return +end + + +@public() +func swap(input_amount: int, route: int[8], pools: Address[8], swaps: int): + bytes user_address = Txn.Sender + + # Swap Route: input_asset_id -> intermediary_asset_id -> output_asset_id + int input_asset_id = route[0] + int output_asset_id = route[swaps] + + assert(input_amount) + + int tmp_asset_in + int tmp_asset_out + bytes pool_address + + int tmp_swap_amount = input_amount + for i in 0:swaps: + tmp_asset_in = route[i] + tmp_asset_out = route[i+1] + pool_address = pools[i] + if (pool_address == app_global_get("talgo_app_address")) && (tmp_asset_in == 0): + tmp_swap_amount = talgo_mint(tmp_swap_amount) + elif (pool_address == app_global_get("talgo_app_address")) && (tmp_asset_out == 0): + tmp_swap_amount = talgo_burn(tmp_swap_amount) + else: + tmp_swap_amount, _ = tinyman_swap(pool_address, "fixed-input", tmp_asset_in, tmp_asset_out, tmp_swap_amount, 1) + end + end + assert(tmp_asset_out == output_asset_id) + + log(ARC28Event("swap(uint64,uint64,uint64,uint64)", itob(input_asset_id), itob(output_asset_id), itob(input_amount), itob(tmp_swap_amount))) + return +end + + +@public() +func noop(): + return +end + + +@public() +func asset_opt_in(asset_ids: int[8]): + # Required Algo to cover minimum balance increase must be supplied. + # It is not checked explicitly. + # Using extra balance is allowed. + for i in 0:8: + opt_in_to_asset_if_needed(asset_ids[i]) + end + return +end + + +# The current manager can propose a new manager. The manager will not be changed until the proposed manager calls accept_manager. +# The current manager can propose multiple times, overwriting the previous proposal. +# permission: manager +@public() +func propose_manager(new_manager: bytes[32]): + assert(Txn.Sender == app_global_get("manager")) + + app_global_put("proposed_manager", new_manager) + log(ARC28Event("propose_manager(address)", new_manager)) + return +end + + +# The proposed manager must call this function to become the manager. +# permission: proposed_manager +@public() +func accept_manager(): + bytes proposed_manager = app_global_get("proposed_manager") + assert(Txn.Sender == proposed_manager) + + app_global_put("manager", proposed_manager) + app_global_put("proposed_manager", "") + log(ARC28Event("accept_manager(address)", proposed_manager)) + return +end + + +@public() +func set_extra_collector(new_collector: bytes[32]): + # Set a new extra collector, only manager can call this method + # Txn: AppCall from manager + + assert(Txn.Sender == app_global_get("manager")) + app_global_put("extra_collector", new_collector) + return +end + + +# Permission: extra_collector +@public() +func claim_extra(asset_id: int): + # Transfer any extra (donations) to the extra_collector + # It must be the first txn of the group. + assert(Txn.GroupIndex == 0) + assert(Txn.Sender == app_global_get("extra_collector")) + + int asset_amount + int extra_asset_id + int asset_count = Txn.NumAssets + + asset_amount = get_balance(Global.CurrentApplicationAddress, asset_id) + assert(asset_amount) + transfer(asset_id, asset_amount, Global.CurrentApplicationAddress, app_global_get("extra_collector")) + return +end + + +#### ---------------------------------------- Internal Functions ---------------------------------------------- #### + +func talgo_mint(algo_amount: int) int: + inner_group: + inner_txn: + TypeEnum: Pay + Fee: 0 + Receiver: app_global_get("talgo_app_address") + Amount: algo_amount + end + inner_txn: + TypeEnum: Appl + Fee: 0 + ApplicationID: app_global_get("talgo_app_id") + ApplicationArgs[0]: "mint" + ApplicationArgs[1]: itob(algo_amount) + end + end + int output_amount = extract_uint64(Itxn.LastLog, 44) + return output_amount +end + + +func talgo_burn(talgo_amount: int) int: + inner_group: + inner_txn: + TypeEnum: Axfer + Fee: 0 + AssetReceiver: app_global_get("talgo_app_address") + AssetAmount: talgo_amount + XferAsset: app_global_get("talgo_asset_id") + end + inner_txn: + TypeEnum: Appl + Fee: 0 + ApplicationID: app_global_get("talgo_app_id") + ApplicationArgs[0]: "burn" + ApplicationArgs[1]: itob(talgo_amount) + end + end + int output_amount = extract_uint64(Itxn.LastLog, 44) + return output_amount +end + + +func tinyman_swap(pool_address: bytes, mode: bytes, asset_in_id: int, asset_out_id: int, asset_input_amount: int, minimum_output_amount: int) int, int: + int initial_input_balance = get_balance(Global.CurrentApplicationAddress, asset_in_id) + int initial_output_balance = get_balance(Global.CurrentApplicationAddress, asset_out_id) + + if asset_in_id: + inner_group: + inner_txn: + TypeEnum: Axfer + Fee: 0 + AssetReceiver: pool_address + AssetAmount: asset_input_amount + XferAsset: asset_in_id + end + inner_txn: + TypeEnum: Appl + Fee: 0 + ApplicationID: app_global_get("tinyman_app_id") + ApplicationArgs[0]: "swap" + ApplicationArgs[1]: mode + ApplicationArgs[2]: itob(minimum_output_amount) + Accounts[0]: pool_address + Assets[0]: asset_in_id + Assets[1]: asset_out_id + Note: Txn.Note + end + end + else: + inner_group: + inner_txn: + TypeEnum: Pay + Fee: 0 + Receiver: pool_address + Amount: asset_input_amount + end + inner_txn: + TypeEnum: Appl + Fee: 0 + ApplicationID: app_global_get("tinyman_app_id") + ApplicationArgs[0]: "swap" + ApplicationArgs[1]: mode + ApplicationArgs[2]: itob(minimum_output_amount) + Accounts[0]: pool_address + Assets[0]: asset_in_id + Assets[1]: asset_out_id + Note: Txn.Note + end + end + end + + int final_input_balance = get_balance(Global.CurrentApplicationAddress, asset_in_id) + int final_output_balance = get_balance(Global.CurrentApplicationAddress, asset_out_id) + int output_amount = final_output_balance - initial_output_balance + int change_amount = final_input_balance - (initial_input_balance - asset_input_amount) + return output_amount, change_amount +end + + +func opt_in_to_asset_if_needed(asset_id: int): + if asset_id: + int is_opted_in + is_opted_in, _ = asset_holding_get(AssetBalance, Global.CurrentApplicationAddress, asset_id) + + if is_opted_in == 0: + transfer(asset_id, 0, Global.CurrentApplicationAddress, Global.CurrentApplicationAddress) + end + end + return +end + + +func get_balance(account_address: bytes, asset_id: int) int: + int balance = 0 + if !asset_id: + balance = balance(account_address) - min_balance(account_address) + else: + _, balance = asset_holding_get(AssetBalance, account_address, asset_id) + end + return balance +end + + +func transfer(asset_id: int, amount: int, sender: bytes, receiver: bytes): + if !asset_id: + inner_txn: + TypeEnum: Pay + Sender: sender + Receiver: receiver + Amount: amount + Fee: 0 + end + else: + inner_txn: + TypeEnum: Axfer + Sender: sender + AssetReceiver: receiver + AssetAmount: amount + XferAsset: asset_id + Fee: 0 + end + end + return +end diff --git a/swap_router/contracts/v3/swap_router_v3_clear_state.tl b/swap_router/contracts/v3/swap_router_v3_clear_state.tl new file mode 100644 index 0000000..3bd1bf7 --- /dev/null +++ b/swap_router/contracts/v3/swap_router_v3_clear_state.tl @@ -0,0 +1,4 @@ +#pragma version 10 +#tealish version git+https://github.com/Hipo/tealish.git@d7441973671cf6b79dd55843016892f4b86ceeba + +exit(1) diff --git a/swap_router/sdk/__init__.py b/swap_router/sdk/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/swap_router/sdk/struct.py b/swap_router/sdk/struct.py new file mode 100644 index 0000000..5616634 --- /dev/null +++ b/swap_router/sdk/struct.py @@ -0,0 +1,162 @@ +import json +import re +from typing import Any, Dict + + +MINIMUM_BALANCE_REQUIREMENT_PER_BOX = 2_500 +MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE = 400 + + +class StructRegistry: + def __init__(self): + self.struct_definitions: Dict[str, Dict] = {} + + def load_from_file(self, filepath: str) -> None: + """Load struct definitions from a JSON file.""" + with open(filepath, 'r') as f: + data = json.load(f) + self.load_from_dict(data.get('structs', {})) + + def load_from_dict(self, struct_dict: Dict) -> None: + """Load struct definitions from a dictionary.""" + self.struct_definitions.update(struct_dict) + + def get_type(self, name: str) -> Any: + """Get the appropriate type handler for a given type name.""" + if name == "int": + return TealishInt() + elif name.startswith("uint"): + return TealishInt() + elif name.startswith("bytes"): + return TealishBytes() + elif name in self.struct_definitions: + return Struct(name=name, manager=self, **self.struct_definitions[name]) + elif "[" in name: + name, length = re.match(r"([A-Za-z_0-9]+)\[(\d+)\]", name).groups() + return ArrayData( + Struct(name=name, manager=self, **self.struct_definitions[name]), + int(length) + ) + else: + raise KeyError(f"Unknown type: {name}") + + def get_struct(self, name: str) -> 'Struct': + """Get a Struct instance by name.""" + if name not in self.struct_definitions: + raise KeyError(f"Struct '{name}' not found") + return Struct(name=name, **self.struct_definitions[name]) + + +STRUCT_REGISTRY = StructRegistry() + + +def register_struct_file(filepath: str) -> None: + STRUCT_REGISTRY.load_from_file(filepath) + + +def get_struct(name: str) -> 'Struct': + return STRUCT_REGISTRY.get_struct(name) + + +class Struct(): + def __init__(self, name, size, fields): + self._name = name + self._size = size + self._fields = fields + self._data = None + + def __call__(self, data=None) -> Any: + if data is None: + data = bytearray(self._size) + struct = Struct(self._name, self._size, self._fields) + struct._data = memoryview(data) + return struct + + def __getattribute__(self, name: str) -> Any: + if name.startswith("_"): + return super().__getattribute__(name) + field = self._fields[name] + start = field["offset"] + end = field["offset"] + field["size"] + value = self._data[start:end] + type = STRUCT_REGISTRY.get_type(field["type"]) + return type(value) + + def __setattr__(self, name: str, value: Any) -> None: + if name.startswith("_"): + return super().__setattr__(name, value) + field = self._fields[name] + start = field["offset"] + end = field["offset"] + field["size"] + if field["type"] in ("int",): + value = value.to_bytes(field["size"], "big") + if isinstance(value, (Struct, ArrayData)): + value = value._data + self._data[start:end] = value + + def __setitem__(self, index, value): + if isinstance(value, (Struct, ArrayData)): + value = value._data + self._data[:] = value + + def __str__(self) -> str: + return repr(bytes(self._data)) + + def __repr__(self) -> str: + fields = {f: getattr(self, f) for f in self._fields} + return f"{self._name}({fields})" + + def __len__(self): + return len(self._data) + + def __conform__(self, protocol): + return bytes(self._data) + + def __bytes__(self): + return bytes(self._data.tobytes()) + + +class ArrayData(): + def __init__(self, struct, length): + self._struct = struct + self._length = length + + def __call__(self, data=None) -> Any: + if data is None: + data = bytearray(self._struct._size * self.length) + self._data = memoryview(data) + return self + + def __getitem__(self, index): + offset = self._struct._size * index + end = offset + self._struct._size + value = self._data[offset:end] + return self._struct(value) + + def __setitem__(self, index, value): + offset = self._struct._size * index + end = offset + self._struct._size + if isinstance(value, Struct): + value = value._data + self._data[offset:end] = value + + def __repr__(self) -> str: + return ", ".join(repr(self[i]) for i in range(self._length)) + + +class TealishInt(): + def __call__(self, value) -> Any: + return int.from_bytes(value, "big") + + +class TealishBytes(): + def __call__(self, value) -> Any: + return value + + +def get_box_costs(boxes): + cost = MINIMUM_BALANCE_REQUIREMENT_PER_BOX + for name, struct in boxes.items(): + cost += len(name) * MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE + cost += struct._size * MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE + return cost diff --git a/swap_router/sdk/v3/client.py b/swap_router/sdk/v3/client.py new file mode 100644 index 0000000..d20caf9 --- /dev/null +++ b/swap_router/sdk/v3/client.py @@ -0,0 +1,263 @@ +from collections import defaultdict +from typing import List, Tuple + +from algosdk import transaction +from algosdk.encoding import decode_address, encode_address +from algosdk.logic import get_application_address +from algosdk.constants import ZERO_ADDRESS + +from tinyman.utils import int_to_bytes + +from sdk.base_client import BaseClient +from sdk.utils import int_array, bytes_array + + +class SwapRouterClient(BaseClient): + + def __init__(self, algod, app_id, tinyman_amm_app_id, talgo_app_id, user_address, user_sk) -> None: + super().__init__(algod, app_id, user_address, user_sk) + self.amm_app_id = tinyman_amm_app_id + self.talgo_app_id = talgo_app_id + state = self.get_globals(talgo_app_id) + self.talgo_app_address = encode_address(state[b"account_0"]) + self.talgo_asset_id = state[b"talgo_asset_id"] + self.talgo_app_accounts = [encode_address(state[b"account_%i" % i]) for i in range(5)] + + def swap(self, input_amount, output_amount, route, pools): + optins = [a for a in route if a and not self.is_opted_in(self.application_address, a)] + + transactions = [ + self.get_optin_if_needed_txn(self.user_address, route[-1]) + ] + + sp = self.get_suggested_params() + transaction_parameters = self.prepare_swap_group_transaction_parameters(input_amount, output_amount, route, pools, optins) + transactions.extend(self.get_transactions_from_parameters(transaction_parameters)) + + inner_txns = sum(params.get("inner_txns", 0) for params in transaction_parameters) + return self._submit(transactions, additional_fees=inner_txns) + + def prepare_swap_group_transaction_parameters(self, input_asset_id, output_asset_id, input_amount, output_amount, routes, pool_mapping, app_asset_optins=[]): + transaction_dicts = [] + inner_transaction_count = 0 + + # Prepare app asset opt-in transactions. + assert len(app_asset_optins) <= 8 + if app_asset_optins: + transaction_dicts.append( + dict( + type="appl", + app_id=self.app_id, + args=["asset_opt_in", int_array(app_asset_optins, 8, 0)], + apps=[self.amm_app_id], + assets=app_asset_optins, + inner_txns=len(app_asset_optins), + ) + ) + inner_transaction_count += len(app_asset_optins) + + # Prepare Axfer/Pay + transaction_dicts.append( + dict( + type="axfer" if input_asset_id else "pay", + receiver=self.application_address, + amount=input_amount, + asset_id=input_asset_id, + ), + ) + + # For each route, group (input_asset, output_asset, pool) + is_talgo_app_used = False + talgo_app_address = get_application_address(self.talgo_app_id) + + swap_pair_pool_mapping: List[List[Tuple[int, int, str]]] = [] + for route, pool_addresses in zip(routes, pool_mapping): + pair_pool_mapping = [] + for index in range(len(pool_addresses)): + pool_address = pool_addresses[index] + input_asset = route[index] + output_asset = route[index + 1] + + if pool_address == talgo_app_address: + is_talgo_app_used = True + continue + + pair_pool_mapping.append((input_asset, output_asset, pool_address)) + swap_pair_pool_mapping.append(pair_pool_mapping) + + ref_groups = [] + for pair_pool_mapping in swap_pair_pool_mapping: + ref_group = [] + for index in range(0, len(pair_pool_mapping), 2): + refs = defaultdict(lambda: []) + for input_asset, output_asset, pool_address in pair_pool_mapping[index: index+2]: + refs['accounts'].append(pool_address) + refs['assets'].append(input_asset) + refs['assets'].append(output_asset) + refs["assets"] = list(set(refs["assets"])) # Remove duplicate intermediary asset. + ref_group.append(refs) + ref_groups.append(ref_group) + + swap_txn_dicts = [] + # Prepare `swap` transactions. + for route, pool_addresses, ref_group in zip(routes, pool_mapping, ref_groups): + route_arg = int_array(elements=route, size=8, default=0) + pools_arg = bytes_array(elements=[decode_address(addr) for addr in pool_addresses], size=8, default=decode_address(ZERO_ADDRESS)) + swaps = len(pool_addresses) + + swap_txn_dict = dict( + type="appl", + app_id=self.app_id, + args=["swap", input_amount, route_arg, pools_arg, swaps], + apps=[self.amm_app_id], + accounts=ref_group[0]["accounts"], + assets=ref_group[0]["assets"], + inner_txns=(swaps * 3) + 1, + ) + + inner_transaction_count += (swaps * 3) + 1 + swap_txn_dicts.append(swap_txn_dict) + + for refs in ref_group[1:]: + swap_txn_dicts.append( + dict( + type="appl", + app_id=self.app_id, + args=["noop"], + apps=[self.amm_app_id], + accounts=refs["accounts"], + assets=refs["assets"], + ) + ) + + if is_talgo_app_used: + swap_txn_dicts.append( + dict( + type="appl", + app_id=self.app_id, + args=["noop"], + apps=[self.amm_app_id], + accounts=refs["accounts"], + assets=refs["assets"], + ) + ) + + # Prepare `start_swap_group` transaction. + index_diff = len(swap_txn_dicts) + 1 + + transaction_dicts.append( + dict( + type="appl", + app_id=self.app_id, + args=[ + "start_swap_group", + int_to_bytes(input_asset_id), + int_to_bytes(output_asset_id), + int_to_bytes(input_amount), + int_to_bytes(index_diff) + ], + assets=[output_asset_id] + ) + ) + transaction_dicts.extend(swap_txn_dicts) + + # Prepare `end_swap_group` transaction. + transaction_dicts.append( + dict( + type="appl", + app_id=self.app_id, + args=[ + "end_swap_group", + int_to_bytes(input_asset_id), + int_to_bytes(output_asset_id), + int_to_bytes(input_amount), + int_to_bytes(output_amount), + int_to_bytes(index_diff) + ], + assets=[output_asset_id] + ) + ) + + return transaction_dicts + + def get_transactions_from_parameters(self, transaction_parameters, sp): + transactions = [] + for params in transaction_parameters: + if params["type"] == "pay": + transactions.append(transaction.PaymentTxn( + sender=self.user_address, + sp=sp, + receiver=params["receiver"], + amt=params["amount"], + )) + elif params["type"] == "axfer": + transactions.append(transaction.AssetTransferTxn( + sender=self.user_address, + sp=sp, + receiver=params["receiver"], + amt=params["amount"], + index=params["asset_id"], + )) + elif params["type"] == "appl": + transactions.append(transaction.ApplicationNoOpTxn( + sender=self.user_address, + sp=sp, + index=params["app_id"], + app_args=params["args"], + accounts=params.get("accounts"), + foreign_assets=params.get("assets"), + foreign_apps=params.get("apps"), + )) + + return transactions + + +class SwapRouterManagerClient: + def claim_extra(self, asset_id): + sp = self.get_suggested_params() + txns = [ + transaction.ApplicationNoOpTxn( + sender=self.user_address, + sp=sp, + index=self.app_id, + app_args=[b"claim_extra", asset_id], + foreign_assets=[asset_id], + ) + ] + return self._submit(txns, additional_fees=1) + + def set_extra_collector(self, new_collector): + sp = self.get_suggested_params() + txns = [ + transaction.ApplicationNoOpTxn( + sender=self.user_address, + sp=sp, + index=self.app_id, + app_args=[b"set_extra_collector", decode_address(new_collector)], + ) + ] + return self._submit(txns, additional_fees=0) + + def propose_manager(self, new_manager): + sp = self.get_suggested_params() + txns = [ + transaction.ApplicationNoOpTxn( + sender=self.user_address, + sp=sp, + index=self.app_id, + app_args=[b"propose_manager", decode_address(new_manager)], + ) + ] + return self._submit(txns, additional_fees=0) + + def accept_manager(self): + sp = self.get_suggested_params() + txns = [ + transaction.ApplicationNoOpTxn( + sender=self.user_address, + sp=sp, + index=self.app_id, + app_args=[b"accept_manager"], + ) + ] + return self._submit(txns, additional_fees=0) \ No newline at end of file From 34282b93e49d431ec6b8653fee41524735865945 Mon Sep 17 00:00:00 2001 From: etzellux Date: Tue, 8 Jul 2025 12:29:30 +0300 Subject: [PATCH 4/9] fix swap_router_v3_approval start_swap_group method --- .../contracts/v3/swap_router_v3_approval.tl | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/swap_router/contracts/v3/swap_router_v3_approval.tl b/swap_router/contracts/v3/swap_router_v3_approval.tl index 1d3bc26..e71fa0c 100644 --- a/swap_router/contracts/v3/swap_router_v3_approval.tl +++ b/swap_router/contracts/v3/swap_router_v3_approval.tl @@ -66,11 +66,11 @@ func start_swap_group(input_asset_id: int, output_asset_id: int, total_input_amo if Gtxn[input_txn_index].TypeEnum == Pay: assert(!input_asset_id) assert(Gtxn[input_txn_index].Receiver == Global.CurrentApplicationAddress) - assert(Gtxn[input_txn_index].Amount == input_amount) + assert(Gtxn[input_txn_index].Amount == total_input_amount) elif Gtxn[input_txn_index].TypeEnum == Axfer: assert(Gtxn[input_txn_index].XferAsset == input_asset_id) assert(Gtxn[input_txn_index].AssetReceiver == Global.CurrentApplicationAddress) - assert(Gtxn[input_txn_index].AssetAmount == input_amount) + assert(Gtxn[input_txn_index].AssetAmount == total_input_amount) else: Error() end @@ -86,10 +86,10 @@ func start_swap_group(input_asset_id: int, output_asset_id: int, total_input_amo bytes apaa0 = Gtxn[txn_index + i].ApplicationArgs[0] if apaa0 == "swap": - tmp_total_input_amount = tmp_total_input_amount + btoi(Gtxn[txn_index].ApplicationArgs[1]) + tmp_total_input_amount = tmp_total_input_amount + btoi(Gtxn[txn_index + i].ApplicationArgs[1]) - int[8] route = Cast(Gtxn[txn_index].ApplicationArgs[3], int[8]) - int swaps = btoi(Gtxn[txn_index].ApplicationArgs[5]) + int[8] route = Cast(Gtxn[txn_index + i].ApplicationArgs[2], int[8]) + int swaps = btoi(Gtxn[txn_index + i].ApplicationArgs[4]) assert(route[0] == input_asset_id) assert(route[swaps] == output_asset_id) @@ -108,13 +108,13 @@ func start_swap_group(input_asset_id: int, output_asset_id: int, total_input_amo assert(Gtxn[txn_index + index_diff].OnCompletion == NoOp) assert(Gtxn[txn_index + index_diff].ApplicationID == Global.CurrentApplicationID) assert(Gtxn[txn_index + index_diff].ApplicationArgs[0] == "end_swap_group") - assert(Gtxn[txn_index + index_diff].ApplicationArgs[1] == input_asset_id) - assert(Gtxn[txn_index + index_diff].ApplicationArgs[2] == output_asset_id) - assert(Gtxn[txn_index + index_diff].ApplicationArgs[3] == total_input_amount) + assert(Gtxn[txn_index + index_diff].ApplicationArgs[1] == itob(input_asset_id)) + assert(Gtxn[txn_index + index_diff].ApplicationArgs[2] == itob(output_asset_id)) + assert(Gtxn[txn_index + index_diff].ApplicationArgs[3] == itob(total_input_amount)) - int current_output_asset_balance + int current_output_asset_balance = 0 if output_asset_id: - current_output_asset_balance, _ = asset_holding_get(AssetBalance, Global.CurrentApplicationAddress, output_asset_id) + _, current_output_asset_balance = asset_holding_get(AssetBalance, Global.CurrentApplicationAddress, output_asset_id) else: current_output_asset_balance = balance(Global.CurrentApplicationAddress) end From b1bc44fec77ee86592aef81ac1a8ffe45809f40a Mon Sep 17 00:00:00 2001 From: etzellux Date: Tue, 8 Jul 2025 12:31:26 +0300 Subject: [PATCH 5/9] fix swap_router_v3_approval end_swap_group method --- swap_router/contracts/v3/swap_router_v3_approval.tl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/swap_router/contracts/v3/swap_router_v3_approval.tl b/swap_router/contracts/v3/swap_router_v3_approval.tl index e71fa0c..b8a56a1 100644 --- a/swap_router/contracts/v3/swap_router_v3_approval.tl +++ b/swap_router/contracts/v3/swap_router_v3_approval.tl @@ -135,11 +135,11 @@ func end_swap_group(input_asset_id: int, output_asset_id: int, total_input_amoun assert(Gtxn[txn_index - index_diff].OnCompletion == NoOp) assert(Gtxn[txn_index - index_diff].ApplicationID == Global.CurrentApplicationID) assert(Gtxn[txn_index - index_diff].ApplicationArgs[0] == "start_swap_group") - assert(Gtxn[txn_index - index_diff].ApplicationArgs[4] == index_diff) + assert(Gtxn[txn_index - index_diff].ApplicationArgs[4] == itob(index_diff)) - int current_output_asset_balance + int current_output_asset_balance = 0 if output_asset_id: - current_output_asset_balance, _ = asset_holding_get(AssetBalance, Global.CurrentApplicationAddress, output_asset_id) + _, current_output_asset_balance = asset_holding_get(AssetBalance, Global.CurrentApplicationAddress, output_asset_id) else: current_output_asset_balance = balance(Global.CurrentApplicationAddress) end From 8d2f0308d9f2c764060f9d4f262dda3565f291f1 Mon Sep 17 00:00:00 2001 From: etzellux Date: Tue, 8 Jul 2025 12:32:46 +0300 Subject: [PATCH 6/9] add initial testing for swap_router_v3 --- swap_router/tests/v3/__init__.py | 0 swap_router/tests/v3/test_swap_router_v3.py | 451 ++++++++++++++++++++ 2 files changed, 451 insertions(+) create mode 100644 swap_router/tests/v3/__init__.py create mode 100644 swap_router/tests/v3/test_swap_router_v3.py diff --git a/swap_router/tests/v3/__init__.py b/swap_router/tests/v3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/swap_router/tests/v3/test_swap_router_v3.py b/swap_router/tests/v3/test_swap_router_v3.py new file mode 100644 index 0000000..12be663 --- /dev/null +++ b/swap_router/tests/v3/test_swap_router_v3.py @@ -0,0 +1,451 @@ +from Cryptodome.Hash import SHA512 +from algojig import TealishProgram +from algojig import get_suggested_params +from algojig.exceptions import LogicEvalError +from algojig.ledger import JigLedger + +from algosdk.abi import Argument +from algosdk.account import generate_account +from algosdk.encoding import decode_address +from algosdk import transaction +from algosdk.logic import get_application_address +from algosdk.constants import ZERO_ADDRESS + +from swap_router.tests.common import MINIMUM_BALANCE, MAX_ASSET_AMOUNT, AMM_APPLICATION_ID, SWAP_ROUTER_APP_ID, SWAP_ROUTER_ADDRESS +from swap_router.sdk.v3.client import SwapRouterClient + +from tinyman.utils import TransactionGroup, int_to_bytes +from swap_router.tests.core import BaseTestCase +from swap_router.tests.utils import bytes_array, int_array, get_event_signature, get_selector, JigAlgod + +swap_router_program = TealishProgram('swap_router/contracts/v3/swap_router_v3_approval.tl') +swap_clear_state_program = TealishProgram('swap_router/contracts/v3/swap_router_v3_clear_state.tl') + + +class CreateAppTestCase(BaseTestCase): + @classmethod + def setUpClass(cls): + cls.sp = get_suggested_params() + cls.app_creator_sk, cls.app_creator_address = generate_account() + + def setUp(self): + self.ledger = JigLedger() + self.ledger.set_account_balance(self.app_creator_address, 10_000_000) + self.create_amm_app() + self.create_talgo_app() + + def test_create_app(self): + txn = transaction.ApplicationCreateTxn( + sender=self.app_creator_address, + sp=self.sp, + on_complete=transaction.OnComplete.NoOpOC, + app_args=["create_application", AMM_APPLICATION_ID, self.talgo_app_id, self.talgo_asset_id], + approval_program=swap_router_program.bytecode, + clear_program=swap_clear_state_program.bytecode, + global_schema=transaction.StateSchema(num_uints=8, num_byte_slices=8), + local_schema=transaction.StateSchema(num_uints=0, num_byte_slices=0), + foreign_apps=[AMM_APPLICATION_ID, self.talgo_app_id], + extra_pages=0, + ) + stxn = txn.sign(self.app_creator_sk) + + block = self.ledger.eval_transactions(transactions=[stxn]) + block_txns = block[b'txns'] + + txn = block_txns[0] + app_id = txn[b'apid'] + global_state = self.ledger.get_global_state(app_id) + self.assertEqual( + global_state, + { + b"tinyman_app_id": AMM_APPLICATION_ID, + b"manager": decode_address(self.app_creator_address), + b"proposed_manager": None, + b"extra_collector": decode_address(self.app_creator_address), + b"talgo_app_address": decode_address(self.talgo_app_address), + b"talgo_app_id": self.talgo_app_id, + b"talgo_asset_id": self.talgo_asset_id, + } + ) + + +class SwapRouterTestCase(BaseTestCase): + @classmethod + def setUpClass(cls): + cls.sp = get_suggested_params() + cls.app_creator_sk, cls.app_creator_address = generate_account() + cls.user_sk, cls.user_addr = generate_account() + + cls.asset_a_id = 1002 + cls.asset_b_id = 1003 + cls.asset_c_id = 1004 + cls.asset_d_id = 1005 + + def create_swap_router_app(self): + self.ledger.create_app(app_id=SWAP_ROUTER_APP_ID, approval_program=swap_router_program, creator=self.app_creator_address) + self.ledger.set_account_balance(SWAP_ROUTER_ADDRESS, MINIMUM_BALANCE) + self.ledger.set_global_state( + SWAP_ROUTER_APP_ID, + { + b'tinyman_app_id': AMM_APPLICATION_ID, + b'manager': decode_address(self.app_creator_address), + b'extra_collector': decode_address(self.app_creator_address), + b'talgo_app_address': decode_address(self.talgo_app_address), + b'talgo_app_id': self.talgo_app_id, + b'talgo_asset_id': self.talgo_asset_id, + } + ) + + def create_pools_for_multi_route(self): + self.reset_ledger() + + self.ledger.add(self.user_addr, 10_000_000, self.talgo_asset_id) + + # TALGO, A + self.pool_0_asset_1_id, self.pool_0_asset_2_id = sorted([self.talgo_asset_id, self.asset_a_id], reverse=True) + # ALGO, B + self.pool_1_asset_1_id, self.pool_1_asset_2_id = sorted([0, self.asset_b_id], reverse=True) + # B, C + self.pool_2_asset_1_id, self.pool_2_asset_2_id = sorted([self.asset_b_id, self.asset_c_id], reverse=True) + # C, D + self.pool_3_asset_1_id, self.pool_3_asset_2_id = sorted([self.asset_c_id, self.asset_d_id], reverse=True) + # A, B + self.pool_4_asset_1_id, self.pool_4_asset_2_id = sorted([self.asset_a_id, self.asset_b_id], reverse=True) + + self.pool_0_address, self.pool_0_token_asset_id = self.bootstrap_pool(self.pool_0_asset_1_id, self.pool_0_asset_2_id) + self.ledger.opt_in_asset(self.user_addr, self.pool_0_token_asset_id) + + self.pool_1_address, self.pool_1_token_asset_id = self.bootstrap_pool(self.pool_1_asset_1_id, self.pool_1_asset_2_id) + self.ledger.opt_in_asset(self.user_addr, self.pool_1_token_asset_id) + + self.pool_2_address, self.pool_2_token_asset_id = self.bootstrap_pool(self.pool_2_asset_1_id, self.pool_2_asset_2_id) + self.ledger.opt_in_asset(self.user_addr, self.pool_2_token_asset_id) + + self.pool_3_address, self.pool_3_token_asset_id = self.bootstrap_pool(self.pool_3_asset_1_id, self.pool_3_asset_2_id) + self.ledger.opt_in_asset(self.user_addr, self.pool_3_token_asset_id) + + self.pool_4_address, self.pool_4_token_asset_id = self.bootstrap_pool(self.pool_4_asset_1_id, self.pool_4_asset_2_id) + self.ledger.opt_in_asset(self.user_addr, self.pool_4_token_asset_id) + + self.set_initial_pool_liquidity( + pool_address=self.pool_0_address, + asset_1_id=self.pool_0_asset_1_id, + asset_2_id=self.pool_0_asset_2_id, + pool_token_asset_id=self.pool_0_token_asset_id, + asset_1_reserves=1_000_000, + asset_2_reserves=1_000_000, + liquidity_provider_address=self.user_addr + ) + + self.set_initial_pool_liquidity( + pool_address=self.pool_1_address, + asset_1_id=self.pool_1_asset_1_id, + asset_2_id=self.pool_1_asset_2_id, + pool_token_asset_id=self.pool_1_token_asset_id, + asset_1_reserves=1_000_000, + asset_2_reserves=1_000_000, + liquidity_provider_address=self.user_addr + ) + + self.set_initial_pool_liquidity( + pool_address=self.pool_2_address, + asset_1_id=self.pool_2_asset_1_id, + asset_2_id=self.pool_2_asset_2_id, + pool_token_asset_id=self.pool_2_token_asset_id, + asset_1_reserves=1_000_000, + asset_2_reserves=1_000_000, + liquidity_provider_address=self.user_addr + ) + + self.set_initial_pool_liquidity( + pool_address=self.pool_3_address, + asset_1_id=self.pool_3_asset_1_id, + asset_2_id=self.pool_3_asset_2_id, + pool_token_asset_id=self.pool_3_token_asset_id, + asset_1_reserves=1_000_000, + asset_2_reserves=1_000_000, + liquidity_provider_address=self.user_addr + ) + + self.set_initial_pool_liquidity( + pool_address=self.pool_4_address, + asset_1_id=self.pool_4_asset_1_id, + asset_2_id=self.pool_4_asset_2_id, + pool_token_asset_id=self.pool_4_token_asset_id, + asset_1_reserves=1_000_000, + asset_2_reserves=1_000_000, + liquidity_provider_address=self.user_addr + ) + + +class AssetOptInTestCase(SwapRouterTestCase): + + def setUp(self): + self.ledger = JigLedger() + self.create_amm_app() + self.create_talgo_app() + self.create_swap_router_app() + self.ledger.set_account_balance(self.user_addr, 1_000_000) + + self.ledger.create_asset(asset_id=self.asset_a_id) + self.ledger.create_asset(asset_id=self.asset_b_id) + self.ledger.create_asset(asset_id=self.asset_c_id) + + def test_asset_opt_in(self): + # Assume that min balance requirement is already covered. + self.ledger.set_account_balance(SWAP_ROUTER_ADDRESS, MINIMUM_BALANCE * 100) + + txn_group = [ + transaction.ApplicationNoOpTxn( + sender=self.user_addr, + sp=self.sp, + index=SWAP_ROUTER_APP_ID, + app_args=["asset_opt_in", int_array([self.asset_a_id, self.asset_b_id, self.asset_c_id], 8, 0)], + foreign_assets=[self.asset_a_id, self.asset_b_id, self.asset_c_id], + ) + ] + txn_group[0].fee = 1000 + 3000 + + txn_group = transaction.assign_group_id(txn_group) + stxns = [ + txn_group[0].sign(self.user_sk), + ] + block = self.ledger.eval_transactions(stxns) + txns = block[b'txns'] + inner_transactions = txns[0][b'dt'][b'itx'] + self.ledger.get_account_balance(SWAP_ROUTER_ADDRESS) + self.assertEqual(len(inner_transactions), 3) + + + +class SwapTestCase(SwapRouterTestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + swap_event_args = [ + Argument(arg_type="uint64", name="input_asset_id"), + Argument(arg_type="uint64", name="output_asset_id"), + Argument(arg_type="uint64", name="input_amount"), + Argument(arg_type="uint64", name="output_amount") + ] + swap_event_signature = get_event_signature(event_name="swap", event_args=swap_event_args) + cls.swap_event_selector = get_selector(signature=swap_event_signature) + + def reset_ledger(self): + self.ledger = JigLedger() + self.create_amm_app() + self.create_talgo_app() + self.create_swap_router_app() + + self.ledger.set_account_balance(self.user_addr, 100_000_000) + self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_a_id) + self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_b_id) + self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_c_id) + self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_d_id) + + self.ledger.opt_in_asset(SWAP_ROUTER_ADDRESS, self.talgo_asset_id) + self.ledger.opt_in_asset(SWAP_ROUTER_ADDRESS, self.asset_a_id) + self.ledger.opt_in_asset(SWAP_ROUTER_ADDRESS, self.asset_b_id) + self.ledger.opt_in_asset(SWAP_ROUTER_ADDRESS, self.asset_c_id) + self.ledger.opt_in_asset(SWAP_ROUTER_ADDRESS, self.asset_d_id) + self.ledger.move(5 * MINIMUM_BALANCE, sender=self.user_addr, receiver=SWAP_ROUTER_ADDRESS) + + def test_multi_route_swap(self): + self.reset_ledger() + self.create_pools_for_multi_route() + + # Prepare transactions. + total_input_amount = 100_000 + min_output_amount = 90_000 + input_asset_id = self.asset_a_id + output_asset_id = self.asset_b_id + + routes = [ + [self.asset_a_id, self.talgo_asset_id, 0, self.asset_b_id], + [self.asset_a_id, self.asset_b_id] + ] + + pool_mapping = [ + [self.pool_0_address, self.talgo_app_address, self.pool_1_address], + [self.pool_4_address] + ] + inner_txn_count = 1 + 3 * (len(pool_mapping[0]) + len(pool_mapping[1])) + index_diff = 4 + + route_0_arg = int_array(routes[0], 8, 0) + pools_0_arg = bytes_array([decode_address(addr) for addr in pool_mapping[0]], 8, decode_address(ZERO_ADDRESS)) + + route_1_arg = int_array(routes[1], 8, 0) + pools_1_arg = bytes_array([decode_address(addr) for addr in pool_mapping[1]], 8, decode_address(ZERO_ADDRESS)) + + transactions = [ + transaction.AssetTransferTxn( + sender=self.user_addr, + sp=self.sp, + receiver=SWAP_ROUTER_ADDRESS, + amt=total_input_amount, + index=input_asset_id + ), + transaction.ApplicationNoOpTxn( + sender=self.user_addr, + sp=self.sp, + index=SWAP_ROUTER_APP_ID, + app_args=[ + "start_swap_group", + int_to_bytes(input_asset_id), + int_to_bytes(output_asset_id), + int_to_bytes(total_input_amount), + int_to_bytes(index_diff) + ], + foreign_apps=[AMM_APPLICATION_ID], + foreign_assets=[input_asset_id, output_asset_id], + ), + transaction.ApplicationNoOpTxn( + sender=self.user_addr, + sp=self.sp, + index=SWAP_ROUTER_APP_ID, + # input_amount: int, route: int[8], pools: Address[8], swaps: int + app_args=[ + "swap", + int_to_bytes(total_input_amount // 2), + route_0_arg, + pools_0_arg, + int_to_bytes(len(pool_mapping[0])) + ], + accounts=[self.pool_0_address, self.pool_1_address], + foreign_apps=[AMM_APPLICATION_ID], + foreign_assets=[self.asset_a_id, self.talgo_asset_id, 0, self.asset_b_id], + ), + transaction.ApplicationNoOpTxn( + sender=self.user_addr, + sp=self.sp, + index=SWAP_ROUTER_APP_ID, + # input_amount: int, route: int[8], pools: Address[8], swaps: int + app_args=[ + "swap", + int_to_bytes(total_input_amount // 2), + route_1_arg, + pools_1_arg, + int_to_bytes(len(pool_mapping[1])) + ], + accounts=[self.pool_4_address], + foreign_apps=[AMM_APPLICATION_ID], + foreign_assets=[self.asset_a_id, self.asset_b_id], + ), + transaction.ApplicationNoOpTxn( + sender=self.user_addr, + sp=self.sp, + index=SWAP_ROUTER_APP_ID, + app_args=["noop"], + foreign_apps=[self.talgo_app_id], + foreign_assets=[ + self.talgo_asset_id + ], + accounts=[ + self.talgo_app_account_1, + self.talgo_app_account_2, + self.talgo_app_account_3, + self.talgo_app_account_4, + ], + ), + transaction.ApplicationNoOpTxn( + sender=self.user_addr, + sp=self.sp, + index=SWAP_ROUTER_APP_ID, + app_args=[ + "end_swap_group", + int_to_bytes(input_asset_id), + int_to_bytes(output_asset_id), + int_to_bytes(total_input_amount), + int_to_bytes(min_output_amount), + int_to_bytes(index_diff) + ], + foreign_apps=[AMM_APPLICATION_ID], + foreign_assets=[input_asset_id, output_asset_id] + ), + ] + + client = SwapRouterClient(JigAlgod(self.ledger), SWAP_ROUTER_APP_ID, AMM_APPLICATION_ID, self.talgo_app_id, self.user_addr, self.user_sk) + client._submit(transactions, additional_fees=inner_txn_count) + + block = self.ledger.last_block + block_txns = block[b'txns'] + + +class AdminTestCase(SwapRouterTestCase): + + def setUp(self): + self.ledger = JigLedger() + self.create_amm_app() + self.create_talgo_app() + self.create_swap_router_app() + self.ledger.set_account_balance(SWAP_ROUTER_ADDRESS, 2 * MINIMUM_BALANCE, 0) + self.ledger.set_account_balance(self.user_addr, 1_000_000) + + def test_set_new_manager(self): + sk, address1 = generate_account() + self.ledger.set_account_balance(address1, 1_000_000, 0) + + client = SwapRouterClient(JigAlgod(self.ledger), SWAP_ROUTER_APP_ID, AMM_APPLICATION_ID, self.talgo_app_id, self.app_creator_address, self.app_creator_sk) + client.propose_manager(address1) + self.assertEqual(client.get_global(b"proposed_manager"), decode_address(address1)) + + client = SwapRouterClient(JigAlgod(self.ledger), SWAP_ROUTER_APP_ID, AMM_APPLICATION_ID, self.talgo_app_id, address1, sk) + client.accept_manager() + self.assertEqual(client.get_global(b"proposed_manager"), None) + self.assertEqual(client.get_global(b"manager"), decode_address(address1)) + + def test_set_new_manager_fail(self): + sk, address1 = generate_account() + self.ledger.set_account_balance(address1, 1_000_000, 0) + + client = SwapRouterClient(JigAlgod(self.ledger), SWAP_ROUTER_APP_ID, AMM_APPLICATION_ID, self.talgo_app_id, self.user_addr, self.user_sk) + with self.assertRaises(LogicEvalError): + client.propose_manager(address1) + + client = SwapRouterClient(JigAlgod(self.ledger), SWAP_ROUTER_APP_ID, AMM_APPLICATION_ID, self.talgo_app_id, self.app_creator_address, self.app_creator_sk) + client.propose_manager(address1) + self.assertEqual(client.get_global(b"proposed_manager"), decode_address(address1)) + + client = SwapRouterClient(JigAlgod(self.ledger), SWAP_ROUTER_APP_ID, AMM_APPLICATION_ID, self.talgo_app_id, self.user_addr, self.user_sk) + with self.assertRaises(LogicEvalError): + client.accept_manager() + + def test_set_extra_collector(self): + client = SwapRouterClient(JigAlgod(self.ledger), SWAP_ROUTER_APP_ID, AMM_APPLICATION_ID, self.talgo_app_id, self.app_creator_address, self.app_creator_sk) + # set new extra collector + sk, address1 = generate_account() + client.set_extra_collector(address1) + self.assertEqual(client.get_global(b"extra_collector"), decode_address(address1)) + + def test_claim_extra(self): + self.ledger.add(SWAP_ROUTER_ADDRESS, 10_000, 0) + self.ledger.add(SWAP_ROUTER_ADDRESS, 10_000, self.talgo_asset_id) + + client = SwapRouterClient(JigAlgod(self.ledger), SWAP_ROUTER_APP_ID, AMM_APPLICATION_ID, self.talgo_app_id, self.app_creator_address, self.app_creator_sk) + + # set new extra collector + sk, address1 = generate_account() + client.set_extra_collector(address1) + + self.ledger.set_account_balance(address1, 1_000_000, 0) + self.ledger.set_account_balance(address1, 0, self.talgo_asset_id) + + client = SwapRouterClient(JigAlgod(self.ledger), SWAP_ROUTER_APP_ID, AMM_APPLICATION_ID, self.talgo_app_id, address1, sk) + + # claim extra ASA + client.claim_extra(self.talgo_asset_id) + app_txn = [txn for txn in self.ledger.last_block[b'txns'] if txn[b"txn"].get(b"apaa", [None])[0] == b"claim_extra"][0] + inner_transactions = app_txn[b'dt'][b'itx'] + itxn = inner_transactions[-1][b'txn'] + transfer_amount = itxn.get(b'aamt', itxn.get(b'amt', 0)) + self.assertEqual(transfer_amount, 10_000) + + # claim extra Algo + client.claim_extra(0) + app_txn = [txn for txn in self.ledger.last_block[b'txns'] if txn[b"txn"].get(b"apaa", [None])[0] == b"claim_extra"][0] + inner_transactions = app_txn[b'dt'][b'itx'] + itxn = inner_transactions[-1][b'txn'] + transfer_amount = itxn.get(b'aamt', itxn.get(b'amt', 0)) + self.assertEqual(transfer_amount, 10_000) From 374808bf144663f001cb2766353edc386b4b39a1 Mon Sep 17 00:00:00 2001 From: etzellux Date: Tue, 8 Jul 2025 12:33:01 +0300 Subject: [PATCH 7/9] import fixes --- swap_router/sdk/base_client.py | 2 +- swap_router/sdk/v3/client.py | 4 ++-- swap_router/tests/v1/__init__.py | 0 swap_router/tests/v2/__init__.py | 0 swap_router/tests/v2/test_swap_router_v2.py | 4 ++-- 5 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 swap_router/tests/v1/__init__.py create mode 100644 swap_router/tests/v2/__init__.py diff --git a/swap_router/sdk/base_client.py b/swap_router/sdk/base_client.py index 2c328a0..823550e 100644 --- a/swap_router/sdk/base_client.py +++ b/swap_router/sdk/base_client.py @@ -4,7 +4,7 @@ from algosdk.logic import get_application_address from tinyman.utils import TransactionGroup -from sdk.struct import get_struct, get_box_costs +from swap_router.sdk.struct import get_struct, get_box_costs class BaseClient(): diff --git a/swap_router/sdk/v3/client.py b/swap_router/sdk/v3/client.py index d20caf9..333f861 100644 --- a/swap_router/sdk/v3/client.py +++ b/swap_router/sdk/v3/client.py @@ -8,8 +8,8 @@ from tinyman.utils import int_to_bytes -from sdk.base_client import BaseClient -from sdk.utils import int_array, bytes_array +from swap_router.sdk.base_client import BaseClient +from swap_router.sdk.utils import int_array, bytes_array class SwapRouterClient(BaseClient): diff --git a/swap_router/tests/v1/__init__.py b/swap_router/tests/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/swap_router/tests/v2/__init__.py b/swap_router/tests/v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/swap_router/tests/v2/test_swap_router_v2.py b/swap_router/tests/v2/test_swap_router_v2.py index 152b66c..e0806a4 100644 --- a/swap_router/tests/v2/test_swap_router_v2.py +++ b/swap_router/tests/v2/test_swap_router_v2.py @@ -13,8 +13,8 @@ from swap_router.sdk.v2.client import SwapRouterClient from swap_router.tests.common import MINIMUM_BALANCE, MAX_ASSET_AMOUNT, AMM_APPLICATION_ID, SWAP_ROUTER_APP_ID, SWAP_ROUTER_ADDRESS -from tests.core import BaseTestCase -from tests.utils import bytes_array, int_array, get_event_signature, get_selector, JigAlgod +from swap_router.tests.core import BaseTestCase +from swap_router.tests.utils import bytes_array, int_array, get_event_signature, get_selector, JigAlgod swap_router_program = TealishProgram('contracts/v2/swap_router_v2_approval.tl') swap_clear_state_program = TealishProgram('contracts/v2/swap_router_v2_clear_state.tl') From b62c27f026bde2dfa301b379456802fa6b143b2e Mon Sep 17 00:00:00 2001 From: etzellux Date: Thu, 10 Jul 2025 15:05:15 +0300 Subject: [PATCH 8/9] add client test and update client --- swap_router/sdk/v3/client.py | 25 +++++++++------ swap_router/tests/v3/test_swap_router_v3.py | 35 +++++++++++++++++++++ 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/swap_router/sdk/v3/client.py b/swap_router/sdk/v3/client.py index 333f861..59c9b6d 100644 --- a/swap_router/sdk/v3/client.py +++ b/swap_router/sdk/v3/client.py @@ -37,10 +37,12 @@ def swap(self, input_amount, output_amount, route, pools): inner_txns = sum(params.get("inner_txns", 0) for params in transaction_parameters) return self._submit(transactions, additional_fees=inner_txns) - def prepare_swap_group_transaction_parameters(self, input_asset_id, output_asset_id, input_amount, output_amount, routes, pool_mapping, app_asset_optins=[]): + def prepare_swap_group_transaction_parameters(self, input_asset_id, output_asset_id, input_amount_mapping, output_amount, routes, pool_mapping, app_asset_optins=[]): transaction_dicts = [] inner_transaction_count = 0 + total_input_amount = sum(input_amount_mapping) + # Prepare app asset opt-in transactions. assert len(app_asset_optins) <= 8 if app_asset_optins: @@ -61,7 +63,7 @@ def prepare_swap_group_transaction_parameters(self, input_asset_id, output_asset dict( type="axfer" if input_asset_id else "pay", receiver=self.application_address, - amount=input_amount, + amount=total_input_amount, asset_id=input_asset_id, ), ) @@ -100,7 +102,7 @@ def prepare_swap_group_transaction_parameters(self, input_asset_id, output_asset swap_txn_dicts = [] # Prepare `swap` transactions. - for route, pool_addresses, ref_group in zip(routes, pool_mapping, ref_groups): + for route, pool_addresses, input_amount, ref_group in zip(routes, pool_mapping, input_amount_mapping, ref_groups): route_arg = int_array(elements=route, size=8, default=0) pools_arg = bytes_array(elements=[decode_address(addr) for addr in pool_addresses], size=8, default=decode_address(ZERO_ADDRESS)) swaps = len(pool_addresses) @@ -129,16 +131,16 @@ def prepare_swap_group_transaction_parameters(self, input_asset_id, output_asset assets=refs["assets"], ) ) - + if is_talgo_app_used: swap_txn_dicts.append( dict( type="appl", app_id=self.app_id, args=["noop"], - apps=[self.amm_app_id], - accounts=refs["accounts"], - assets=refs["assets"], + apps=[self.amm_app_id, self.talgo_app_id], + accounts=self.talgo_app_accounts[1:], + assets=[self.talgo_asset_id] ) ) @@ -153,7 +155,7 @@ def prepare_swap_group_transaction_parameters(self, input_asset_id, output_asset "start_swap_group", int_to_bytes(input_asset_id), int_to_bytes(output_asset_id), - int_to_bytes(input_amount), + int_to_bytes(total_input_amount), int_to_bytes(index_diff) ], assets=[output_asset_id] @@ -170,7 +172,7 @@ def prepare_swap_group_transaction_parameters(self, input_asset_id, output_asset "end_swap_group", int_to_bytes(input_asset_id), int_to_bytes(output_asset_id), - int_to_bytes(input_amount), + int_to_bytes(total_input_amount), int_to_bytes(output_amount), int_to_bytes(index_diff) ], @@ -180,7 +182,10 @@ def prepare_swap_group_transaction_parameters(self, input_asset_id, output_asset return transaction_dicts - def get_transactions_from_parameters(self, transaction_parameters, sp): + def get_transactions_from_parameters(self, transaction_parameters, sp=None): + if sp is None: + sp = self.get_suggested_params() + transactions = [] for params in transaction_parameters: if params["type"] == "pay": diff --git a/swap_router/tests/v3/test_swap_router_v3.py b/swap_router/tests/v3/test_swap_router_v3.py index 12be663..2bdd7be 100644 --- a/swap_router/tests/v3/test_swap_router_v3.py +++ b/swap_router/tests/v3/test_swap_router_v3.py @@ -371,6 +371,41 @@ def test_multi_route_swap(self): block = self.ledger.last_block block_txns = block[b'txns'] + + def test_multi_route_swap_with_client(self): + self.reset_ledger() + self.create_pools_for_multi_route() + + # Prepare transactions. + total_input_amount = 100_000 + min_output_amount = 90_000 + input_asset_id = self.asset_a_id + output_asset_id = self.asset_b_id + + routes = [ + [self.asset_a_id, self.talgo_asset_id, 0, self.asset_b_id], + [self.asset_a_id, self.asset_b_id] + ] + + pool_mapping = [ + [self.pool_0_address, self.talgo_app_address, self.pool_1_address], + [self.pool_4_address] + ] + inner_txn_count = 1 + 3 * (len(pool_mapping[0]) + len(pool_mapping[1])) + client = SwapRouterClient(JigAlgod(self.ledger), SWAP_ROUTER_APP_ID, AMM_APPLICATION_ID, self.talgo_app_id, self.user_addr, self.user_sk) + transactions = client.prepare_swap_group_transaction_parameters( + input_asset_id=input_asset_id, + output_asset_id=output_asset_id, + input_amount_mapping=[total_input_amount // 2, total_input_amount // 2], + output_amount=min_output_amount, + routes=routes, + pool_mapping=pool_mapping + ) + transactions = client.get_transactions_from_parameters(transaction_parameters=transactions) + client._submit(transactions, additional_fees=inner_txn_count) + + block = self.ledger.last_block + block_txns = block[b'txns'] class AdminTestCase(SwapRouterTestCase): From a73b0766bca5c0f1603d4a2dda7fbf1c94a2fe85 Mon Sep 17 00:00:00 2001 From: etzellux Date: Mon, 14 Jul 2025 12:46:14 +0300 Subject: [PATCH 9/9] update client --- swap_router/sdk/base_client.py | 15 +++++++++------ swap_router/sdk/v3/client.py | 12 +++++++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/swap_router/sdk/base_client.py b/swap_router/sdk/base_client.py index 823550e..acd1750 100644 --- a/swap_router/sdk/base_client.py +++ b/swap_router/sdk/base_client.py @@ -119,9 +119,12 @@ def is_opted_in(self, address, asset_id): def get_optin_if_needed_txn(self, sender, asset_id): if not self.is_opted_in(sender, asset_id): - txn = transaction.AssetOptInTxn( - sender=sender, - sp=self.get_suggested_params(), - index=asset_id, - ) - return txn + return self.get_optin_txn(sender, asset_id) + + def get_optin_txn(self, sender, asset_id): + txn = transaction.AssetOptInTxn( + sender=sender, + sp=self.get_suggested_params(), + index=asset_id, + ) + return txn diff --git a/swap_router/sdk/v3/client.py b/swap_router/sdk/v3/client.py index 59c9b6d..f8553d6 100644 --- a/swap_router/sdk/v3/client.py +++ b/swap_router/sdk/v3/client.py @@ -23,16 +23,18 @@ def __init__(self, algod, app_id, tinyman_amm_app_id, talgo_app_id, user_address self.talgo_asset_id = state[b"talgo_asset_id"] self.talgo_app_accounts = [encode_address(state[b"account_%i" % i]) for i in range(5)] - def swap(self, input_amount, output_amount, route, pools): - optins = [a for a in route if a and not self.is_opted_in(self.application_address, a)] + def swap(self, input_asset_id, output_asset_id, input_amount_mapping, output_amount, routes, pool_mapping): + app_asset_optins = [] + for route in routes: + app_asset_optins.extend([aid for aid in route if aid and not self.is_opted_in(self.application_address, aid)]) transactions = [ - self.get_optin_if_needed_txn(self.user_address, route[-1]) + self.get_optin_if_needed_txn(self.user_address, routes[-1]) ] sp = self.get_suggested_params() - transaction_parameters = self.prepare_swap_group_transaction_parameters(input_amount, output_amount, route, pools, optins) - transactions.extend(self.get_transactions_from_parameters(transaction_parameters)) + transaction_parameters = self.prepare_swap_group_transaction_parameters(input_asset_id, output_asset_id, input_amount_mapping, output_amount, routes, pool_mapping, app_asset_optins) + transactions.extend(self.get_transactions_from_parameters(transaction_parameters, sp)) inner_txns = sum(params.get("inner_txns", 0) for params in transaction_parameters) return self._submit(transactions, additional_fees=inner_txns)