From 9bac951ece2733cac0de199f8b55768c727bac89 Mon Sep 17 00:00:00 2001 From: Fergal Walsh Date: Mon, 20 Dec 2021 15:19:56 +0000 Subject: [PATCH 01/73] Initial work on staking related functionality --- tinyman/v1/staking/__init__.py | 55 +++++++++++++++++++++++++++++++++ tinyman/v1/staking/asc.json | 32 +++++++++++++++++++ tinyman/v1/staking/contracts.py | 9 ++++++ 3 files changed, 96 insertions(+) create mode 100644 tinyman/v1/staking/__init__.py create mode 100644 tinyman/v1/staking/asc.json create mode 100644 tinyman/v1/staking/contracts.py diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py new file mode 100644 index 0000000..c9abec2 --- /dev/null +++ b/tinyman/v1/staking/__init__.py @@ -0,0 +1,55 @@ +from base64 import b64decode, b64encode +from algosdk.future.transaction import ApplicationCreateTxn, OnComplete, StateSchema, ApplicationUpdateTxn, ApplicationNoOpTxn +from tinyman.utils import TransactionGroup, int_to_bytes +from .contracts import staking_app_def + + +def prepare_create_transaction(sender, suggested_params): + txn = ApplicationCreateTxn( + sender=sender, + sp=suggested_params, + on_complete=OnComplete.NoOpOC.real, + approval_program=b64decode(staking_app_def['approval_program']['bytecode']), + clear_program=b64decode(staking_app_def['clear_program']['bytecode']), + global_schema=StateSchema(**staking_app_def['global_state_schema']), + local_schema=StateSchema(**staking_app_def['local_state_schema']), + ) + return TransactionGroup([txn]) + + +def prepare_update_transaction(app_id, sender, suggested_params): + txn = ApplicationUpdateTxn( + index=app_id, + sender=sender, + sp=suggested_params, + approval_program=b64decode(staking_app_def['approval_program']['bytecode']), + clear_program=b64decode(staking_app_def['clear_program']['bytecode']), + ) + return TransactionGroup([txn]) + +def prepare_commit_transaction(app_id, program_id, program_account, pool_asset_id, amount, sender, suggested_params): + txn = ApplicationNoOpTxn( + index=app_id, + sender=sender, + sp=suggested_params, + app_args=['commit', int_to_bytes(amount), int_to_bytes(program_id)], + foreign_assets=[pool_asset_id], + accounts=[program_account], + note=b'tinymanStaking/v1:b' + int_to_bytes(program_id) + int_to_bytes(pool_asset_id) + int_to_bytes(amount) + ) + return TransactionGroup([txn]) + + +def parse_commit_transaction(txn, app_id): + if txn.get('application-transaction'): + app_call = txn['application-transaction'] + if app_call['application-id'] == app_id and app_call['application-args'][0] == b64encode('commit'): + result = {} + result['pooler'] = txn['sender'] + result['program_address'] = app_call['accounts'][0] + result['pool_asset_id'] = app_call['foreign-assets'][0] + result['program_id'] = int.from_bytes(b64decode(app_call['application-args'][2]), 'big') + result['amount'] = int.from_bytes(b64decode(app_call['application-args'][1]), 'big') + result['balance'] = int.from_bytes(b64decode(txn['logs'][0])[8:], 'big') + return result + return None diff --git a/tinyman/v1/staking/asc.json b/tinyman/v1/staking/asc.json new file mode 100644 index 0000000..6cfcce4 --- /dev/null +++ b/tinyman/v1/staking/asc.json @@ -0,0 +1,32 @@ +{ + "repo": "https://github.com/tinymanorg/tinyman-staking", + "ref": "main", + "contracts": { + "staking_app": { + "type": "app", + "approval_program": { + "bytecode": "BSACAQAxGSMSQAAgMRkiEkAAiTEZgQISQACDMRmBBBJAAHsxGYEFEkAAeQA2GgCABmNvbW1pdBJAAAFDgBN0aW55bWFuU3Rha2luZy92MTpiMQVRABMSMQVXEwA1ATQBI1s2GgIXEjQBgQhbNjAAEkQ0AYEQWzYaARcSRCM2MABwAERJFoAIYmFsYW5jZSBMULA2GgEXD0QiQyJDIkMyCTEAEkMyCTEAEkMA", + "address": "NSQUSJZUSE3HABWUHTW6TIEY5MQ55FRIKU7IF2WVLQ6V77OOVCREWHYF5E", + "size": 171, + "variables": [], + "source": "https://github.com/tinymanorg/tinyman-staking/tree/main/contracts/staking.teal" + }, + "clear_program": { + "bytecode": "BIEB", + "address": "P7GEWDXXW5IONRW6XRIRVPJCT2XXEQGOBGG65VJPBUOYZEJCBZWTPHS3VQ", + "size": 3, + "variables": [], + "source": "https://github.com/tinymanorg/tinyman-staking/tree/main/contracts/clear_state.teal" + }, + "global_state_schema": { + "num_uints": 2, + "num_byte_slices": 2 + }, + "local_state_schema": { + "num_uints": 5, + "num_byte_slices": 11 + }, + "name": "staking_app" + } + } +} \ No newline at end of file diff --git a/tinyman/v1/staking/contracts.py b/tinyman/v1/staking/contracts.py new file mode 100644 index 0000000..64d6910 --- /dev/null +++ b/tinyman/v1/staking/contracts.py @@ -0,0 +1,9 @@ +import json +import importlib.resources +from algosdk.future.transaction import LogicSig +import tinyman.v1 +from tinyman.utils import get_program + +_contracts = json.loads(importlib.resources.read_text(tinyman.v1.staking, 'asc.json')) + +staking_app_def = _contracts['contracts']['staking_app'] From ef310a0843fd78da1bf9c9b6b865963426b2b24f Mon Sep 17 00:00:00 2001 From: Fergal Walsh Date: Mon, 20 Dec 2021 15:22:22 +0000 Subject: [PATCH 02/73] Staking commitment example --- examples/staking_commitment.py | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 examples/staking_commitment.py diff --git a/examples/staking_commitment.py b/examples/staking_commitment.py new file mode 100644 index 0000000..81b8e32 --- /dev/null +++ b/examples/staking_commitment.py @@ -0,0 +1,43 @@ +# This sample is provided for demonstration purposes only. +# It is not intended for production use. +# This example does not constitute trading advice. + +from tinyman.v1.client import TinymanTestnetClient + +from tinyman.v1.staking import prepare_commit_transaction + +# Hardcoding account keys is not a great practice. This is for demonstration purposes only. +# See the README & Docs for alternative signing methods. +account = { + 'address': 'ALGORAND_ADDRESS_HERE', + 'private_key': 'base64_private_key_here', # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary +} + +client = TinymanTestnetClient(user_address=account['address']) +# By default all subsequent operations are on behalf of user_address + +# Fetch our two assets of interest +TINYUSDC = client.fetch_asset(21582668) +ALGO = client.fetch_asset(0) + +# Fetch the pool we will work with +pool = client.fetch_pool(TINYUSDC, ALGO) + + +sp = client.algod.suggested_params() + +txn_group = prepare_commit_transaction( + app_id=client.staking_app_id, + program_id=1, + program_account='B4XVZ226UPFEIQBPIY6H454YA4B7HYXGEM7UDQR2RJP66HVLOARZTUTS6Q', + pool_asset_id=pool.liquidity_asset.id, + amount=700_000_000, + sender=account['address'], + suggested_params=sp, +) + +txn_group.sign_with_private_key(account['address'], account['private_key']) +result = client.submit(txn_group, wait=True) +print(result) + + From 09bf7bab3259d6982a128df6d70acc312dbcfbfe Mon Sep 17 00:00:00 2001 From: Fergal Walsh Date: Mon, 20 Dec 2021 16:20:30 +0000 Subject: [PATCH 03/73] Updates and examples --- examples/staking/commitments.py | 10 +++++++ .../make_commitment.py} | 3 +- tinyman/v1/client.py | 7 +++-- tinyman/v1/constants.py | 1 + tinyman/v1/staking/__init__.py | 28 ++++++++++++------- tinyman/v1/staking/contracts.py | 2 +- 6 files changed, 35 insertions(+), 16 deletions(-) create mode 100644 examples/staking/commitments.py rename examples/{staking_commitment.py => staking/make_commitment.py} (93%) diff --git a/examples/staking/commitments.py b/examples/staking/commitments.py new file mode 100644 index 0000000..5bb1296 --- /dev/null +++ b/examples/staking/commitments.py @@ -0,0 +1,10 @@ +import requests +from tinyman.v1.staking import parse_commit_transaction + +app_id = 51948952 +result = requests.get(f'https://indexer.testnet.algoexplorerapi.io/v2/transactions?application-id={app_id}&latest=50').json() +for txn in result['transactions']: + commit = parse_commit_transaction(txn, app_id) + if commit: + print(commit) + print() \ No newline at end of file diff --git a/examples/staking_commitment.py b/examples/staking/make_commitment.py similarity index 93% rename from examples/staking_commitment.py rename to examples/staking/make_commitment.py index 81b8e32..bd2da68 100644 --- a/examples/staking_commitment.py +++ b/examples/staking/make_commitment.py @@ -14,7 +14,6 @@ } client = TinymanTestnetClient(user_address=account['address']) -# By default all subsequent operations are on behalf of user_address # Fetch our two assets of interest TINYUSDC = client.fetch_asset(21582668) @@ -31,7 +30,7 @@ program_id=1, program_account='B4XVZ226UPFEIQBPIY6H454YA4B7HYXGEM7UDQR2RJP66HVLOARZTUTS6Q', pool_asset_id=pool.liquidity_asset.id, - amount=700_000_000, + amount=600_000_000, sender=account['address'], suggested_params=sp, ) diff --git a/tinyman/v1/client.py b/tinyman/v1/client.py index 580eac9..cfac670 100644 --- a/tinyman/v1/client.py +++ b/tinyman/v1/client.py @@ -6,12 +6,13 @@ from tinyman.utils import wait_for_confirmation from tinyman.assets import Asset, AssetAmount from .optin import prepare_app_optin_transactions,prepare_asset_optin_transactions -from .constants import TESTNET_VALIDATOR_APP_ID, MAINNET_VALIDATOR_APP_ID +from .constants import TESTNET_VALIDATOR_APP_ID, MAINNET_VALIDATOR_APP_ID, TESTNET_STAKING_APP_ID class TinymanClient: - def __init__(self, algod_client: AlgodClient, validator_app_id: int, user_address=None): + def __init__(self, algod_client: AlgodClient, validator_app_id: int, user_address=None, staking_app_id: int=None): self.algod = algod_client self.validator_app_id = validator_app_id + self.staking_app_id = staking_app_id self.assets_cache = {} self.user_address = user_address @@ -99,7 +100,7 @@ class TinymanTestnetClient(TinymanClient): def __init__(self, algod_client=None, user_address=None): if algod_client is None: algod_client = AlgodClient('', 'https://api.testnet.algoexplorer.io', headers={'User-Agent': 'algosdk'}) - super().__init__(algod_client, validator_app_id=TESTNET_VALIDATOR_APP_ID, user_address=user_address) + super().__init__(algod_client, validator_app_id=TESTNET_VALIDATOR_APP_ID, user_address=user_address, staking_app_id=TESTNET_STAKING_APP_ID) class TinymanMainnetClient(TinymanClient): diff --git a/tinyman/v1/constants.py b/tinyman/v1/constants.py index 0ef6e71..92c2a6f 100644 --- a/tinyman/v1/constants.py +++ b/tinyman/v1/constants.py @@ -6,6 +6,7 @@ TESTNET_VALIDATOR_APP_ID_V1_0 = 21580889 TESTNET_VALIDATOR_APP_ID_V1_1 = 62368684 +TESTNET_STAKING_APP_ID = 51948952 MAINNET_VALIDATOR_APP_ID_V1_0 = 350338509 MAINNET_VALIDATOR_APP_ID_V1_1 = 552635992 diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index c9abec2..ed49a7b 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -1,10 +1,10 @@ from base64 import b64decode, b64encode from algosdk.future.transaction import ApplicationCreateTxn, OnComplete, StateSchema, ApplicationUpdateTxn, ApplicationNoOpTxn from tinyman.utils import TransactionGroup, int_to_bytes -from .contracts import staking_app_def def prepare_create_transaction(sender, suggested_params): + from .contracts import staking_app_def txn = ApplicationCreateTxn( sender=sender, sp=suggested_params, @@ -18,6 +18,7 @@ def prepare_create_transaction(sender, suggested_params): def prepare_update_transaction(app_id, sender, suggested_params): + from .contracts import staking_app_def txn = ApplicationUpdateTxn( index=app_id, sender=sender, @@ -43,13 +44,20 @@ def prepare_commit_transaction(app_id, program_id, program_account, pool_asset_i def parse_commit_transaction(txn, app_id): if txn.get('application-transaction'): app_call = txn['application-transaction'] - if app_call['application-id'] == app_id and app_call['application-args'][0] == b64encode('commit'): + if app_call['on-completion'] != 'noop': + return + if app_call['application-id'] != app_id: + return + if app_call['application-args'][0] == b64encode(b'commit').decode(): result = {} - result['pooler'] = txn['sender'] - result['program_address'] = app_call['accounts'][0] - result['pool_asset_id'] = app_call['foreign-assets'][0] - result['program_id'] = int.from_bytes(b64decode(app_call['application-args'][2]), 'big') - result['amount'] = int.from_bytes(b64decode(app_call['application-args'][1]), 'big') - result['balance'] = int.from_bytes(b64decode(txn['logs'][0])[8:], 'big') - return result - return None + try: + result['pooler'] = txn['sender'] + result['program_address'] = app_call['accounts'][0] + result['pool_asset_id'] = app_call['foreign-assets'][0] + result['program_id'] = int.from_bytes(b64decode(app_call['application-args'][2]), 'big') + result['amount'] = int.from_bytes(b64decode(app_call['application-args'][1]), 'big') + result['balance'] = int.from_bytes(txn['logs'][0].encode()[8:], 'big') + return result + except Exception as e: + return + return diff --git a/tinyman/v1/staking/contracts.py b/tinyman/v1/staking/contracts.py index 64d6910..508ca70 100644 --- a/tinyman/v1/staking/contracts.py +++ b/tinyman/v1/staking/contracts.py @@ -1,7 +1,7 @@ import json import importlib.resources from algosdk.future.transaction import LogicSig -import tinyman.v1 +import tinyman.v1.staking from tinyman.utils import get_program _contracts = json.loads(importlib.resources.read_text(tinyman.v1.staking, 'asc.json')) From 6492c895030ad7fb1a0e84080034433a801aa977 Mon Sep 17 00:00:00 2001 From: Fergal Walsh Date: Thu, 10 Feb 2022 14:53:37 +0000 Subject: [PATCH 04/73] Fix balance parsing from log --- tinyman/v1/staking/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index ed49a7b..608a69c 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -56,7 +56,7 @@ def parse_commit_transaction(txn, app_id): result['pool_asset_id'] = app_call['foreign-assets'][0] result['program_id'] = int.from_bytes(b64decode(app_call['application-args'][2]), 'big') result['amount'] = int.from_bytes(b64decode(app_call['application-args'][1]), 'big') - result['balance'] = int.from_bytes(txn['logs'][0].encode()[8:], 'big') + result['balance'] = int.from_bytes(b64decode(txn['logs'][0])[8:], 'big') return result except Exception as e: return From a8adc6b900a06207258baba3f63406a2af2b0734 Mon Sep 17 00:00:00 2001 From: Fergal Walsh Date: Thu, 10 Feb 2022 14:54:29 +0000 Subject: [PATCH 05/73] Update prepare_commit_transaction --- tinyman/v1/staking/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index 608a69c..8d3f110 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -28,13 +28,14 @@ def prepare_update_transaction(app_id, sender, suggested_params): ) return TransactionGroup([txn]) -def prepare_commit_transaction(app_id, program_id, program_account, pool_asset_id, amount, sender, suggested_params): + +def prepare_commit_transaction(app_id, program_id, program_account, pool_asset_id, amount, reward_asset_id, sender, suggested_params): txn = ApplicationNoOpTxn( index=app_id, sender=sender, sp=suggested_params, app_args=['commit', int_to_bytes(amount), int_to_bytes(program_id)], - foreign_assets=[pool_asset_id], + foreign_assets=[pool_asset_id, reward_asset_id], accounts=[program_account], note=b'tinymanStaking/v1:b' + int_to_bytes(program_id) + int_to_bytes(pool_asset_id) + int_to_bytes(amount) ) From f0a1826dacb15abdd92b02f01268961e559fcbf0 Mon Sep 17 00:00:00 2001 From: Fergal Walsh Date: Tue, 15 Feb 2022 11:06:16 +0000 Subject: [PATCH 06/73] Useful utils --- tinyman/utils.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tinyman/utils.py b/tinyman/utils.py index c4ddcd8..c18689e 100644 --- a/tinyman/utils.py +++ b/tinyman/utils.py @@ -74,6 +74,19 @@ def int_to_bytes(num): return num.to_bytes(8, 'big') +def int_list_to_bytes(nums): + return b''.join([int_to_bytes(x) for x in nums]) + + +def bytes_to_int(b): + return int.from_bytes(b, 'big') + + +def bytes_to_int_list(b): + n = len(b) // 8 + return [bytes_to_int(b[(i * 8):((i + 1) * 8)]) for i in range(n)] + + def get_state_int(state, key): if type(key) == str: key = b64encode(key.encode()) @@ -86,6 +99,41 @@ def get_state_bytes(state, key): return state.get(key.decode(), {'bytes': ''})['bytes'] +def get_state_from_account_info(account_info, app_id): + try: + app = [a for a in account_info['apps-local-state'] if a['id'] == app_id][0] + except IndexError: + return {} + try: + app_state = {} + for x in app['key-value']: + key = b64decode(x['key']) + if x['value']['type'] == 1: + value = b64decode(x['value'].get('bytes', '')) + else: + value = x['value'].get('uint', 0) + app_state[key] = value + except KeyError: + return {} + return app_state + + +def apply_delta(state, delta): + state = dict(state) + for d in delta: + key = b64decode(d['key']) + if d['value']['action'] == 1: + state[key] = b64decode(d['value'].get('bytes', '')) + elif d['value']['action'] == 2: + state[key] = d['value'].get('uint', 0) + elif d['value']['action'] == 3: + state.pop(key) + else: + raise Exception(d['value']['action']) + return state + + + class TransactionGroup: def __init__(self, transactions): From 491e4c3dbf73119280ab49b483a157293a0f614c Mon Sep 17 00:00:00 2001 From: Fergal Walsh Date: Tue, 15 Feb 2022 11:07:57 +0000 Subject: [PATCH 07/73] Update staking functionality --- tinyman/v1/staking/__init__.py | 177 +++++++++++++++++++++++++++++++-- tinyman/v1/staking/asc.json | 6 +- 2 files changed, 174 insertions(+), 9 deletions(-) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index 8d3f110..a68f0bd 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -1,9 +1,13 @@ +from hashlib import sha256 +from datetime import datetime from base64 import b64decode, b64encode -from algosdk.future.transaction import ApplicationCreateTxn, OnComplete, StateSchema, ApplicationUpdateTxn, ApplicationNoOpTxn -from tinyman.utils import TransactionGroup, int_to_bytes +import json +from typing import List +from algosdk.future.transaction import ApplicationClearStateTxn, ApplicationCreateTxn, ApplicationOptInTxn, OnComplete, PaymentTxn, StateSchema, ApplicationUpdateTxn, ApplicationNoOpTxn +from tinyman.utils import TransactionGroup, apply_delta, bytes_to_int_list, int_list_to_bytes, int_to_bytes -def prepare_create_transaction(sender, suggested_params): +def prepare_create_transaction(args, sender, suggested_params): from .contracts import staking_app_def txn = ApplicationCreateTxn( sender=sender, @@ -13,11 +17,12 @@ def prepare_create_transaction(sender, suggested_params): clear_program=b64decode(staking_app_def['clear_program']['bytecode']), global_schema=StateSchema(**staking_app_def['global_state_schema']), local_schema=StateSchema(**staking_app_def['local_state_schema']), + app_args=args, ) return TransactionGroup([txn]) -def prepare_update_transaction(app_id, sender, suggested_params): +def prepare_update_transaction(app_id: int, sender, suggested_params): from .contracts import staking_app_def txn = ApplicationUpdateTxn( index=app_id, @@ -29,7 +34,7 @@ def prepare_update_transaction(app_id, sender, suggested_params): return TransactionGroup([txn]) -def prepare_commit_transaction(app_id, program_id, program_account, pool_asset_id, amount, reward_asset_id, sender, suggested_params): +def prepare_commit_transaction(app_id: int, program_id: int, program_account: str, pool_asset_id: int, amount:int, reward_asset_id:int , sender, suggested_params): txn = ApplicationNoOpTxn( index=app_id, sender=sender, @@ -42,7 +47,7 @@ def prepare_commit_transaction(app_id, program_id, program_account, pool_asset_i return TransactionGroup([txn]) -def parse_commit_transaction(txn, app_id): +def parse_commit_transaction(txn, app_id: int): if txn.get('application-transaction'): app_call = txn['application-transaction'] if app_call['on-completion'] != 'noop': @@ -62,3 +67,163 @@ def parse_commit_transaction(txn, app_id): except Exception as e: return return + + +def parse_program_config_transaction(txn, app_id: int): + if txn.get('application-transaction'): + app_call = txn['application-transaction'] + if app_call['application-id'] != app_id: + return + if app_call['on-completion'] == 'clear': + return ('clear', None) + arg1 = b64decode(app_call['application-args'][0]).decode() + local_delta = txn['local-state-delta'][0]['delta'] + return (arg1, local_delta) + return + + +def parse_program_update_transaction(txn, app_id: int): + if txn.get('application-transaction'): + app_call = txn['application-transaction'] + if app_call['on-completion'] != 'noop': + return + if app_call['application-id'] != app_id: + return + if app_call['application-args'][0] == b64encode(b'update').decode(): + try: + local_delta = txn['local-state-delta'][0]['delta'] + state = apply_delta({}, local_delta) + result = parse_program_state(txn['sender'], state) + return result + except Exception as e: + return + return + + +def parse_program_state(address, state): + result = {} + result['program_address'] = address + result['id'] = state[b'id'] + result['url'] = state[b'url'] + result['reward_asset_id'] = state[b'reward_asset_id'] + result['reward_period'] = state[b'reward_period'] + result['start_date'] = datetime.fromtimestamp(state[b'start_time']).date() + result['end_date'] = datetime.fromtimestamp(state[b'end_time']).date() + result['pools'] = [] + asset_ids = bytes_to_int_list(state[b'assets']) + mins = bytes_to_int_list(state[b'mins']) + empty_rewards_bytes = int_list_to_bytes([0] * 15) + rewards = [] + for i in range(1, 8): + r = bytes_to_int_list(state.get(f'r{i}'.encode(), empty_rewards_bytes)) + start = r[0] + amounts = r[1:] + if start: + rewards.append({'start_date': str(datetime.fromtimestamp(start).date()), 'amounts': amounts}) + for i in range(len(asset_ids)): + if asset_ids[i] > 0: + result['pools'].append({ + 'asset_id': asset_ids[i], + 'min_amount': mins[i], + 'reward_amounts': {x['start_date']: x['amounts'][i] for x in rewards}, + }) + return result + + +def prepare_setup_transaction(app_id: int, url: str, reward_asset_id: int, reward_period: int, start_time: int, end_time: int, asset_ids: List[int], min_amounts: List[int], sender, suggested_params): + assets = [0] * 14 + mins = [0] * 14 + for i in range(len(asset_ids)): + assets[i] = asset_ids[i] + mins[i] = min_amounts[i] + txn = ApplicationOptInTxn( + index=app_id, + sender=sender, + sp=suggested_params, + # setup, url, reward_asset_id, reward_period, start_time, end_time, int[14]{asset_id_1, asset_id_2, ...} + app_args=[ + 'setup', + url, + int_to_bytes(reward_asset_id), + int_to_bytes(reward_period), + int_to_bytes(start_time), + int_to_bytes(end_time), + int_list_to_bytes(assets), + int_list_to_bytes(mins), + ], + foreign_assets=[reward_asset_id], + ) + return TransactionGroup([txn]) + + +def prepare_clear_state_transaction(app_id, sender, suggested_params): + clear_txn = ApplicationClearStateTxn(index=app_id, sender=sender, sp=suggested_params) + return TransactionGroup([clear_txn]) + + +def prepare_update_rewards_transaction(app_id: int, reward_amounts_dict: dict, sender, suggested_params): + r = [ + [0] * 15, + [0] * 15, + [0] * 15, + [0] * 15, + [0] * 15, + [0] * 15, + [0] * 15, + ] + for i, start_time in enumerate(sorted(reward_amounts_dict.keys())): + amounts = reward_amounts_dict[start_time] + r[i][0] = start_time + for j, x in enumerate(amounts): + r[i][j+1] = x + + txn = ApplicationNoOpTxn( + index=app_id, + sender=sender, + sp=suggested_params, + # ("update_rewards", int[15]{rewards_first_valid_time, rewards_asset_1, rewards_asset_2, ...}, int[15]{rewards_first_valid_time, rewards_asset_1, rewards_asset_2, ...}, ...) + app_args=[ + 'update_rewards', + int_list_to_bytes(r[0]), + int_list_to_bytes(r[1]), + int_list_to_bytes(r[2]), + int_list_to_bytes(r[3]), + int_list_to_bytes(r[4]), + int_list_to_bytes(r[5]), + int_list_to_bytes(r[6]), + ], + ) + return TransactionGroup([txn]) + + +def prepare_payment_transaction(staker_address: str, reward_asset_id: int, amount: int, metadata: dict, sender, suggested_params): + note = b'tinymanStaking/v1:j' + json.dumps(metadata, sort_keys=True).encode() + # Compose a lease key from the distribution key (date, pool_address) and staker_address + # This is to prevent accidently submitting multiple payments for the same staker for the same cycles + # Note: the lease is only ensured unique between first_valid & last_valid + lease_data = json.dumps([metadata['distribution'], staker_address]).encode() + lease = sha256(lease_data).digest() + if reward_asset_id == 0: + txn = PaymentTxn( + sender=sender, + sp=suggested_params, + receiver=staker_address, + amt=amount, + note=note, + lease=lease, + ) + return txn + else: + raise NotImplemented() + + +def prepare_reward_metadata_for_payment(distribution_date: str, cycles_rewards: List[List], pool_address: int, pool_token: int, pool_name: str): + + data = { + "distribution": distribution_date + '_' + pool_address, + "pool_address": pool_address, + "pool_token": pool_token, + "pool_name": pool_name, + "rewards": [[str(cycle), str(amount)] for (cycle, amount) in cycles_rewards], + } + return data diff --git a/tinyman/v1/staking/asc.json b/tinyman/v1/staking/asc.json index 6cfcce4..f1e5d61 100644 --- a/tinyman/v1/staking/asc.json +++ b/tinyman/v1/staking/asc.json @@ -5,9 +5,9 @@ "staking_app": { "type": "app", "approval_program": { - "bytecode": "BSACAQAxGSMSQAAgMRkiEkAAiTEZgQISQACDMRmBBBJAAHsxGYEFEkAAeQA2GgCABmNvbW1pdBJAAAFDgBN0aW55bWFuU3Rha2luZy92MTpiMQVRABMSMQVXEwA1ATQBI1s2GgIXEjQBgQhbNjAAEkQ0AYEQWzYaARcSRCM2MABwAERJFoAIYmFsYW5jZSBMULA2GgEXD0QiQyJDIkMyCTEAEkMyCTEAEkMA", - "address": "NSQUSJZUSE3HABWUHTW6TIEY5MQ55FRIKU7IF2WVLQ6V77OOVCREWHYF5E", - "size": 171, + "bytecode": "BSADAAEIJgcIZW5kX3RpbWUMdmVyaWZpY2F0aW9uAmlkBmFzc2V0cwRtaW5zD3Jld2FyZF9hc3NldF9pZA9wcm9ncmFtX2NvdW50ZXIxGSISQAAgMRkjEkABiTEZgQISQAHtMRmBBBJAAeUxGYEFEkAB4wA2GgCABmNyZWF0ZRJAAFI2GgCABmNvbW1pdBJAAEU2GgCABnVwZGF0ZRJAATM2GgCADnVwZGF0ZV9yZXdhcmRzEkAA0jYaAIALZW5kX3Byb2dyYW0SQAD/NhoAKRJAAQIAI0MjKmI2GgIXEkQjKGIyBw1EIytiIkokC1s2MAASQAAKIwhJgQ4MREL/60xINQEjJwRiNAEkC1s1AjYaARdBABc2GgEXNAIPRCMnBWJJQQAGIkxwAERISCI2MABwAERJFoAIYmFsYW5jZSBMULA2GgEXD0SAE3RpbnltYW5TdGFraW5nL3YxOmIxBVEAExIxBVcTADUBNAEiWzYaAhcSNAEkWzYwABJENAGBEFs2GgEXEkQjQyKAAnIxNhoBZiKAAnIyNhoCZiKAAnIzNhoDZiKAAnI0NhoEZiKAAnI1NhoFZiKAAnI2NhoGZiKAAnI3NhoHZiNDIig2GgEXZiNDI0MyCTEAEkQiKTYaAWYjQzYaAIAFc2V0dXASRCcGZCMINQEnBjQBZyIqNAFmIoADdXJsNhoBZiInBTYaAhdmIoANcmV3YXJkX3BlcmlvZDYaAxdmIoAKc3RhcnRfdGltZTYaBBdmIig2GgUXZiIrNhoGZiInBDYaB2YjQzIJMQASQzIJMQASQwA=", + "address": "ZAVVJXHZZITNIM4QCXA7Z6UVR2M4Z2UCR5GR4J5JHHS23RHAGF7LBGVS7E", + "size": 605, "variables": [], "source": "https://github.com/tinymanorg/tinyman-staking/tree/main/contracts/staking.teal" }, From 028b38382a0d2ec6669e9b83397053c5413501d3 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Tue, 15 Feb 2022 19:48:13 +0300 Subject: [PATCH 08/73] add .idea to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c821c84..75cb1b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +.idea # Byte-compiled / optimized / DLL files __pycache__/ From 758e7315929868a4fb41c241dc278501a7fefc7a Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 16 Feb 2022 11:57:43 +0300 Subject: [PATCH 09/73] add reward payment transaction parser --- tinyman/v1/staking/__init__.py | 91 ++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 5 deletions(-) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index a68f0bd..57c7ee9 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -1,9 +1,13 @@ -from hashlib import sha256 -from datetime import datetime -from base64 import b64decode, b64encode import json +from base64 import b64decode, b64encode +from datetime import datetime +from hashlib import sha256 from typing import List + +from algosdk.constants import payment_txn, assettransfer_txn +from algosdk.encoding import is_valid_address from algosdk.future.transaction import ApplicationClearStateTxn, ApplicationCreateTxn, ApplicationOptInTxn, OnComplete, PaymentTxn, StateSchema, ApplicationUpdateTxn, ApplicationNoOpTxn + from tinyman.utils import TransactionGroup, apply_delta, bytes_to_int_list, int_list_to_bytes, int_to_bytes @@ -54,7 +58,7 @@ def parse_commit_transaction(txn, app_id: int): return if app_call['application-id'] != app_id: return - if app_call['application-args'][0] == b64encode(b'commit').decode(): + if app_call['application-args'][0] == b64encode(b'commit').decode(): result = {} try: result['pooler'] = txn['sender'] @@ -89,7 +93,7 @@ def parse_program_update_transaction(txn, app_id: int): return if app_call['application-id'] != app_id: return - if app_call['application-args'][0] == b64encode(b'update').decode(): + if app_call['application-args'][0] == b64encode(b'update').decode(): try: local_delta = txn['local-state-delta'][0]['delta'] state = apply_delta({}, local_delta) @@ -227,3 +231,80 @@ def prepare_reward_metadata_for_payment(distribution_date: str, cycles_rewards: "rewards": [[str(cycle), str(amount)] for (cycle, amount) in cycles_rewards], } return data + + +def parse_payment_transaction(txn): + prefix = "tinymanStaking/v1:j" + date_format = "%Y%m%d" + + if "note" not in txn: + return + + if txn["tx-type"] == payment_txn: + reward_token = 0 + pooler = txn["payment-transaction"]["receiver"] + transfer_amount = txn["payment-transaction"]["amount"] + elif txn["tx-type"] == assettransfer_txn: + reward_token = txn["asset-transfer-transaction"]["asset-id"] + pooler = txn["asset-transfer-transaction"]["receiver"] + transfer_amount = txn["asset-transfer-transaction"]["amount"] + else: + return + + note = b64decode(txn['note']).decode() + if not note.startswith(prefix): + return + + payment_data = json.loads(note.removeprefix(prefix)) + if not {"distribution", "pool_address", "pool_name", "pool_token", "rewards"} <= set(payment_data): + return + + if not isinstance(payment_data["rewards"], list): + return + + if not payment_data["rewards"]: + return + + try: + distribution_date, pool_address = payment_data["distribution"].split("_") + distribution_date = datetime.strptime(distribution_date, date_format).date() + except ValueError: + return + + if pool_address != payment_data["pool_address"]: + return + + if not is_valid_address(pool_address): + return + + try: + pool_token = int(payment_data["pool_token"]) + except ValueError: + return + + rewards = [] + try: + for reward_date, reward_amount in payment_data["rewards"]: + rewards.append({ + "date": datetime.strptime(reward_date, date_format).date(), + "amount": int(reward_amount) + }) + except ValueError: + return + + total_reward = sum([reward["amount"] for reward in rewards]) + + if total_reward < transfer_amount: + return + + result = { + "distribution_date": distribution_date, + "program_address": txn["sender"], + "pooler": pooler, + "pool_address": pool_address, + "pool_name": payment_data["pool_name"], + "pool_token": pool_token, + "reward_token": reward_token, + "rewards": rewards, + } + return result From 179689265c729c90cb885b94e459f088da9e0569 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 16 Feb 2022 15:21:50 +0300 Subject: [PATCH 10/73] improve naming of reward payment parser --- tinyman/v1/staking/__init__.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index 57c7ee9..f31880b 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -233,7 +233,7 @@ def prepare_reward_metadata_for_payment(distribution_date: str, cycles_rewards: return data -def parse_payment_transaction(txn): +def parse_reward_payment_transaction(txn): prefix = "tinymanStaking/v1:j" date_format = "%Y%m%d" @@ -241,12 +241,12 @@ def parse_payment_transaction(txn): return if txn["tx-type"] == payment_txn: - reward_token = 0 - pooler = txn["payment-transaction"]["receiver"] + reward_asset_id = 0 + staker_address = txn["payment-transaction"]["receiver"] transfer_amount = txn["payment-transaction"]["amount"] elif txn["tx-type"] == assettransfer_txn: - reward_token = txn["asset-transfer-transaction"]["asset-id"] - pooler = txn["asset-transfer-transaction"]["receiver"] + reward_asset_id = txn["asset-transfer-transaction"]["asset-id"] + staker_address = txn["asset-transfer-transaction"]["receiver"] transfer_amount = txn["asset-transfer-transaction"]["amount"] else: return @@ -278,15 +278,15 @@ def parse_payment_transaction(txn): return try: - pool_token = int(payment_data["pool_token"]) + pool_asset_id = int(payment_data["pool_asset_id"]) except ValueError: return rewards = [] try: - for reward_date, reward_amount in payment_data["rewards"]: + for cycle, reward_amount in payment_data["rewards"]: rewards.append({ - "date": datetime.strptime(reward_date, date_format).date(), + "cycle": datetime.strptime(cycle, date_format).date(), "amount": int(reward_amount) }) except ValueError: @@ -300,11 +300,11 @@ def parse_payment_transaction(txn): result = { "distribution_date": distribution_date, "program_address": txn["sender"], - "pooler": pooler, + "staker_address": staker_address, "pool_address": pool_address, "pool_name": payment_data["pool_name"], - "pool_token": pool_token, - "reward_token": reward_token, + "pool_asset_id": pool_asset_id, + "reward_asset_id": reward_asset_id, "rewards": rewards, } return result From 76e8cbb6209edb7af84009a69a43fe27f36c8f16 Mon Sep 17 00:00:00 2001 From: Fergal Walsh Date: Wed, 16 Feb 2022 12:43:06 +0000 Subject: [PATCH 11/73] Updates for reward notes --- tinyman/v1/staking/__init__.py | 40 ++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index f31880b..eab862a 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -201,11 +201,11 @@ def prepare_update_rewards_transaction(app_id: int, reward_amounts_dict: dict, s def prepare_payment_transaction(staker_address: str, reward_asset_id: int, amount: int, metadata: dict, sender, suggested_params): - note = b'tinymanStaking/v1:j' + json.dumps(metadata, sort_keys=True).encode() + note = generate_note_from_metadata(metadata) # Compose a lease key from the distribution key (date, pool_address) and staker_address # This is to prevent accidently submitting multiple payments for the same staker for the same cycles # Note: the lease is only ensured unique between first_valid & last_valid - lease_data = json.dumps([metadata['distribution'], staker_address]).encode() + lease_data = json.dumps([metadata['rewards']['distribution'], staker_address]).encode() lease = sha256(lease_data).digest() if reward_asset_id == 0: txn = PaymentTxn( @@ -221,18 +221,31 @@ def prepare_payment_transaction(staker_address: str, reward_asset_id: int, amoun raise NotImplemented() -def prepare_reward_metadata_for_payment(distribution_date: str, cycles_rewards: List[List], pool_address: int, pool_token: int, pool_name: str): - +def prepare_reward_metadata_for_payment(distribution_date: str, cycles_rewards: List[List], pool_address: int, pool_asset_id: int, pool_name: str): data = { - "distribution": distribution_date + '_' + pool_address, - "pool_address": pool_address, - "pool_token": pool_token, - "pool_name": pool_name, - "rewards": [[str(cycle), str(amount)] for (cycle, amount) in cycles_rewards], + "rewards": { + "distribution": distribution_date + '_' + pool_address, + "pool_address": pool_address, + "pool_asset_id": pool_asset_id, + "pool_name": pool_name, + "rewards": [[str(cycle), str(amount)] for (cycle, amount) in cycles_rewards], + }, } return data +def generate_note_from_metadata(metadata): + note = b'tinymanStaking/v1:j' + json.dumps(metadata, sort_keys=True).encode() + return note + + +def get_note_prefix_for_distribution(distribution_date, pool_address): + metadata = prepare_reward_metadata_for_payment(distribution_date, cycles_rewards=[], pool_address=pool_address, pool_token=None, pool_name=None) + note = generate_note_from_metadata(metadata) + prefix = note.split(b', "pool_address"')[0] + return prefix + + def parse_reward_payment_transaction(txn): prefix = "tinymanStaking/v1:j" date_format = "%Y%m%d" @@ -255,8 +268,13 @@ def parse_reward_payment_transaction(txn): if not note.startswith(prefix): return - payment_data = json.loads(note.removeprefix(prefix)) - if not {"distribution", "pool_address", "pool_name", "pool_token", "rewards"} <= set(payment_data): + data = json.loads(note.lstrip(prefix)) + if "rewards" not in data: + return + + payment_data = data["rewards"] + + if not {"distribution", "pool_address", "pool_name", "pool_asset_id", "rewards"} <= set(payment_data): return if not isinstance(payment_data["rewards"], list): From 491fcbab8fdbec121869c85468371863a5a9e623 Mon Sep 17 00:00:00 2001 From: Fergal Walsh Date: Wed, 16 Feb 2022 12:59:31 +0000 Subject: [PATCH 12/73] Fix arg name --- tinyman/v1/staking/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index eab862a..19779d8 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -240,7 +240,7 @@ def generate_note_from_metadata(metadata): def get_note_prefix_for_distribution(distribution_date, pool_address): - metadata = prepare_reward_metadata_for_payment(distribution_date, cycles_rewards=[], pool_address=pool_address, pool_token=None, pool_name=None) + metadata = prepare_reward_metadata_for_payment(distribution_date, cycles_rewards=[], pool_address=pool_address, pool_asset_id=None, pool_name=None) note = generate_note_from_metadata(metadata) prefix = note.split(b', "pool_address"')[0] return prefix From 1fbd6c762c9cdac40d06f4142c59a45a0234ae9d Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 16 Feb 2022 18:30:01 +0300 Subject: [PATCH 13/73] fix reward payment parser --- tinyman/v1/staking/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index 19779d8..f459075 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -264,7 +264,12 @@ def parse_reward_payment_transaction(txn): else: return - note = b64decode(txn['note']).decode() + note = b64decode(txn['note']) + try: + note = note.decode() + except UnicodeDecodeError: + return + if not note.startswith(prefix): return @@ -273,6 +278,8 @@ def parse_reward_payment_transaction(txn): return payment_data = data["rewards"] + if not isinstance(payment_data, dict): + return if not {"distribution", "pool_address", "pool_name", "pool_asset_id", "rewards"} <= set(payment_data): return From 40e8db85ce4dcc1d2262d5e92c12f40b6593f7aa Mon Sep 17 00:00:00 2001 From: Fergal Walsh Date: Wed, 9 Mar 2022 17:13:47 +0000 Subject: [PATCH 14/73] Updates for staking --- tinyman/utils.py | 5 +++++ tinyman/v1/client.py | 10 ++++++++-- tinyman/v1/constants.py | 1 + tinyman/v1/staking/__init__.py | 19 ++++++++++--------- tinyman/v1/staking/asc.json | 6 +++--- 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/tinyman/utils.py b/tinyman/utils.py index c18689e..a2acdd5 100644 --- a/tinyman/utils.py +++ b/tinyman/utils.py @@ -1,4 +1,5 @@ from base64 import b64decode, b64encode +from datetime import datetime from algosdk.future.transaction import LogicSigTransaction, assign_group_id from algosdk.error import AlgodHTTPError @@ -133,6 +134,10 @@ def apply_delta(state, delta): return state +def timestamp_to_date_str(t): + d = datetime.fromtimestamp(t).date() + return d.strftime('%Y-%m-%d') + class TransactionGroup: diff --git a/tinyman/v1/client.py b/tinyman/v1/client.py index cfac670..9cda3f4 100644 --- a/tinyman/v1/client.py +++ b/tinyman/v1/client.py @@ -6,7 +6,7 @@ from tinyman.utils import wait_for_confirmation from tinyman.assets import Asset, AssetAmount from .optin import prepare_app_optin_transactions,prepare_asset_optin_transactions -from .constants import TESTNET_VALIDATOR_APP_ID, MAINNET_VALIDATOR_APP_ID, TESTNET_STAKING_APP_ID +from .constants import TESTNET_VALIDATOR_APP_ID, MAINNET_VALIDATOR_APP_ID, TESTNET_STAKING_APP_ID, MAINNET_STAKING_APP_ID class TinymanClient: def __init__(self, algod_client: AlgodClient, validator_app_id: int, user_address=None, staking_app_id: int=None): @@ -107,5 +107,11 @@ class TinymanMainnetClient(TinymanClient): def __init__(self, algod_client=None, user_address=None): if algod_client is None: algod_client = AlgodClient('', 'https://api.algoexplorer.io', headers={'User-Agent': 'algosdk'}) - super().__init__(algod_client, validator_app_id=MAINNET_VALIDATOR_APP_ID, user_address=user_address) + super().__init__(algod_client, validator_app_id=MAINNET_VALIDATOR_APP_ID, user_address=user_address, staking_app_id=MAINNET_STAKING_APP_ID) + +class TinymanSandboxClient(TinymanClient): + def __init__(self, algod_client=None, user_address=None): + if algod_client is None: + algod_client = AlgodClient('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'http://localhost:4001', headers={'User-Agent': 'algosdk'}) + super().__init__(algod_client, validator_app_id=TESTNET_VALIDATOR_APP_ID, user_address=user_address, staking_app_id=6) diff --git a/tinyman/v1/constants.py b/tinyman/v1/constants.py index 92c2a6f..93ef6b4 100644 --- a/tinyman/v1/constants.py +++ b/tinyman/v1/constants.py @@ -10,6 +10,7 @@ MAINNET_VALIDATOR_APP_ID_V1_0 = 350338509 MAINNET_VALIDATOR_APP_ID_V1_1 = 552635992 +MAINNET_STAKING_APP_ID = 649588853 TESTNET_VALIDATOR_APP_ID = TESTNET_VALIDATOR_APP_ID_V1_1 MAINNET_VALIDATOR_APP_ID = MAINNET_VALIDATOR_APP_ID_V1_1 diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index 19779d8..f699f12 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -8,7 +8,7 @@ from algosdk.encoding import is_valid_address from algosdk.future.transaction import ApplicationClearStateTxn, ApplicationCreateTxn, ApplicationOptInTxn, OnComplete, PaymentTxn, StateSchema, ApplicationUpdateTxn, ApplicationNoOpTxn -from tinyman.utils import TransactionGroup, apply_delta, bytes_to_int_list, int_list_to_bytes, int_to_bytes +from tinyman.utils import TransactionGroup, apply_delta, bytes_to_int_list, int_list_to_bytes, int_to_bytes, timestamp_to_date_str def prepare_create_transaction(args, sender, suggested_params): @@ -106,16 +106,18 @@ def parse_program_update_transaction(txn, app_id: int): def parse_program_state(address, state): result = {} - result['program_address'] = address + result['address'] = address result['id'] = state[b'id'] result['url'] = state[b'url'] result['reward_asset_id'] = state[b'reward_asset_id'] result['reward_period'] = state[b'reward_period'] - result['start_date'] = datetime.fromtimestamp(state[b'start_time']).date() - result['end_date'] = datetime.fromtimestamp(state[b'end_time']).date() + result['start_date'] = timestamp_to_date_str(state[b'start_time']) + result['end_date'] = timestamp_to_date_str(state[b'end_time']) result['pools'] = [] asset_ids = bytes_to_int_list(state[b'assets']) + result['asset_ids'] = asset_ids mins = bytes_to_int_list(state[b'mins']) + result['mins'] = mins empty_rewards_bytes = int_list_to_bytes([0] * 15) rewards = [] for i in range(1, 8): @@ -123,7 +125,8 @@ def parse_program_state(address, state): start = r[0] amounts = r[1:] if start: - rewards.append({'start_date': str(datetime.fromtimestamp(start).date()), 'amounts': amounts}) + rewards.append({'start_date': timestamp_to_date_str(start), 'amounts': amounts}) + result['reward_amounts_dict'] = rewards for i in range(len(asset_ids)): if asset_ids[i] > 0: result['pools'].append({ @@ -172,8 +175,6 @@ def prepare_update_rewards_transaction(app_id: int, reward_amounts_dict: dict, s [0] * 15, [0] * 15, [0] * 15, - [0] * 15, - [0] * 15, ] for i, start_time in enumerate(sorted(reward_amounts_dict.keys())): amounts = reward_amounts_dict[start_time] @@ -193,8 +194,6 @@ def prepare_update_rewards_transaction(app_id: int, reward_amounts_dict: dict, s int_list_to_bytes(r[2]), int_list_to_bytes(r[3]), int_list_to_bytes(r[4]), - int_list_to_bytes(r[5]), - int_list_to_bytes(r[6]), ], ) return TransactionGroup([txn]) @@ -326,3 +325,5 @@ def parse_reward_payment_transaction(txn): "rewards": rewards, } return result + + diff --git a/tinyman/v1/staking/asc.json b/tinyman/v1/staking/asc.json index f1e5d61..15f49aa 100644 --- a/tinyman/v1/staking/asc.json +++ b/tinyman/v1/staking/asc.json @@ -5,9 +5,9 @@ "staking_app": { "type": "app", "approval_program": { - "bytecode": "BSADAAEIJgcIZW5kX3RpbWUMdmVyaWZpY2F0aW9uAmlkBmFzc2V0cwRtaW5zD3Jld2FyZF9hc3NldF9pZA9wcm9ncmFtX2NvdW50ZXIxGSISQAAgMRkjEkABiTEZgQISQAHtMRmBBBJAAeUxGYEFEkAB4wA2GgCABmNyZWF0ZRJAAFI2GgCABmNvbW1pdBJAAEU2GgCABnVwZGF0ZRJAATM2GgCADnVwZGF0ZV9yZXdhcmRzEkAA0jYaAIALZW5kX3Byb2dyYW0SQAD/NhoAKRJAAQIAI0MjKmI2GgIXEkQjKGIyBw1EIytiIkokC1s2MAASQAAKIwhJgQ4MREL/60xINQEjJwRiNAEkC1s1AjYaARdBABc2GgEXNAIPRCMnBWJJQQAGIkxwAERISCI2MABwAERJFoAIYmFsYW5jZSBMULA2GgEXD0SAE3RpbnltYW5TdGFraW5nL3YxOmIxBVEAExIxBVcTADUBNAEiWzYaAhcSNAEkWzYwABJENAGBEFs2GgEXEkQjQyKAAnIxNhoBZiKAAnIyNhoCZiKAAnIzNhoDZiKAAnI0NhoEZiKAAnI1NhoFZiKAAnI2NhoGZiKAAnI3NhoHZiNDIig2GgEXZiNDI0MyCTEAEkQiKTYaAWYjQzYaAIAFc2V0dXASRCcGZCMINQEnBjQBZyIqNAFmIoADdXJsNhoBZiInBTYaAhdmIoANcmV3YXJkX3BlcmlvZDYaAxdmIoAKc3RhcnRfdGltZTYaBBdmIig2GgUXZiIrNhoGZiInBDYaB2YjQzIJMQASQzIJMQASQwA=", - "address": "ZAVVJXHZZITNIM4QCXA7Z6UVR2M4Z2UCR5GR4J5JHHS23RHAGF7LBGVS7E", - "size": 605, + "bytecode": "BSADAAEIJgcIZW5kX3RpbWUGYXNzZXRzBG1pbnMMdmVyaWZpY2F0aW9uAmlkD3Jld2FyZF9hc3NldF9pZA9wcm9ncmFtX2NvdW50ZXIxGSISQAAvMRkjEkAAGTEZgQISQAICMRmBBBJAAfwxGYEFEkAB+gA2GgCABXNldHVwEkABgwA2GgCABmNyZWF0ZRJAAGg2GgCABmNvbW1pdBJAAFs2GgCABnVwZGF0ZRJAAUU2GgCADnVwZGF0ZV9yZXdhcmRzEkAA6DYaAIANdXBkYXRlX2Fzc2V0cxJAAQE2GgCAC2VuZF9wcm9ncmFtEkAA+zYaACsSQAD+ACNDIycEYjYaAhcSRCMoYjIHDUQjKWIiSiQLWzYwABJAAAojCEmBDgxEQv/rTEg1ASMqYjQBJAtbNQI2GgEXQQAXNhoBFzQCD0QjJwViSUEABiJMcABESEgiNjAAcABESRaACGJhbGFuY2UgTFCwNhoBFw9EgBN0aW55bWFuU3Rha2luZy92MTpiMQVRABMSMQVXEwA1ATQBIls2GgIXEjQBJFs2MAASRDQBgRBbNhoBFxJEI0MigAJyMTYaAWYigAJyMjYaAmYigAJyMzYaA2YigAJyNDYaBGYigAJyNTYaBWYjQyIpNhoBZiIqNhoCZiNDIig2GgEXZiNDI0MyCTEAEkQiKzYaAWYjQycGZCMINQEnBjQBZyInBDQBZiKAA3VybDYaAWYiJwU2GgIXZiKADXJld2FyZF9wZXJpb2Q2GgMXZiKACnN0YXJ0X3RpbWU2GgQXZiIoNhoFF2YiKTYaBmYiKjYaB2YjQyNDMgkxABJDMgkxABJDAA==", + "address": "XHG22P7FZNM4FIUYYK3IV6BHWE7HOV5PEV5LHA64GFKB44PQBFGDNTE32E", + "size": 628, "variables": [], "source": "https://github.com/tinymanorg/tinyman-staking/tree/main/contracts/staking.teal" }, From 9790a2381d5e86f5fb5f4000fff0d8004a7d79b7 Mon Sep 17 00:00:00 2001 From: Fergal Walsh Date: Fri, 11 Mar 2022 13:30:21 +0000 Subject: [PATCH 15/73] Update staking commitment transaction to fit Ledger limits --- tinyman/v1/staking/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index df0a9e4..c30ad78 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -43,7 +43,7 @@ def prepare_commit_transaction(app_id: int, program_id: int, program_account: st index=app_id, sender=sender, sp=suggested_params, - app_args=['commit', int_to_bytes(amount), int_to_bytes(program_id)], + app_args=['commit', int_to_bytes(amount)], foreign_assets=[pool_asset_id, reward_asset_id], accounts=[program_account], note=b'tinymanStaking/v1:b' + int_to_bytes(program_id) + int_to_bytes(pool_asset_id) + int_to_bytes(amount) @@ -61,10 +61,11 @@ def parse_commit_transaction(txn, app_id: int): if app_call['application-args'][0] == b64encode(b'commit').decode(): result = {} try: + note = txn['note'] result['pooler'] = txn['sender'] result['program_address'] = app_call['accounts'][0] result['pool_asset_id'] = app_call['foreign-assets'][0] - result['program_id'] = int.from_bytes(b64decode(app_call['application-args'][2]), 'big') + result['program_id'] = int.from_bytes(b64decode(note)[19:19+8], 'big') result['amount'] = int.from_bytes(b64decode(app_call['application-args'][1]), 'big') result['balance'] = int.from_bytes(b64decode(txn['logs'][0])[8:], 'big') return result From 5c4318dab6828197fdf48958d24474ceab8cc32f Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Fri, 1 Apr 2022 13:00:28 +0300 Subject: [PATCH 16/73] add flake8 rules --- .flake8 | 2 ++ .pre-commit-config.yaml | 7 +++++++ 2 files changed, 9 insertions(+) create mode 100644 .flake8 create mode 100644 .pre-commit-config.yaml diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..b651ebd --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +ignore = E501,F403,F405,E126,E121,W503 \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c46302b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: +- repo: https://github.com/pycqa/flake8 + rev: '3.9.2' # pick a git hash / tag to point to + hooks: + - id: flake8 + args: ['--ignore=E501,F403,F405,E126,E121,W503'] + exclude: ^(env|venv) From e34817a1b38778469d999ff87b518db6dac5a1fa Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Fri, 1 Apr 2022 13:00:56 +0300 Subject: [PATCH 17/73] fix code formatting --- tinyman/v1/staking/__init__.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index c30ad78..2f5372e 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -38,7 +38,7 @@ def prepare_update_transaction(app_id: int, sender, suggested_params): return TransactionGroup([txn]) -def prepare_commit_transaction(app_id: int, program_id: int, program_account: str, pool_asset_id: int, amount:int, reward_asset_id:int , sender, suggested_params): +def prepare_commit_transaction(app_id: int, program_id: int, program_account: str, pool_asset_id: int, amount: int, reward_asset_id: int, sender, suggested_params): txn = ApplicationNoOpTxn( index=app_id, sender=sender, @@ -65,11 +65,11 @@ def parse_commit_transaction(txn, app_id: int): result['pooler'] = txn['sender'] result['program_address'] = app_call['accounts'][0] result['pool_asset_id'] = app_call['foreign-assets'][0] - result['program_id'] = int.from_bytes(b64decode(note)[19:19+8], 'big') + result['program_id'] = int.from_bytes(b64decode(note)[19:19 + 8], 'big') result['amount'] = int.from_bytes(b64decode(app_call['application-args'][1]), 'big') result['balance'] = int.from_bytes(b64decode(txn['logs'][0])[8:], 'big') return result - except Exception as e: + except Exception: return return @@ -100,7 +100,7 @@ def parse_program_update_transaction(txn, app_id: int): state = apply_delta({}, local_delta) result = parse_program_state(txn['sender'], state) return result - except Exception as e: + except Exception: return return @@ -181,7 +181,7 @@ def prepare_update_rewards_transaction(app_id: int, reward_amounts_dict: dict, s amounts = reward_amounts_dict[start_time] r[i][0] = start_time for j, x in enumerate(amounts): - r[i][j+1] = x + r[i][j + 1] = x txn = ApplicationNoOpTxn( index=app_id, @@ -218,7 +218,7 @@ def prepare_payment_transaction(staker_address: str, reward_asset_id: int, amoun ) return txn else: - raise NotImplemented() + raise NotImplementedError() def prepare_reward_metadata_for_payment(distribution_date: str, cycles_rewards: List[List], pool_address: int, pool_asset_id: int, pool_name: str): @@ -333,5 +333,3 @@ def parse_reward_payment_transaction(txn): "rewards": rewards, } return result - - From 833fb6881e8d24675fc9a6bc42292570902fc226 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Fri, 1 Apr 2022 16:41:35 +0300 Subject: [PATCH 18/73] update reward payment note format --- tinyman/v1/staking/__init__.py | 120 ++++++++++++++++++++++++++++++--- 1 file changed, 111 insertions(+), 9 deletions(-) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index 2f5372e..3ef5ce2 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -1,4 +1,5 @@ import json +import re from base64 import b64decode, b64encode from datetime import datetime from hashlib import sha256 @@ -221,33 +222,50 @@ def prepare_payment_transaction(staker_address: str, reward_asset_id: int, amoun raise NotImplementedError() -def prepare_reward_metadata_for_payment(distribution_date: str, cycles_rewards: List[List], pool_address: int, pool_asset_id: int, pool_name: str): +def prepare_reward_metadata_for_payment(distribution_date: str, program_id: str, pool_address: str, pool_asset_id: int, pool_name: str, first_cycle: str, last_cycle: str): data = { "rewards": { - "distribution": distribution_date + '_' + pool_address, + "distribution": f"{pool_asset_id}_{program_id}_{distribution_date}", "pool_address": pool_address, + "distribution_date": distribution_date, "pool_asset_id": pool_asset_id, + "program_id": program_id, "pool_name": pool_name, - "rewards": [[str(cycle), str(amount)] for (cycle, amount) in cycles_rewards], + "first_cycle": first_cycle, + "last_cycle": last_cycle, }, } return data def generate_note_from_metadata(metadata): - note = b'tinymanStaking/v1:j' + json.dumps(metadata, sort_keys=True).encode() + note = b'tinymanStaking/v2:j' + json.dumps(metadata, sort_keys=True).encode() return note def get_note_prefix_for_distribution(distribution_date, pool_address): - metadata = prepare_reward_metadata_for_payment(distribution_date, cycles_rewards=[], pool_address=pool_address, pool_asset_id=None, pool_name=None) + metadata = prepare_reward_metadata_for_payment(distribution_date, program_id=None, pool_address=pool_address, pool_asset_id=None, pool_name=None, first_cycle=None, last_cycle=None) note = generate_note_from_metadata(metadata) - prefix = note.split(b', "pool_address"')[0] + prefix = note.split(b', "distribution_date"')[0] return prefix +def get_note_version(note): + assert isinstance(note, (bytes, str)) + + if isinstance(note, bytes): + note = note.decode() + + m = re.match(r"^tinymanStaking/v(?P\w+):j.*", note) + if m is None: + raise ValueError("Couldn't determine the version.") + + version = m.groupdict()["version"] + assert version in ["1", "2"] + return version + + def parse_reward_payment_transaction(txn): - prefix = "tinymanStaking/v1:j" date_format = "%Y%m%d" if "note" not in txn: @@ -265,15 +283,18 @@ def parse_reward_payment_transaction(txn): return note = b64decode(txn['note']) + note_version = get_note_version(note) + note_prefix = f"tinymanStaking/v{note_version}:j" + try: note = note.decode() except UnicodeDecodeError: return - if not note.startswith(prefix): + if not note.startswith(note_prefix): return - data = json.loads(note.lstrip(prefix)) + data = json.loads(note.lstrip(note_prefix)) if "rewards" not in data: return @@ -281,6 +302,28 @@ def parse_reward_payment_transaction(txn): if not isinstance(payment_data, dict): return + if note_version == "1": + return _parse_reward_payment_transaction_v1( + payment_data=payment_data, + txn=txn, + reward_asset_id=reward_asset_id, + transfer_amount=transfer_amount, + staker_address=staker_address, + date_format=date_format + ) + + if note_version == "2": + return _parse_reward_payment_transaction_v2( + payment_data=payment_data, + txn=txn, + reward_asset_id=reward_asset_id, + transfer_amount=transfer_amount, + staker_address=staker_address, + date_format=date_format + ) + + +def _parse_reward_payment_transaction_v1(*, payment_data, txn, reward_asset_id, transfer_amount, staker_address, date_format): if not {"distribution", "pool_address", "pool_name", "pool_asset_id", "rewards"} <= set(payment_data): return @@ -323,6 +366,8 @@ def parse_reward_payment_transaction(txn): return result = { + "version": "1", + "distribution": payment_data["distribution"], "distribution_date": distribution_date, "program_address": txn["sender"], "staker_address": staker_address, @@ -330,6 +375,63 @@ def parse_reward_payment_transaction(txn): "pool_name": payment_data["pool_name"], "pool_asset_id": pool_asset_id, "reward_asset_id": reward_asset_id, + "total_amount": transfer_amount, "rewards": rewards, } return result + + +def _parse_reward_payment_transaction_v2(*, payment_data, txn, reward_asset_id, transfer_amount, staker_address, date_format): + if not {"distribution", "pool_address", "pool_name", "pool_asset_id", "program_id", "distribution_date", "first_cycle", "last_cycle"} <= set(payment_data): + return + + try: + pool_asset_id, program_id, distribution_date = payment_data["distribution"].split("_") + except ValueError: + return + + if pool_asset_id != payment_data["pool_asset_id"]: + return + + if program_id != payment_data["program_id"]: + return + + if distribution_date != payment_data["distribution_date"]: + return + + try: + pool_asset_id = int(pool_asset_id) + except ValueError: + return + + try: + program_id = int(program_id) + except ValueError: + return + + try: + distribution_date = datetime.strptime(distribution_date, date_format).date() + first_cycle = datetime.strptime(payment_data["first_cycle"], date_format).date() + last_cycle = datetime.strptime(payment_data["last_cycle"], date_format).date() + except ValueError: + return + + if not is_valid_address(payment_data["pool_address"]): + return + + result = { + "version": "2", + "distribution": payment_data["distribution"], + "distribution_date": distribution_date, + "program_id": program_id, + "program_address": txn["sender"], + "staker_address": staker_address, + "pool_address": payment_data["pool_address"], + "pool_name": payment_data["pool_name"], + "pool_asset_id": pool_asset_id, + "reward_asset_id": reward_asset_id, + "total_amount": transfer_amount, + "first_cycle": first_cycle, + "last_cycle": last_cycle, + } + return result From 9b04b6e13d99388702259a0aa8bc77315f2e6c3f Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Fri, 1 Apr 2022 18:19:55 +0300 Subject: [PATCH 19/73] add get_reward_metadata_from_note function --- tinyman/v1/staking/__init__.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index 3ef5ce2..3c9ecb1 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -258,13 +258,28 @@ def get_note_version(note): m = re.match(r"^tinymanStaking/v(?P\w+):j.*", note) if m is None: - raise ValueError("Couldn't determine the version.") + raise ValueError("Invalid note.") - version = m.groupdict()["version"] + version = m.group("version") assert version in ["1", "2"] return version +def get_reward_metadata_from_note(note: str): + assert isinstance(note, (bytes, str)) + + if isinstance(note, bytes): + note = note.decode() + + m = re.match(r"^tinymanStaking/v(?P\w+):j(?P.*)", note) + if m is None: + raise ValueError("Invalid note.") + + metadata = m.group("metadata") + metadata = json.loads(metadata) + return metadata + + def parse_reward_payment_transaction(txn): date_format = "%Y%m%d" From 466eb94e0dde9b71abc6f8a9d95a7233695c7717 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Fri, 1 Apr 2022 18:23:21 +0300 Subject: [PATCH 20/73] add DATE_FORMAT constant for staking --- tinyman/v1/staking/__init__.py | 19 ++++++++----------- tinyman/v1/staking/constants.py | 1 + 2 files changed, 9 insertions(+), 11 deletions(-) create mode 100644 tinyman/v1/staking/constants.py diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index 3c9ecb1..4c86fa8 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -10,6 +10,7 @@ from algosdk.future.transaction import ApplicationClearStateTxn, ApplicationCreateTxn, ApplicationOptInTxn, OnComplete, PaymentTxn, StateSchema, ApplicationUpdateTxn, ApplicationNoOpTxn from tinyman.utils import TransactionGroup, apply_delta, bytes_to_int_list, int_list_to_bytes, int_to_bytes, timestamp_to_date_str +from tinyman.v1.staking.constants import DATE_FORMAT def prepare_create_transaction(args, sender, suggested_params): @@ -281,8 +282,6 @@ def get_reward_metadata_from_note(note: str): def parse_reward_payment_transaction(txn): - date_format = "%Y%m%d" - if "note" not in txn: return @@ -324,7 +323,6 @@ def parse_reward_payment_transaction(txn): reward_asset_id=reward_asset_id, transfer_amount=transfer_amount, staker_address=staker_address, - date_format=date_format ) if note_version == "2": @@ -334,11 +332,10 @@ def parse_reward_payment_transaction(txn): reward_asset_id=reward_asset_id, transfer_amount=transfer_amount, staker_address=staker_address, - date_format=date_format ) -def _parse_reward_payment_transaction_v1(*, payment_data, txn, reward_asset_id, transfer_amount, staker_address, date_format): +def _parse_reward_payment_transaction_v1(*, payment_data, txn, reward_asset_id, transfer_amount, staker_address): if not {"distribution", "pool_address", "pool_name", "pool_asset_id", "rewards"} <= set(payment_data): return @@ -350,7 +347,7 @@ def _parse_reward_payment_transaction_v1(*, payment_data, txn, reward_asset_id, try: distribution_date, pool_address = payment_data["distribution"].split("_") - distribution_date = datetime.strptime(distribution_date, date_format).date() + distribution_date = datetime.strptime(distribution_date, DATE_FORMAT).date() except ValueError: return @@ -369,7 +366,7 @@ def _parse_reward_payment_transaction_v1(*, payment_data, txn, reward_asset_id, try: for cycle, reward_amount in payment_data["rewards"]: rewards.append({ - "cycle": datetime.strptime(cycle, date_format).date(), + "cycle": datetime.strptime(cycle, DATE_FORMAT).date(), "amount": int(reward_amount) }) except ValueError: @@ -396,7 +393,7 @@ def _parse_reward_payment_transaction_v1(*, payment_data, txn, reward_asset_id, return result -def _parse_reward_payment_transaction_v2(*, payment_data, txn, reward_asset_id, transfer_amount, staker_address, date_format): +def _parse_reward_payment_transaction_v2(*, payment_data, txn, reward_asset_id, transfer_amount, staker_address): if not {"distribution", "pool_address", "pool_name", "pool_asset_id", "program_id", "distribution_date", "first_cycle", "last_cycle"} <= set(payment_data): return @@ -425,9 +422,9 @@ def _parse_reward_payment_transaction_v2(*, payment_data, txn, reward_asset_id, return try: - distribution_date = datetime.strptime(distribution_date, date_format).date() - first_cycle = datetime.strptime(payment_data["first_cycle"], date_format).date() - last_cycle = datetime.strptime(payment_data["last_cycle"], date_format).date() + distribution_date = datetime.strptime(distribution_date, DATE_FORMAT).date() + first_cycle = datetime.strptime(payment_data["first_cycle"], DATE_FORMAT).date() + last_cycle = datetime.strptime(payment_data["last_cycle"], DATE_FORMAT).date() except ValueError: return diff --git a/tinyman/v1/staking/constants.py b/tinyman/v1/staking/constants.py new file mode 100644 index 0000000..64fa003 --- /dev/null +++ b/tinyman/v1/staking/constants.py @@ -0,0 +1 @@ +DATE_FORMAT = "%Y%m%d" From 80a9eb6532300e144f44eb74ba5495b8b7eb0b4d Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Mon, 4 Apr 2022 15:31:35 +0300 Subject: [PATCH 21/73] fix reward payment parser --- tinyman/v1/staking/__init__.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index 4c86fa8..7ffd85a 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -399,6 +399,8 @@ def _parse_reward_payment_transaction_v2(*, payment_data, txn, reward_asset_id, try: pool_asset_id, program_id, distribution_date = payment_data["distribution"].split("_") + pool_asset_id = int(pool_asset_id) + program_id = int(program_id) except ValueError: return @@ -411,16 +413,6 @@ def _parse_reward_payment_transaction_v2(*, payment_data, txn, reward_asset_id, if distribution_date != payment_data["distribution_date"]: return - try: - pool_asset_id = int(pool_asset_id) - except ValueError: - return - - try: - program_id = int(program_id) - except ValueError: - return - try: distribution_date = datetime.strptime(distribution_date, DATE_FORMAT).date() first_cycle = datetime.strptime(payment_data["first_cycle"], DATE_FORMAT).date() @@ -436,7 +428,7 @@ def _parse_reward_payment_transaction_v2(*, payment_data, txn, reward_asset_id, "distribution": payment_data["distribution"], "distribution_date": distribution_date, "program_id": program_id, - "program_address": txn["sender"], + "program_distribution_address": txn["sender"], "staker_address": staker_address, "pool_address": payment_data["pool_address"], "pool_name": payment_data["pool_name"], From bb388393f5af7583af9d305af123598414dd81bd Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Tue, 5 Apr 2022 16:58:51 +0300 Subject: [PATCH 22/73] handle invalid reward payment note --- tinyman/v1/staking/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index 7ffd85a..daa85d5 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -297,7 +297,10 @@ def parse_reward_payment_transaction(txn): return note = b64decode(txn['note']) - note_version = get_note_version(note) + try: + note_version = get_note_version(note) + except ValueError: + return note_prefix = f"tinymanStaking/v{note_version}:j" try: From 64705e8c094c5325d5c39233493796c6b30c5b79 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 7 Apr 2022 13:16:29 +0300 Subject: [PATCH 23/73] fix reward payment metadata --- tinyman/v1/staking/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index daa85d5..85b4164 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -223,14 +223,14 @@ def prepare_payment_transaction(staker_address: str, reward_asset_id: int, amoun raise NotImplementedError() -def prepare_reward_metadata_for_payment(distribution_date: str, program_id: str, pool_address: str, pool_asset_id: int, pool_name: str, first_cycle: str, last_cycle: str): +def prepare_reward_metadata_for_payment(distribution_date: str, program_id: int, pool_address: str, pool_asset_id: int, pool_name: str, first_cycle: str, last_cycle: str): data = { "rewards": { "distribution": f"{pool_asset_id}_{program_id}_{distribution_date}", "pool_address": pool_address, "distribution_date": distribution_date, - "pool_asset_id": pool_asset_id, - "program_id": program_id, + "pool_asset_id": int(pool_asset_id), + "program_id": int(program_id), "pool_name": pool_name, "first_cycle": first_cycle, "last_cycle": last_cycle, From 0522607d67780137f748eb29716e1ee5cc35ac47 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 28 Apr 2022 18:32:52 +0300 Subject: [PATCH 24/73] add asa reward support to staking --- tinyman/v1/staking/__init__.py | 23 ++++++++++++++++------- tinyman/v1/staking/contracts.py | 5 ++--- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index 85b4164..b0d9902 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -5,9 +5,9 @@ from hashlib import sha256 from typing import List -from algosdk.constants import payment_txn, assettransfer_txn +from algosdk.constants import PAYMENT_TXN, ASSETTRANSFER_TXN from algosdk.encoding import is_valid_address -from algosdk.future.transaction import ApplicationClearStateTxn, ApplicationCreateTxn, ApplicationOptInTxn, OnComplete, PaymentTxn, StateSchema, ApplicationUpdateTxn, ApplicationNoOpTxn +from algosdk.future.transaction import ApplicationClearStateTxn, ApplicationCreateTxn, ApplicationOptInTxn, OnComplete, PaymentTxn, StateSchema, ApplicationUpdateTxn, ApplicationNoOpTxn, AssetTransferTxn from tinyman.utils import TransactionGroup, apply_delta, bytes_to_int_list, int_list_to_bytes, int_to_bytes, timestamp_to_date_str from tinyman.v1.staking.constants import DATE_FORMAT @@ -40,13 +40,13 @@ def prepare_update_transaction(app_id: int, sender, suggested_params): return TransactionGroup([txn]) -def prepare_commit_transaction(app_id: int, program_id: int, program_account: str, pool_asset_id: int, amount: int, reward_asset_id: int, sender, suggested_params): +def prepare_commit_transaction(app_id: int, program_id: int, program_account: str, pool_asset_id: int, amount: int, sender, suggested_params): txn = ApplicationNoOpTxn( index=app_id, sender=sender, sp=suggested_params, app_args=['commit', int_to_bytes(amount)], - foreign_assets=[pool_asset_id, reward_asset_id], + foreign_assets=[pool_asset_id], accounts=[program_account], note=b'tinymanStaking/v1:b' + int_to_bytes(program_id) + int_to_bytes(pool_asset_id) + int_to_bytes(amount) ) @@ -220,7 +220,16 @@ def prepare_payment_transaction(staker_address: str, reward_asset_id: int, amoun ) return txn else: - raise NotImplementedError() + txn = AssetTransferTxn( + sender=sender, + sp=suggested_params, + receiver=staker_address, + index=reward_asset_id, + amt=amount, + note=note, + lease=lease, + ) + return txn def prepare_reward_metadata_for_payment(distribution_date: str, program_id: int, pool_address: str, pool_asset_id: int, pool_name: str, first_cycle: str, last_cycle: str): @@ -285,11 +294,11 @@ def parse_reward_payment_transaction(txn): if "note" not in txn: return - if txn["tx-type"] == payment_txn: + if txn["tx-type"] == PAYMENT_TXN: reward_asset_id = 0 staker_address = txn["payment-transaction"]["receiver"] transfer_amount = txn["payment-transaction"]["amount"] - elif txn["tx-type"] == assettransfer_txn: + elif txn["tx-type"] == ASSETTRANSFER_TXN: reward_asset_id = txn["asset-transfer-transaction"]["asset-id"] staker_address = txn["asset-transfer-transaction"]["receiver"] transfer_amount = txn["asset-transfer-transaction"]["amount"] diff --git a/tinyman/v1/staking/contracts.py b/tinyman/v1/staking/contracts.py index 508ca70..d34c6a6 100644 --- a/tinyman/v1/staking/contracts.py +++ b/tinyman/v1/staking/contracts.py @@ -1,8 +1,7 @@ -import json import importlib.resources -from algosdk.future.transaction import LogicSig +import json + import tinyman.v1.staking -from tinyman.utils import get_program _contracts = json.loads(importlib.resources.read_text(tinyman.v1.staking, 'asc.json')) From f567d5eee7824fdbf822097a8dec390b8248dc9b Mon Sep 17 00:00:00 2001 From: Fergal Walsh Date: Thu, 19 May 2022 11:02:29 +0100 Subject: [PATCH 25/73] Add round to commitment dict --- tinyman/v1/staking/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index 85b4164..31e6853 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -70,6 +70,7 @@ def parse_commit_transaction(txn, app_id: int): result['program_id'] = int.from_bytes(b64decode(note)[19:19 + 8], 'big') result['amount'] = int.from_bytes(b64decode(app_call['application-args'][1]), 'big') result['balance'] = int.from_bytes(b64decode(txn['logs'][0])[8:], 'big') + result['round'] = txn['confirmed-round'] return result except Exception: return From f0269943bd7ebf82f1f75456bbe3c446f88949a1 Mon Sep 17 00:00:00 2001 From: Fergal Walsh Date: Thu, 19 May 2022 11:05:12 +0100 Subject: [PATCH 26/73] add prepare_end_program_transaction --- tinyman/v1/staking/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index 31e6853..c170605 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -203,6 +203,19 @@ def prepare_update_rewards_transaction(app_id: int, reward_amounts_dict: dict, s return TransactionGroup([txn]) +def prepare_end_program_transaction(app_id: int, end_time: int, sender, suggested_params): + txn = ApplicationNoOpTxn( + index=app_id, + sender=sender, + sp=suggested_params, + app_args=[ + 'end_program', + int_to_bytes(end_time), + ], + ) + return TransactionGroup([txn]) + + def prepare_payment_transaction(staker_address: str, reward_asset_id: int, amount: int, metadata: dict, sender, suggested_params): note = generate_note_from_metadata(metadata) # Compose a lease key from the distribution key (date, pool_address) and staker_address From ffd2d136ea065a5eaea4e922e5829d3ddb6c4c8d Mon Sep 17 00:00:00 2001 From: Fergal Walsh Date: Thu, 19 May 2022 11:05:24 +0100 Subject: [PATCH 27/73] Updated asc.json --- asc.json | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 asc.json diff --git a/asc.json b/asc.json new file mode 100644 index 0000000..a97408d --- /dev/null +++ b/asc.json @@ -0,0 +1,32 @@ +{ + "repo": "https://github.com/tinymanorg/tinyman-staking", + "ref": "main", + "contracts": { + "staking_app": { + "type": "app", + "approval_program": { + "bytecode": "BSADAAEIJgcIZW5kX3RpbWUGYXNzZXRzBG1pbnMMdmVyaWZpY2F0aW9uCGJhbGFuY2UgAmlkD3Byb2dyYW1fY291bnRlcjEZIhJAAC8xGSMSQAAZMRmBAhJAAicxGYEEEkACITEZgQUSQAIfADYaAIAFc2V0dXASQAGZADYaAIAGY3JlYXRlEkAAijYaAIAGY29tbWl0EkAAfTYaAIAFY2xhaW0SQAEDNhoAgAZ1cGRhdGUSQAE8NhoAgA51cGRhdGVfcmV3YXJkcxJAAN82GgCADXVwZGF0ZV9hc3NldHMSQAD4NhoAgAtlbmRfcHJvZ3JhbRJAAPI2GgArEkAA9TYaAIALbG9nX2JhbGFuY2USQADvACNDIyhiMgcNRCMpYiJKJAtbNjAAEkAACiMISYEODERC/+tMSDUBIypiNAEkC1s1AjYaARdBAAg2GgEXNAIPRCI2MABwAERJFicETFCwNhoBFw9EgBN0aW55bWFuU3Rha2luZy92MTpiMQVRABMSRDEFVxMANQE0ASJbIycFYhJENAEkWzYwABJENAGBEFs2GgEXEkQjQyNDIoACcjE2GgFmIoACcjI2GgJmIoACcjM2GgNmIoACcjQ2GgRmIoACcjU2GgVmI0MiKTYaAWYiKjYaAmYjQyIoNhoBF2YjQyNDMgkxABJEIis2GgFmI0MiNjAAcABESUQWJwRMULAjQycGZCMINQEnBjQBZyInBTQBZiKAA3VybDYaAWYigA9yZXdhcmRfYXNzZXRfaWQ2GgIXZiKADXJld2FyZF9wZXJpb2Q2GgMXZiKACnN0YXJ0X3RpbWU2GgQXZiIoNhoFF2YiKTYaBmYiKjYaB2YjQyNDMgkxABJDMgkxABJDAA==", + "address": "KJ3W4IB66Q4ZITCNVABJXAV4I4HKWSZIJMD6BTFATQAWO5AKOV5VMZ6OFI", + "size": 658, + "variables": [], + "source": "https://github.com/tinymanorg/tinyman-staking/tree/main/contracts/staking.teal" + }, + "clear_program": { + "bytecode": "BIEB", + "address": "P7GEWDXXW5IONRW6XRIRVPJCT2XXEQGOBGG65VJPBUOYZEJCBZWTPHS3VQ", + "size": 3, + "variables": [], + "source": "https://github.com/tinymanorg/tinyman-staking/tree/main/contracts/clear_state.teal" + }, + "global_state_schema": { + "num_uints": 2, + "num_byte_slices": 2 + }, + "local_state_schema": { + "num_uints": 5, + "num_byte_slices": 11 + }, + "name": "staking_app" + } + } +} \ No newline at end of file From 3eddc2acb96d70db4010f0efe20e953a7a042736 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 2 Jun 2022 17:59:07 +0300 Subject: [PATCH 28/73] update staking commitment transaction group --- tinyman/v1/staking/__init__.py | 40 ++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index 0c84cc0..def26cf 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -3,7 +3,7 @@ from base64 import b64decode, b64encode from datetime import datetime from hashlib import sha256 -from typing import List +from typing import List, Optional from algosdk.constants import PAYMENT_TXN, ASSETTRANSFER_TXN from algosdk.encoding import is_valid_address @@ -40,8 +40,8 @@ def prepare_update_transaction(app_id: int, sender, suggested_params): return TransactionGroup([txn]) -def prepare_commit_transaction(app_id: int, program_id: int, program_account: str, pool_asset_id: int, amount: int, sender, suggested_params): - txn = ApplicationNoOpTxn( +def prepare_commit_transaction(app_id: int, program_id: int, program_account: str, pool_asset_id: int, amount: int, sender: str, suggested_params, required_asset_id: Optional[int] = None): + commitment_txn = ApplicationNoOpTxn( index=app_id, sender=sender, sp=suggested_params, @@ -50,7 +50,19 @@ def prepare_commit_transaction(app_id: int, program_id: int, program_account: st accounts=[program_account], note=b'tinymanStaking/v1:b' + int_to_bytes(program_id) + int_to_bytes(pool_asset_id) + int_to_bytes(amount) ) - return TransactionGroup([txn]) + transactions = [commitment_txn] + + if required_asset_id is not None: + nft_log_balance_txn = ApplicationNoOpTxn( + index=app_id, + sender=sender, + sp=suggested_params, + app_args=['log_balance'], + foreign_assets=[required_asset_id], + ) + transactions.append(nft_log_balance_txn) + + return TransactionGroup(transactions) def parse_commit_transaction(txn, app_id: int): @@ -77,6 +89,26 @@ def parse_commit_transaction(txn, app_id: int): return +def parse_log_balance_transaction(txn, app_id: int): + if txn.get('application-transaction'): + app_call = txn['application-transaction'] + if app_call['on-completion'] != 'noop': + return + if app_call['application-id'] != app_id: + return + if app_call['application-args'][0] == b64encode(b'log_balance').decode(): + result = {} + try: + result['pooler'] = txn['sender'] + result['asset_id'] = app_call['foreign-assets'][0] + result['balance'] = int.from_bytes(b64decode(txn['logs'][0])[8:], 'big') + result['round'] = txn['confirmed-round'] + return result + except Exception: + return + return + + def parse_program_config_transaction(txn, app_id: int): if txn.get('application-transaction'): app_call = txn['application-transaction'] From b68c3787d447a131108c1ce77b584f6e701a4f82 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Mon, 26 Sep 2022 14:44:43 +0300 Subject: [PATCH 29/73] fix formatting errors --- .pre-commit-config.yaml | 2 +- examples/add_liquidity1.py | 4 +- examples/pooling1.py | 2 +- examples/staking/commitments.py | 2 +- examples/staking/make_commitment.py | 4 +- examples/swapping1.py | 2 +- examples/swapping1_less_convenience.py | 2 +- tinyman/assets.py | 3 +- tinyman/utils.py | 11 +++--- tinyman/v1/bootstrap.py | 6 +-- tinyman/v1/burn.py | 3 -- tinyman/v1/client.py | 14 +++---- tinyman/v1/fees.py | 3 -- tinyman/v1/mint.py | 3 -- tinyman/v1/optin.py | 3 -- tinyman/v1/optout.py | 4 -- tinyman/v1/pools.py | 55 +++++++++++++------------- tinyman/v1/redeem.py | 3 -- tinyman/v1/swap.py | 7 +--- 19 files changed, 52 insertions(+), 81 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c46302b..fc80a46 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: -- repo: https://github.com/pycqa/flake8 + - repo: https://github.com/pycqa/flake8 rev: '3.9.2' # pick a git hash / tag to point to hooks: - id: flake8 diff --git a/examples/add_liquidity1.py b/examples/add_liquidity1.py index c7b8fd5..3580413 100644 --- a/examples/add_liquidity1.py +++ b/examples/add_liquidity1.py @@ -10,7 +10,7 @@ # See the README & Docs for alternative signing methods. account = { 'address': 'ALGORAND_ADDRESS_HERE', - 'private_key': 'base64_private_key_here', # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary + 'private_key': 'base64_private_key_here', # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary } algod = AlgodClient('', 'http://localhost:8080', headers={'User-Agent': 'algosdk'}) @@ -50,4 +50,4 @@ share = info['share'] * 100 print(f'Pool Tokens: {info[pool.liquidity_asset]}') print(f'Assets: {info[TINYUSDC]}, {info[ALGO]}') -print(f'Share of pool: {share:.3f}%') \ No newline at end of file +print(f'Share of pool: {share:.3f}%') diff --git a/examples/pooling1.py b/examples/pooling1.py index b3c4c12..fb45167 100644 --- a/examples/pooling1.py +++ b/examples/pooling1.py @@ -10,7 +10,7 @@ # See the README & Docs for alternative signing methods. account = { 'address': 'ALGORAND_ADDRESS_HERE', - 'private_key': 'base64_private_key_here', # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary + 'private_key': 'base64_private_key_here', # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary } algod = AlgodClient('', 'http://localhost:8080', headers={'User-Agent': 'algosdk'}) diff --git a/examples/staking/commitments.py b/examples/staking/commitments.py index 5bb1296..0cf7a52 100644 --- a/examples/staking/commitments.py +++ b/examples/staking/commitments.py @@ -7,4 +7,4 @@ commit = parse_commit_transaction(txn, app_id) if commit: print(commit) - print() \ No newline at end of file + print() diff --git a/examples/staking/make_commitment.py b/examples/staking/make_commitment.py index bd2da68..a40ee07 100644 --- a/examples/staking/make_commitment.py +++ b/examples/staking/make_commitment.py @@ -10,7 +10,7 @@ # See the README & Docs for alternative signing methods. account = { 'address': 'ALGORAND_ADDRESS_HERE', - 'private_key': 'base64_private_key_here', # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary + 'private_key': 'base64_private_key_here', # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary } client = TinymanTestnetClient(user_address=account['address']) @@ -38,5 +38,3 @@ txn_group.sign_with_private_key(account['address'], account['private_key']) result = client.submit(txn_group, wait=True) print(result) - - diff --git a/examples/swapping1.py b/examples/swapping1.py index 68d22b1..c76e045 100644 --- a/examples/swapping1.py +++ b/examples/swapping1.py @@ -12,7 +12,7 @@ # See the README & Docs for alternative signing methods. account = { 'address': 'ALGORAND_ADDRESS_HERE', - 'private_key': 'base64_private_key_here', # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary + 'private_key': 'base64_private_key_here', # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary } algod = AlgodClient('', 'http://localhost:8080', headers={'User-Agent': 'algosdk'}) diff --git a/examples/swapping1_less_convenience.py b/examples/swapping1_less_convenience.py index 90796fa..04789af 100644 --- a/examples/swapping1_less_convenience.py +++ b/examples/swapping1_less_convenience.py @@ -17,7 +17,7 @@ # See the README & Docs for alternative signing methods. account = { 'address': 'ALGORAND_ADDRESS_HERE', - 'private_key': 'base64_private_key_here', # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary + 'private_key': 'base64_private_key_here', # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary } diff --git a/tinyman/assets.py b/tinyman/assets.py index 6648d4f..0bd89c2 100644 --- a/tinyman/assets.py +++ b/tinyman/assets.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from decimal import Decimal + @dataclass class Asset: id: int @@ -10,7 +11,7 @@ class Asset: def __call__(self, amount: int) -> "AssetAmount": return AssetAmount(self, amount) - + def __hash__(self) -> int: return self.id diff --git a/tinyman/utils.py b/tinyman/utils.py index b5d11e9..24b8e37 100644 --- a/tinyman/utils.py +++ b/tinyman/utils.py @@ -6,6 +6,7 @@ warnings.simplefilter('always', DeprecationWarning) + def get_program(definition, variables=None): """ Return a byte array to be used in LogicSig. @@ -51,7 +52,7 @@ def sign_and_submit_transactions(client, transactions, signed_transactions, send for i, txn in enumerate(transactions): if txn.sender == sender: signed_transactions[i] = txn.sign(sender_sk) - + txid = client.send_transactions(signed_transactions) txinfo = wait_for_confirmation_algosdk(client, txid) txinfo["txid"] = txid @@ -143,10 +144,10 @@ def __init__(self, transactions): transactions = assign_group_id(transactions) self.transactions = transactions self.signed_transactions = [None for _ in self.transactions] - + def sign(self, user): user.sign_transaction_group(self) - + def sign_with_logicisg(self, logicsig): address = logicsig.address() for i, txn in enumerate(self.transactions): @@ -157,7 +158,7 @@ def sign_with_private_key(self, address, private_key): for i, txn in enumerate(self.transactions): if txn.sender == address: self.signed_transactions[i] = txn.sign(private_key) - + def submit(self, algod, wait=False): try: txid = algod.send_transactions(self.signed_transactions) @@ -168,5 +169,3 @@ def submit(self, algod, wait=False): txinfo["txid"] = txid return txinfo return {'txid': txid} - - diff --git a/tinyman/v1/bootstrap.py b/tinyman/v1/bootstrap.py index 2109675..7da9a8f 100644 --- a/tinyman/v1/bootstrap.py +++ b/tinyman/v1/bootstrap.py @@ -1,8 +1,4 @@ -import base64 -from os import name -import algosdk from algosdk.future.transaction import ApplicationOptInTxn, PaymentTxn, AssetCreateTxn, AssetOptInTxn -from algosdk.v2client.algod import AlgodClient from tinyman.utils import int_to_bytes, TransactionGroup from .contracts import get_pool_logicsig @@ -58,4 +54,4 @@ def prepare_bootstrap_transactions(validator_app_id, asset1_id, asset2_id, asset ] txn_group = TransactionGroup(txns) txn_group.sign_with_logicisg(pool_logicsig) - return txn_group \ No newline at end of file + return txn_group diff --git a/tinyman/v1/burn.py b/tinyman/v1/burn.py index d964ec1..3071b79 100644 --- a/tinyman/v1/burn.py +++ b/tinyman/v1/burn.py @@ -1,6 +1,3 @@ -import base64 -from os import name -import algosdk from algosdk.future.transaction import ApplicationNoOpTxn, PaymentTxn, AssetTransferTxn from tinyman.utils import TransactionGroup diff --git a/tinyman/v1/client.py b/tinyman/v1/client.py index 4329678..218ebf8 100644 --- a/tinyman/v1/client.py +++ b/tinyman/v1/client.py @@ -1,21 +1,20 @@ -import json from base64 import b64decode from algosdk.v2client.algod import AlgodClient -from algosdk.error import AlgodHTTPError from algosdk.encoding import encode_address from algosdk.future.transaction import wait_for_confirmation from tinyman.assets import Asset, AssetAmount -from .optin import prepare_app_optin_transactions,prepare_asset_optin_transactions +from .optin import prepare_app_optin_transactions, prepare_asset_optin_transactions from .constants import TESTNET_VALIDATOR_APP_ID, MAINNET_VALIDATOR_APP_ID, TESTNET_STAKING_APP_ID, MAINNET_STAKING_APP_ID + class TinymanClient: - def __init__(self, algod_client: AlgodClient, validator_app_id: int, user_address=None, staking_app_id: int=None): + def __init__(self, algod_client: AlgodClient, validator_app_id: int, user_address=None, staking_app_id: int = None): self.algod = algod_client self.validator_app_id = validator_app_id self.staking_app_id = staking_app_id self.assets_cache = {} self.user_address = user_address - + def fetch_pool(self, asset1, asset2, fetch=True): from .pools import Pool return Pool(self, asset1, asset2, fetch=fetch) @@ -27,7 +26,6 @@ def fetch_asset(self, asset_id): self.assets_cache[asset_id] = asset return self.assets_cache[asset_id] - def submit(self, transaction_group, wait=False): txid = self.algod.send_transactions(transaction_group.signed_transactions) if wait: @@ -80,7 +78,7 @@ def fetch_excess_amounts(self, user_address=None): pools[pool_address][asset] = AssetAmount(asset, value) return pools - + def is_opted_in(self, user_address=None): user_address = user_address or self.user_address account_info = self.algod.account_info(user_address) @@ -93,7 +91,7 @@ def asset_is_opted_in(self, asset_id, user_address=None): user_address = user_address or self.user_address account_info = self.algod.account_info(user_address) for a in account_info.get('assets', []): - if a['asset-id']==asset_id: + if a['asset-id'] == asset_id: return True return False diff --git a/tinyman/v1/fees.py b/tinyman/v1/fees.py index 69cf57e..ef67d86 100644 --- a/tinyman/v1/fees.py +++ b/tinyman/v1/fees.py @@ -1,6 +1,3 @@ -import base64 -from os import name -import algosdk from algosdk.future.transaction import ApplicationNoOpTxn, PaymentTxn, AssetTransferTxn from tinyman.utils import TransactionGroup diff --git a/tinyman/v1/mint.py b/tinyman/v1/mint.py index 42add5a..6157ac1 100644 --- a/tinyman/v1/mint.py +++ b/tinyman/v1/mint.py @@ -1,6 +1,3 @@ -import base64 -from os import name -import algosdk from algosdk.future.transaction import ApplicationNoOpTxn, PaymentTxn, AssetTransferTxn from tinyman.utils import TransactionGroup diff --git a/tinyman/v1/optin.py b/tinyman/v1/optin.py index bfdc0cc..efd1883 100644 --- a/tinyman/v1/optin.py +++ b/tinyman/v1/optin.py @@ -1,7 +1,4 @@ -import base64 -import algosdk from algosdk.future.transaction import ApplicationOptInTxn, AssetOptInTxn -from algosdk.v2client.algod import AlgodClient from tinyman.utils import TransactionGroup diff --git a/tinyman/v1/optout.py b/tinyman/v1/optout.py index 6e20e40..ceef150 100644 --- a/tinyman/v1/optout.py +++ b/tinyman/v1/optout.py @@ -1,10 +1,6 @@ -import base64 -import algosdk from algosdk.future.transaction import ApplicationClearStateTxn from algosdk.v2client.algod import AlgodClient -from .contracts import validator_app_def - def get_optout_transactions(client: AlgodClient, sender, validator_app_id): suggested_params = client.suggested_params() diff --git a/tinyman/v1/pools.py b/tinyman/v1/pools.py index 4e1ccec..a2ca672 100644 --- a/tinyman/v1/pools.py +++ b/tinyman/v1/pools.py @@ -1,10 +1,10 @@ import math from dataclasses import dataclass -from base64 import b64encode, b64decode +from base64 import b64encode from algosdk.v2client.algod import AlgodClient from algosdk.encoding import decode_address from .contracts import get_pool_logicsig -from tinyman.utils import get_state_int, get_state_bytes +from tinyman.utils import get_state_int from tinyman.assets import Asset, AssetAmount from .swap import prepare_swap_transactions from .bootstrap import prepare_bootstrap_transactions @@ -14,7 +14,6 @@ from .optin import prepare_asset_optin_transactions from .fees import prepare_redeem_fees_transactions from .client import TinymanClient -from tinyman.v1 import swap def get_pool_info(client: AlgodClient, validator_app_id, asset1_id, asset2_id): @@ -165,7 +164,7 @@ def __init__(self, client: TinymanClient, asset_a: Asset, asset_b: Asset, info=N self.refresh() elif info is not None: self.update_from_info(info) - + @classmethod def from_account_info(cls, account_info, client=None): info = get_pool_info_from_account_info(account_info) @@ -178,7 +177,7 @@ def refresh(self, info=None): if not info: return self.update_from_info(info) - + def update_from_info(self, info): if info['liquidity_asset_id'] is not None: self.exists = True @@ -196,11 +195,11 @@ def update_from_info(self, info): self.min_balance = self.get_minimum_balance() if self.asset2.id == 0: self.asset2_reserves = (self.algo_balance - self.min_balance) - self.outstanding_asset2_amount - + def get_logicsig(self): pool_logicsig = get_pool_logicsig(self.validator_app_id, self.asset1.id, self.asset2.id) return pool_logicsig - + @property def address(self): logicsig = self.get_logicsig() @@ -240,8 +239,8 @@ def convert(self, amount: AssetAmount): return AssetAmount(self.asset2, int(amount.amount * self.asset1_price)) elif amount.asset == self.asset2: 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): + + def fetch_mint_quote(self, amount_a: AssetAmount, amount_b: AssetAmount = None, slippage=0.05): 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() @@ -258,7 +257,8 @@ def fetch_mint_quote(self, amount_a: AssetAmount, amount_b: AssetAmount=None, sl amount1.amount * self.issued_liquidity / self.asset1_reserves, amount2.amount * self.issued_liquidity / self.asset2_reserves, ) - else: # first mint + else: + # first mint if not amount1 or not amount2: raise Exception('Amounts required for both assets for first mint!') liquidity_asset_amount = math.sqrt(amount1.amount * amount2.amount) - 1000 @@ -306,9 +306,9 @@ def fetch_fixed_input_swap_quote(self, amount_in: AssetAmount, slippage=0.05) -> if not input_supply or not output_supply: raise Exception('Pool has no liquidity!') - + # k = input_supply * output_supply - # ignoring fees, k must remain constant + # ignoring fees, k must remain constant # (input_supply + asset_in) * (output_supply - amount_out) = k k = input_supply * output_supply asset_in_amount_minus_fee = (asset_in_amount * 997) / 1000 @@ -342,14 +342,14 @@ def fetch_fixed_output_swap_quote(self, amount_out: AssetAmount, slippage=0.05) asset_in = self.asset1 input_supply = self.asset1_reserves output_supply = self.asset2_reserves - + # k = input_supply * output_supply - # ignoring fees, k must remain constant + # ignoring fees, k must remain constant # (input_supply + asset_in) * (output_supply - amount_out) = k k = input_supply * output_supply calculated_amount_in_without_fee = (k / (output_supply - asset_out_amount)) - input_supply - asset_in_amount = calculated_amount_in_without_fee * 1000/997 + asset_in_amount = calculated_amount_in_without_fee * 1000 / 997 swap_fees = asset_in_amount - calculated_amount_in_without_fee amount_in = AssetAmount(asset_in, int(asset_in_amount)) @@ -379,13 +379,13 @@ def prepare_swap_transactions(self, amount_in: AssetAmount, amount_out: AssetAmo liquidity_asset_id=self.liquidity_asset.id, asset_in_id=amount_in.asset.id, asset_in_amount=amount_in.amount, - asset_out_amount=amount_out.amount, - swap_type=swap_type, + asset_out_amount=amount_out.amount, + swap_type=swap_type, sender=swapper_address, suggested_params=suggested_params, ) return txn_group - + def prepare_swap_transactions_from_quote(self, quote: SwapQuote, swapper_address=None): return self.prepare_swap_transactions( amount_in=quote.amount_in_with_slippage, @@ -484,7 +484,7 @@ def prepare_liquidity_asset_optin_transactions(self, user_address=None): suggested_params=suggested_params, ) return txn_group - + def prepare_redeem_fees_transactions(self, amount, creator, user_address=None): user_address = user_address or self.client.user_address suggested_params = self.client.algod.suggested_params() @@ -513,18 +513,20 @@ def get_minimum_balance(self): total_uints = 16 total_byteslices = 0 - total = MIN_BALANCE_PER_ACCOUNT + \ - (MIN_BALANCE_PER_ASSET * num_assets) + \ - (MIN_BALANCE_PER_APP * (num_created_apps + num_local_apps)) + \ - (MIN_BALANCE_PER_APP_UINT * total_uints) + \ - (MIN_BALANCE_PER_APP_BYTESLICE * total_byteslices) + total = ( + MIN_BALANCE_PER_ACCOUNT + + (MIN_BALANCE_PER_ASSET * num_assets) + + (MIN_BALANCE_PER_APP * (num_created_apps + num_local_apps)) + + (MIN_BALANCE_PER_APP_UINT * total_uints) + + (MIN_BALANCE_PER_APP_BYTESLICE * total_byteslices) + ) return total def fetch_excess_amounts(self, user_address=None): user_address = user_address or self.client.user_address pool_excess = self.client.fetch_excess_amounts(user_address).get(self.address, {}) return pool_excess - + def fetch_pool_position(self, pooler_address=None): pooler_address = pooler_address or self.client.user_address account_info = self.client.algod.account_info(pooler_address) @@ -541,7 +543,7 @@ def fetch_pool_position(self, pooler_address=None): def fetch_state(self, key=None): account_info = self.client.algod.account_info(self.address) try: - validator_app_id = account_info['apps-local-state'][0]['id'] + _ = account_info['apps-local-state'][0]['id'] except IndexError: return {} validator_app_state = {x['key']: x['value'] for x in account_info['apps-local-state'][0]['key-value']} @@ -550,4 +552,3 @@ def fetch_state(self, key=None): return get_state_int(validator_app_state, key) else: return validator_app_state - diff --git a/tinyman/v1/redeem.py b/tinyman/v1/redeem.py index 9a2cf2b..7fd8ec8 100644 --- a/tinyman/v1/redeem.py +++ b/tinyman/v1/redeem.py @@ -1,6 +1,3 @@ -import base64 -from os import name -import algosdk from algosdk.future.transaction import ApplicationNoOpTxn, PaymentTxn, AssetTransferTxn from tinyman.utils import TransactionGroup diff --git a/tinyman/v1/swap.py b/tinyman/v1/swap.py index 0022a59..7594e90 100644 --- a/tinyman/v1/swap.py +++ b/tinyman/v1/swap.py @@ -1,6 +1,3 @@ -import base64 -from os import name -import algosdk from algosdk.future.transaction import ApplicationNoOpTxn, PaymentTxn, AssetTransferTxn from tinyman.utils import TransactionGroup @@ -40,7 +37,7 @@ def prepare_swap_transactions(validator_app_id, asset1_id, asset2_id, liquidity_ receiver=pool_address, amt=int(asset_in_amount), index=asset_in_id, - ) if asset_in_id != 0 else PaymentTxn( + ) if asset_in_id != 0 else PaymentTxn( sender=sender, sp=suggested_params, receiver=pool_address, @@ -62,4 +59,4 @@ def prepare_swap_transactions(validator_app_id, asset1_id, asset2_id, liquidity_ txn_group = TransactionGroup(txns) txn_group.sign_with_logicisg(pool_logicsig) - return txn_group \ No newline at end of file + return txn_group From e26844cfdd60c7d50a16b9f2ae99662a9c950dc9 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Mon, 26 Sep 2022 14:52:43 +0300 Subject: [PATCH 30/73] add black to pre-commit hooks --- .flake8 | 2 +- .pre-commit-config.yaml | 9 +- examples/add_liquidity1.py | 26 ++- examples/pooling1.py | 18 +- examples/staking/commitments.py | 6 +- examples/staking/make_commitment.py | 12 +- examples/swapping1.py | 30 +-- examples/swapping1_less_convenience.py | 50 +++-- setup.py | 2 +- tinyman/assets.py | 30 +-- tinyman/utils.py | 83 +++---- tinyman/v1/bootstrap.py | 31 ++- tinyman/v1/burn.py | 24 +- tinyman/v1/client.py | 56 +++-- tinyman/v1/contracts.py | 19 +- tinyman/v1/fees.py | 21 +- tinyman/v1/mint.py | 24 +- tinyman/v1/pools.py | 291 ++++++++++++++++--------- tinyman/v1/redeem.py | 23 +- tinyman/v1/staking/__init__.py | 288 ++++++++++++++++-------- tinyman/v1/staking/contracts.py | 4 +- tinyman/v1/swap.py | 33 ++- 22 files changed, 711 insertions(+), 371 deletions(-) diff --git a/.flake8 b/.flake8 index b651ebd..4fb53ea 100644 --- a/.flake8 +++ b/.flake8 @@ -1,2 +1,2 @@ [flake8] -ignore = E501,F403,F405,E126,E121,W503 \ No newline at end of file +ignore = E501,F403,F405,E126,E121,W503,E203 \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fc80a46..24c7e2f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,5 +3,12 @@ repos: rev: '3.9.2' # pick a git hash / tag to point to hooks: - id: flake8 - args: ['--ignore=E501,F403,F405,E126,E121,W503'] + args: ['--ignore=E501,F403,F405,E126,E121,W503,E203'] + exclude: ^(env|venv) + + - repo: https://github.com/psf/black + rev: 22.8.0 + hooks: + - id: black + args: ['--check'] exclude: ^(env|venv) diff --git a/examples/add_liquidity1.py b/examples/add_liquidity1.py index 3580413..8d45996 100644 --- a/examples/add_liquidity1.py +++ b/examples/add_liquidity1.py @@ -9,12 +9,14 @@ # Hardcoding account keys is not a great practice. This is for demonstration purposes only. # See the README & Docs for alternative signing methods. account = { - 'address': 'ALGORAND_ADDRESS_HERE', - 'private_key': 'base64_private_key_here', # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary + "address": "ALGORAND_ADDRESS_HERE", + "private_key": "base64_private_key_here", # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary } -algod = AlgodClient('', 'http://localhost:8080', headers={'User-Agent': 'algosdk'}) -client = TinymanTestnetClient(algod_client=algod, user_address=account['address']) +algod = AlgodClient( + "", "http://localhost:8080", headers={"User-Agent": "algosdk"} +) +client = TinymanTestnetClient(algod_client=algod, user_address=account["address"]) # By default all subsequent operations are on behalf of user_address # Fetch our two assets of interest @@ -33,21 +35,23 @@ if quote.amounts_in[ALGO] < 5_000_000: # Prepare the mint transactions from the quote and sign them transaction_group = pool.prepare_mint_transactions_from_quote(quote) - transaction_group.sign_with_private_key(account['address'], account['private_key']) + transaction_group.sign_with_private_key(account["address"], account["private_key"]) result = client.submit(transaction_group, wait=True) # Check if any excess liquidity asset remaining after the mint excess = pool.fetch_excess_amounts() if pool.liquidity_asset in excess: amount = excess[pool.liquidity_asset] - print(f'Excess: {amount}') + print(f"Excess: {amount}") if amount > 1_000_000: transaction_group = pool.prepare_redeem_transactions(amount) - transaction_group.sign_with_private_key(account['address'], account['private_key']) + transaction_group.sign_with_private_key( + account["address"], account["private_key"] + ) result = client.submit(transaction_group, wait=True) info = pool.fetch_pool_position() -share = info['share'] * 100 -print(f'Pool Tokens: {info[pool.liquidity_asset]}') -print(f'Assets: {info[TINYUSDC]}, {info[ALGO]}') -print(f'Share of pool: {share:.3f}%') +share = info["share"] * 100 +print(f"Pool Tokens: {info[pool.liquidity_asset]}") +print(f"Assets: {info[TINYUSDC]}, {info[ALGO]}") +print(f"Share of pool: {share:.3f}%") diff --git a/examples/pooling1.py b/examples/pooling1.py index fb45167..26114be 100644 --- a/examples/pooling1.py +++ b/examples/pooling1.py @@ -9,12 +9,14 @@ # Hardcoding account keys is not a great practice. This is for demonstration purposes only. # See the README & Docs for alternative signing methods. account = { - 'address': 'ALGORAND_ADDRESS_HERE', - 'private_key': 'base64_private_key_here', # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary + "address": "ALGORAND_ADDRESS_HERE", + "private_key": "base64_private_key_here", # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary } -algod = AlgodClient('', 'http://localhost:8080', headers={'User-Agent': 'algosdk'}) -client = TinymanTestnetClient(algod_client=algod, user_address=account['address']) +algod = AlgodClient( + "", "http://localhost:8080", headers={"User-Agent": "algosdk"} +) +client = TinymanTestnetClient(algod_client=algod, user_address=account["address"]) # By default all subsequent operations are on behalf of user_address # Fetch our two assets of interest @@ -25,7 +27,7 @@ pool = client.fetch_pool(TINYUSDC, ALGO) info = pool.fetch_pool_position() -share = info['share'] * 100 -print(f'Pool Tokens: {info[pool.liquidity_asset]}') -print(f'Assets: {info[TINYUSDC]}, {info[ALGO]}') -print(f'Share of pool: {share:.3f}%') +share = info["share"] * 100 +print(f"Pool Tokens: {info[pool.liquidity_asset]}") +print(f"Assets: {info[TINYUSDC]}, {info[ALGO]}") +print(f"Share of pool: {share:.3f}%") diff --git a/examples/staking/commitments.py b/examples/staking/commitments.py index 0cf7a52..fb5691b 100644 --- a/examples/staking/commitments.py +++ b/examples/staking/commitments.py @@ -2,8 +2,10 @@ from tinyman.v1.staking import parse_commit_transaction app_id = 51948952 -result = requests.get(f'https://indexer.testnet.algoexplorerapi.io/v2/transactions?application-id={app_id}&latest=50').json() -for txn in result['transactions']: +result = requests.get( + f"https://indexer.testnet.algoexplorerapi.io/v2/transactions?application-id={app_id}&latest=50" +).json() +for txn in result["transactions"]: commit = parse_commit_transaction(txn, app_id) if commit: print(commit) diff --git a/examples/staking/make_commitment.py b/examples/staking/make_commitment.py index a40ee07..2796e43 100644 --- a/examples/staking/make_commitment.py +++ b/examples/staking/make_commitment.py @@ -9,11 +9,11 @@ # Hardcoding account keys is not a great practice. This is for demonstration purposes only. # See the README & Docs for alternative signing methods. account = { - 'address': 'ALGORAND_ADDRESS_HERE', - 'private_key': 'base64_private_key_here', # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary + "address": "ALGORAND_ADDRESS_HERE", + "private_key": "base64_private_key_here", # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary } -client = TinymanTestnetClient(user_address=account['address']) +client = TinymanTestnetClient(user_address=account["address"]) # Fetch our two assets of interest TINYUSDC = client.fetch_asset(21582668) @@ -28,13 +28,13 @@ txn_group = prepare_commit_transaction( app_id=client.staking_app_id, program_id=1, - program_account='B4XVZ226UPFEIQBPIY6H454YA4B7HYXGEM7UDQR2RJP66HVLOARZTUTS6Q', + program_account="B4XVZ226UPFEIQBPIY6H454YA4B7HYXGEM7UDQR2RJP66HVLOARZTUTS6Q", pool_asset_id=pool.liquidity_asset.id, amount=600_000_000, - sender=account['address'], + sender=account["address"], suggested_params=sp, ) -txn_group.sign_with_private_key(account['address'], account['private_key']) +txn_group.sign_with_private_key(account["address"], account["private_key"]) result = client.submit(txn_group, wait=True) print(result) diff --git a/examples/swapping1.py b/examples/swapping1.py index c76e045..028aea5 100644 --- a/examples/swapping1.py +++ b/examples/swapping1.py @@ -11,19 +11,21 @@ # Hardcoding account keys is not a great practice. This is for demonstration purposes only. # See the README & Docs for alternative signing methods. account = { - 'address': 'ALGORAND_ADDRESS_HERE', - 'private_key': 'base64_private_key_here', # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary + "address": "ALGORAND_ADDRESS_HERE", + "private_key": "base64_private_key_here", # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary } -algod = AlgodClient('', 'http://localhost:8080', headers={'User-Agent': 'algosdk'}) -client = TinymanTestnetClient(algod_client=algod, user_address=account['address']) +algod = AlgodClient( + "", "http://localhost:8080", headers={"User-Agent": "algosdk"} +) +client = TinymanTestnetClient(algod_client=algod, user_address=account["address"]) # By default all subsequent operations are on behalf of user_address # Check if the account is opted into Tinyman and optin if necessary -if(not client.is_opted_in()): - print('Account not opted into app, opting in now..') +if not client.is_opted_in(): + print("Account not opted into app, opting in now..") transaction_group = client.prepare_app_optin_transactions() - transaction_group.sign_with_private_key(account['address'], account['private_key']) + transaction_group.sign_with_private_key(account["address"], account["private_key"]) result = client.submit(transaction_group, wait=True) @@ -38,16 +40,16 @@ # Get a quote for a swap of 1 ALGO to TINYUSDC with 1% slippage tolerance quote = pool.fetch_fixed_input_swap_quote(ALGO(1_000_000), slippage=0.01) print(quote) -print(f'TINYUSDC per ALGO: {quote.price}') -print(f'TINYUSDC per ALGO (worst case): {quote.price_with_slippage}') +print(f"TINYUSDC per ALGO: {quote.price}") +print(f"TINYUSDC per ALGO (worst case): {quote.price_with_slippage}") # We only want to sell if ALGO is > 180 TINYUSDC (It's testnet!) if quote.price_with_slippage > 180: - print(f'Swapping {quote.amount_in} to {quote.amount_out_with_slippage}') + print(f"Swapping {quote.amount_in} to {quote.amount_out_with_slippage}") # Prepare a transaction group transaction_group = pool.prepare_swap_transactions_from_quote(quote) # Sign the group with our key - transaction_group.sign_with_private_key(account['address'], account['private_key']) + transaction_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation result = client.submit(transaction_group, wait=True) @@ -55,9 +57,11 @@ excess = pool.fetch_excess_amounts() if TINYUSDC in excess: amount = excess[TINYUSDC] - print(f'Excess: {amount}') + print(f"Excess: {amount}") # We might just let the excess accumulate rather than redeeming if its < 1 TinyUSDC if amount > 1_000_000: transaction_group = pool.prepare_redeem_transactions(amount) - transaction_group.sign_with_private_key(account['address'], account['private_key']) + transaction_group.sign_with_private_key( + account["address"], account["private_key"] + ) result = client.submit(transaction_group, wait=True) diff --git a/examples/swapping1_less_convenience.py b/examples/swapping1_less_convenience.py index 04789af..6345c4e 100644 --- a/examples/swapping1_less_convenience.py +++ b/examples/swapping1_less_convenience.py @@ -16,12 +16,14 @@ # Hardcoding account keys is not a great practice. This is for demonstration purposes only. # See the README & Docs for alternative signing methods. account = { - 'address': 'ALGORAND_ADDRESS_HERE', - 'private_key': 'base64_private_key_here', # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary + "address": "ALGORAND_ADDRESS_HERE", + "private_key": "base64_private_key_here", # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary } -algod = AlgodClient('', 'http://localhost:8080', headers={'User-Agent': 'algosdk'}) +algod = AlgodClient( + "", "http://localhost:8080", headers={"User-Agent": "algosdk"} +) client = TinymanClient( algod_client=algod, @@ -30,19 +32,19 @@ # Check if the account is opted into Tinyman and optin if necessary -if(not client.is_opted_in(account['address'])): - print('Account not opted into app, opting in now..') - transaction_group = client.prepare_app_optin_transactions(account['address']) +if not client.is_opted_in(account["address"]): + print("Account not opted into app, opting in now..") + transaction_group = client.prepare_app_optin_transactions(account["address"]) for i, txn in enumerate(transaction_group.transactions): - if txn.sender == account['address']: - transaction_group.signed_transactions[i] = txn.sign(account['private_key']) + if txn.sender == account["address"]: + transaction_group.signed_transactions[i] = txn.sign(account["private_key"]) txid = client.algod.send_transactions(transaction_group.signed_transactions) wait_for_confirmation(algod, txid) # Fetch our two assets of interest -TINYUSDC = Asset(id=21582668, name='TinyUSDC', unit_name='TINYUSDC', decimals=6) -ALGO = Asset(id=0, name='Algo', unit_name='ALGO', decimals=6) +TINYUSDC = Asset(id=21582668, name="TinyUSDC", unit_name="TINYUSDC", decimals=6) +ALGO = Asset(id=0, name="Algo", unit_name="ALGO", decimals=6) # Create the pool we will work with and fetch its on-chain state pool = Pool(client, asset_a=TINYUSDC, asset_b=ALGO, fetch=True) @@ -51,37 +53,41 @@ # Get a quote for a swap of 1 ALGO to TINYUSDC with 1% slippage tolerance quote = pool.fetch_fixed_input_swap_quote(ALGO(1_000_000), slippage=0.01) print(quote) -print(f'TINYUSDC per ALGO: {quote.price}') -print(f'TINYUSDC per ALGO (worst case): {quote.price_with_slippage}') +print(f"TINYUSDC per ALGO: {quote.price}") +print(f"TINYUSDC per ALGO (worst case): {quote.price_with_slippage}") # We only want to sell if ALGO is > 180 TINYUSDC (It's testnet!) if quote.price_with_slippage > 180: - print(f'Swapping {quote.amount_in} to {quote.amount_out_with_slippage}') + print(f"Swapping {quote.amount_in} to {quote.amount_out_with_slippage}") # Prepare a transaction group transaction_group = pool.prepare_swap_transactions( amount_in=quote.amount_in, amount_out=quote.amount_out_with_slippage, - swap_type='fixed-input', - swapper_address=account['address'], + swap_type="fixed-input", + swapper_address=account["address"], ) # Sign the group with our key for i, txn in enumerate(transaction_group.transactions): - if txn.sender == account['address']: - transaction_group.signed_transactions[i] = txn.sign(account['private_key']) + if txn.sender == account["address"]: + transaction_group.signed_transactions[i] = txn.sign(account["private_key"]) txid = algod.send_transactions(transaction_group.signed_transactions) wait_for_confirmation(algod, txid) # Check if any excess remaining after the swap - excess = pool.fetch_excess_amounts(account['address']) + excess = pool.fetch_excess_amounts(account["address"]) if TINYUSDC.id in excess: amount = excess[TINYUSDC.id] - print(f'Excess: {amount}') + print(f"Excess: {amount}") # We might just let the excess accumulate rather than redeeming if its < 1 TinyUSDC if amount > 1_000_000: - transaction_group = pool.prepare_redeem_transactions(amount, account['address']) + transaction_group = pool.prepare_redeem_transactions( + amount, account["address"] + ) # Sign the group with our key for i, txn in enumerate(transaction_group.transactions): - if txn.sender == account['address']: - transaction_group.signed_transactions[i] = txn.sign(account['private_key']) + if txn.sender == account["address"]: + transaction_group.signed_transactions[i] = txn.sign( + account["private_key"] + ) txid = algod.send_transactions(transaction_group.signed_transactions) wait_for_confirmation(algod, txid) diff --git a/setup.py b/setup.py index 9e606aa..b4ce07e 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,6 @@ install_requires=["py-algorand-sdk >= 1.6.0"], packages=setuptools.find_packages(), python_requires=">=3.7", - package_data={'tinyman.v1': ['asc.json']}, + package_data={"tinyman.v1": ["asc.json"]}, include_package_data=True, ) diff --git a/tinyman/assets.py b/tinyman/assets.py index 0bd89c2..f367058 100644 --- a/tinyman/assets.py +++ b/tinyman/assets.py @@ -17,20 +17,20 @@ def __hash__(self) -> int: def fetch(self, algod): if self.id > 0: - params = algod.asset_info(self.id)['params'] + params = algod.asset_info(self.id)["params"] else: params = { - 'name': 'Algo', - 'unit-name': 'ALGO', - 'decimals': 6, + "name": "Algo", + "unit-name": "ALGO", + "decimals": 6, } - self.name = params['name'] - self.unit_name = params['unit-name'] - self.decimals = params['decimals'] + self.name = params["name"] + self.unit_name = params["unit-name"] + self.decimals = params["decimals"] return self def __repr__(self) -> str: - return f'Asset({self.unit_name} - {self.id})' + return f"Asset({self.unit_name} - {self.id})" @dataclass @@ -41,39 +41,39 @@ class AssetAmount: def __mul__(self, other: float): if isinstance(other, (float, int)): return AssetAmount(self.asset, int(self.amount * other)) - raise TypeError('Unsupported types for *') + raise TypeError("Unsupported types for *") def __add__(self, other: "AssetAmount"): if isinstance(other, AssetAmount) and other.asset == self.asset: return AssetAmount(self.asset, int(self.amount + other.amount)) - raise TypeError('Unsupported types for +') + raise TypeError("Unsupported types for +") def __sub__(self, other: "AssetAmount"): if isinstance(other, AssetAmount) and other.asset == self.asset: return AssetAmount(self.asset, int(self.amount - other.amount)) - raise TypeError('Unsupported types for -') + raise TypeError("Unsupported types for -") def __gt__(self, other: "AssetAmount"): if isinstance(other, AssetAmount) and other.asset == self.asset: return self.amount > other.amount if isinstance(other, (float, int)): return self.amount > other - raise TypeError('Unsupported types for >') + raise TypeError("Unsupported types for >") def __lt__(self, other: "AssetAmount"): if isinstance(other, AssetAmount) and other.asset == self.asset: return self.amount < other.amount if isinstance(other, (float, int)): return self.amount < other - raise TypeError('Unsupported types for <') + raise TypeError("Unsupported types for <") def __eq__(self, other: "AssetAmount"): if isinstance(other, AssetAmount) and other.asset == self.asset: return self.amount == other.amount if isinstance(other, (float, int)): return self.amount == other - raise TypeError('Unsupported types for ==') + raise TypeError("Unsupported types for ==") def __repr__(self) -> str: amount = Decimal(self.amount) / Decimal(10**self.asset.decimals) - return f'{self.asset.unit_name}(\'{amount}\')' + return f"{self.asset.unit_name}('{amount}')" diff --git a/tinyman/utils.py b/tinyman/utils.py index 24b8e37..ba3b483 100644 --- a/tinyman/utils.py +++ b/tinyman/utils.py @@ -1,28 +1,32 @@ from base64 import b64decode, b64encode import warnings from datetime import datetime -from algosdk.future.transaction import LogicSigTransaction, assign_group_id, wait_for_confirmation as wait_for_confirmation_algosdk +from algosdk.future.transaction import ( + LogicSigTransaction, + assign_group_id, + wait_for_confirmation as wait_for_confirmation_algosdk, +) from algosdk.error import AlgodHTTPError -warnings.simplefilter('always', DeprecationWarning) +warnings.simplefilter("always", DeprecationWarning) def get_program(definition, variables=None): """ Return a byte array to be used in LogicSig. """ - template = definition['bytecode'] + template = definition["bytecode"] template_bytes = list(b64decode(template)) offset = 0 - for v in sorted(definition['variables'], key=lambda v: v['index']): - name = v['name'].split('TMPL_')[-1].lower() + for v in sorted(definition["variables"], key=lambda v: v["index"]): + name = v["name"].split("TMPL_")[-1].lower() value = variables[name] - start = v['index'] - offset - end = start + v['length'] - value_encoded = encode_value(value, v['type']) + start = v["index"] - offset + end = start + v["length"] + value_encoded = encode_value(value, v["type"]) value_encoded_len = len(value_encoded) - diff = v['length'] - value_encoded_len + diff = v["length"] - value_encoded_len offset += diff template_bytes[start:end] = list(value_encoded) @@ -30,15 +34,15 @@ def get_program(definition, variables=None): def encode_value(value, type): - if type == 'int': + if type == "int": return encode_varint(value) - raise Exception('Unsupported value type %s!' % type) + raise Exception("Unsupported value type %s!" % type) def encode_varint(number): - buf = b'' + buf = b"" while True: - towrite = number & 0x7f + towrite = number & 0x7F number >>= 7 if number: buf += bytes([towrite | 0x80]) @@ -48,7 +52,9 @@ def encode_varint(number): return buf -def sign_and_submit_transactions(client, transactions, signed_transactions, sender, sender_sk): +def sign_and_submit_transactions( + client, transactions, signed_transactions, sender, sender_sk +): for i, txn in enumerate(transactions): if txn.sender == sender: signed_transactions[i] = txn.sign(sender_sk) @@ -64,54 +70,58 @@ def wait_for_confirmation(client, txid): Deprecated. Use algosdk if you are importing wait_for_confirmation individually. """ - warnings.warn('tinyman.utils.wait_for_confirmation is deprecated. Use algosdk.future.transaction.wait_for_confirmation instead if you are importing individually.', DeprecationWarning, stacklevel=2) + warnings.warn( + "tinyman.utils.wait_for_confirmation is deprecated. Use algosdk.future.transaction.wait_for_confirmation instead if you are importing individually.", + DeprecationWarning, + stacklevel=2, + ) txinfo = wait_for_confirmation_algosdk(client, txid) txinfo["txid"] = txid return txinfo def int_to_bytes(num): - return num.to_bytes(8, 'big') + return num.to_bytes(8, "big") def int_list_to_bytes(nums): - return b''.join([int_to_bytes(x) for x in nums]) + return b"".join([int_to_bytes(x) for x in nums]) def bytes_to_int(b): - return int.from_bytes(b, 'big') + return int.from_bytes(b, "big") def bytes_to_int_list(b): n = len(b) // 8 - return [bytes_to_int(b[(i * 8):((i + 1) * 8)]) for i in range(n)] + return [bytes_to_int(b[(i * 8) : ((i + 1) * 8)]) for i in range(n)] def get_state_int(state, key): if type(key) == str: key = b64encode(key.encode()) - return state.get(key.decode(), {'uint': 0})['uint'] + return state.get(key.decode(), {"uint": 0})["uint"] def get_state_bytes(state, key): if type(key) == str: key = b64encode(key.encode()) - return state.get(key.decode(), {'bytes': ''})['bytes'] + return state.get(key.decode(), {"bytes": ""})["bytes"] def get_state_from_account_info(account_info, app_id): try: - app = [a for a in account_info['apps-local-state'] if a['id'] == app_id][0] + app = [a for a in account_info["apps-local-state"] if a["id"] == app_id][0] except IndexError: return {} try: app_state = {} - for x in app['key-value']: - key = b64decode(x['key']) - if x['value']['type'] == 1: - value = b64decode(x['value'].get('bytes', '')) + for x in app["key-value"]: + key = b64decode(x["key"]) + if x["value"]["type"] == 1: + value = b64decode(x["value"].get("bytes", "")) else: - value = x['value'].get('uint', 0) + value = x["value"].get("uint", 0) app_state[key] = value except KeyError: return {} @@ -121,25 +131,24 @@ def get_state_from_account_info(account_info, app_id): def apply_delta(state, delta): state = dict(state) for d in delta: - key = b64decode(d['key']) - if d['value']['action'] == 1: - state[key] = b64decode(d['value'].get('bytes', '')) - elif d['value']['action'] == 2: - state[key] = d['value'].get('uint', 0) - elif d['value']['action'] == 3: + key = b64decode(d["key"]) + if d["value"]["action"] == 1: + state[key] = b64decode(d["value"].get("bytes", "")) + elif d["value"]["action"] == 2: + state[key] = d["value"].get("uint", 0) + elif d["value"]["action"] == 3: state.pop(key) else: - raise Exception(d['value']['action']) + raise Exception(d["value"]["action"]) return state def timestamp_to_date_str(t): d = datetime.fromtimestamp(t).date() - return d.strftime('%Y-%m-%d') + return d.strftime("%Y-%m-%d") class TransactionGroup: - def __init__(self, transactions): transactions = assign_group_id(transactions) self.transactions = transactions @@ -168,4 +177,4 @@ def submit(self, algod, wait=False): txinfo = wait_for_confirmation_algosdk(algod, txid) txinfo["txid"] = txid return txinfo - return {'txid': txid} + return {"txid": txid} diff --git a/tinyman/v1/bootstrap.py b/tinyman/v1/bootstrap.py index 7da9a8f..3080cef 100644 --- a/tinyman/v1/bootstrap.py +++ b/tinyman/v1/bootstrap.py @@ -1,17 +1,30 @@ -from algosdk.future.transaction import ApplicationOptInTxn, PaymentTxn, AssetCreateTxn, AssetOptInTxn +from algosdk.future.transaction import ( + ApplicationOptInTxn, + PaymentTxn, + AssetCreateTxn, + AssetOptInTxn, +) from tinyman.utils import int_to_bytes, TransactionGroup from .contracts import get_pool_logicsig -def prepare_bootstrap_transactions(validator_app_id, asset1_id, asset2_id, asset1_unit_name, asset2_unit_name, sender, suggested_params): +def prepare_bootstrap_transactions( + validator_app_id, + asset1_id, + asset2_id, + asset1_unit_name, + asset2_unit_name, + sender, + suggested_params, +): pool_logicsig = get_pool_logicsig(validator_app_id, asset1_id, asset2_id) pool_address = pool_logicsig.address() - assert(asset1_id > asset2_id) + assert asset1_id > asset2_id if asset2_id == 0: - asset2_unit_name = 'ALGO' + asset2_unit_name = "ALGO" txns = [ PaymentTxn( @@ -19,13 +32,13 @@ def prepare_bootstrap_transactions(validator_app_id, asset1_id, asset2_id, asset sp=suggested_params, receiver=pool_address, amt=961000 if asset2_id > 0 else 860000, - note='fee', + note="fee", ), ApplicationOptInTxn( sender=pool_address, sp=suggested_params, index=validator_app_id, - app_args=['bootstrap', int_to_bytes(asset1_id), int_to_bytes(asset2_id)], + app_args=["bootstrap", int_to_bytes(asset1_id), int_to_bytes(asset2_id)], foreign_assets=[asset1_id] if asset2_id == 0 else [asset1_id, asset2_id], ), AssetCreateTxn( @@ -33,9 +46,9 @@ def prepare_bootstrap_transactions(validator_app_id, asset1_id, asset2_id, asset sp=suggested_params, total=0xFFFFFFFFFFFFFFFF, decimals=6, - unit_name='TMPOOL11', - asset_name=f'TinymanPool1.1 {asset1_unit_name}-{asset2_unit_name}', - url='https://tinyman.org', + unit_name="TMPOOL11", + asset_name=f"TinymanPool1.1 {asset1_unit_name}-{asset2_unit_name}", + url="https://tinyman.org", default_frozen=False, ), AssetOptInTxn( diff --git a/tinyman/v1/burn.py b/tinyman/v1/burn.py index 3071b79..53f82e3 100644 --- a/tinyman/v1/burn.py +++ b/tinyman/v1/burn.py @@ -4,7 +4,17 @@ from .contracts import get_pool_logicsig -def prepare_burn_transactions(validator_app_id, asset1_id, asset2_id, liquidity_asset_id, asset1_amount, asset2_amount, liquidity_asset_amount, sender, suggested_params): +def prepare_burn_transactions( + validator_app_id, + asset1_id, + asset2_id, + liquidity_asset_id, + asset1_amount, + asset2_amount, + liquidity_asset_amount, + sender, + suggested_params, +): pool_logicsig = get_pool_logicsig(validator_app_id, asset1_id, asset2_id) pool_address = pool_logicsig.address() @@ -14,15 +24,17 @@ def prepare_burn_transactions(validator_app_id, asset1_id, asset2_id, liquidity_ sp=suggested_params, receiver=pool_address, amt=3000, - note='fee', + note="fee", ), ApplicationNoOpTxn( sender=pool_address, sp=suggested_params, index=validator_app_id, - app_args=['burn'], + app_args=["burn"], accounts=[sender], - foreign_assets=[asset1_id, liquidity_asset_id] if asset2_id == 0 else [asset1_id, asset2_id, liquidity_asset_id], + foreign_assets=[asset1_id, liquidity_asset_id] + if asset2_id == 0 + else [asset1_id, asset2_id, liquidity_asset_id], ), AssetTransferTxn( sender=pool_address, @@ -37,7 +49,9 @@ def prepare_burn_transactions(validator_app_id, asset1_id, asset2_id, liquidity_ receiver=sender, amt=int(asset2_amount), index=asset2_id, - ) if asset2_id != 0 else PaymentTxn( + ) + if asset2_id != 0 + else PaymentTxn( sender=pool_address, sp=suggested_params, receiver=sender, diff --git a/tinyman/v1/client.py b/tinyman/v1/client.py index 218ebf8..50d5768 100644 --- a/tinyman/v1/client.py +++ b/tinyman/v1/client.py @@ -4,11 +4,22 @@ from algosdk.future.transaction import wait_for_confirmation from tinyman.assets import Asset, AssetAmount from .optin import prepare_app_optin_transactions, prepare_asset_optin_transactions -from .constants import TESTNET_VALIDATOR_APP_ID, MAINNET_VALIDATOR_APP_ID, TESTNET_STAKING_APP_ID, MAINNET_STAKING_APP_ID +from .constants import ( + TESTNET_VALIDATOR_APP_ID, + MAINNET_VALIDATOR_APP_ID, + TESTNET_STAKING_APP_ID, + MAINNET_STAKING_APP_ID, +) class TinymanClient: - def __init__(self, algod_client: AlgodClient, validator_app_id: int, user_address=None, staking_app_id: int = None): + def __init__( + self, + algod_client: AlgodClient, + validator_app_id: int, + user_address=None, + staking_app_id: int = None, + ): self.algod = algod_client self.validator_app_id = validator_app_id self.staking_app_id = staking_app_id @@ -17,6 +28,7 @@ def __init__(self, algod_client: AlgodClient, validator_app_id: int, user_addres def fetch_pool(self, asset1, asset2, fetch=True): from .pools import Pool + return Pool(self, asset1, asset2, fetch=fetch) def fetch_asset(self, asset_id): @@ -32,7 +44,7 @@ def submit(self, transaction_group, wait=False): txinfo = wait_for_confirmation(self.algod, txid) txinfo["txid"] = txid return txinfo - return {'txid': txid} + return {"txid": txid} def prepare_app_optin_transactions(self, user_address=None): user_address = user_address or self.user_address @@ -58,22 +70,28 @@ def fetch_excess_amounts(self, user_address=None): user_address = user_address or self.user_address account_info = self.algod.account_info(user_address) try: - validator_app = [a for a in account_info['apps-local-state'] if a['id'] == self.validator_app_id][0] + validator_app = [ + a + for a in account_info["apps-local-state"] + if a["id"] == self.validator_app_id + ][0] except IndexError: return {} try: - validator_app_state = {x['key']: x['value'] for x in validator_app['key-value']} + validator_app_state = { + x["key"]: x["value"] for x in validator_app["key-value"] + } except KeyError: return {} pools = {} for key in validator_app_state: b = b64decode(key.encode()) - if b[-9:-8] == b'e': - value = validator_app_state[key]['uint'] + if b[-9:-8] == b"e": + value = validator_app_state[key]["uint"] pool_address = encode_address(b[:-9]) pools[pool_address] = pools.get(pool_address, {}) - asset_id = int.from_bytes(b[-8:], 'big') + asset_id = int.from_bytes(b[-8:], "big") asset = self.fetch_asset(asset_id) pools[pool_address][asset] = AssetAmount(asset, value) @@ -82,25 +100,35 @@ def fetch_excess_amounts(self, user_address=None): def is_opted_in(self, user_address=None): user_address = user_address or self.user_address account_info = self.algod.account_info(user_address) - for a in account_info.get('apps-local-state', []): - if a['id'] == self.validator_app_id: + for a in account_info.get("apps-local-state", []): + if a["id"] == self.validator_app_id: return True return False def asset_is_opted_in(self, asset_id, user_address=None): user_address = user_address or self.user_address account_info = self.algod.account_info(user_address) - for a in account_info.get('assets', []): - if a['asset-id'] == asset_id: + for a in account_info.get("assets", []): + if a["asset-id"] == asset_id: return True return False class TinymanTestnetClient(TinymanClient): def __init__(self, algod_client: AlgodClient, user_address=None): - super().__init__(algod_client, validator_app_id=TESTNET_VALIDATOR_APP_ID, user_address=user_address, staking_app_id=TESTNET_STAKING_APP_ID) + super().__init__( + algod_client, + validator_app_id=TESTNET_VALIDATOR_APP_ID, + user_address=user_address, + staking_app_id=TESTNET_STAKING_APP_ID, + ) class TinymanMainnetClient(TinymanClient): def __init__(self, algod_client: AlgodClient, user_address=None): - super().__init__(algod_client, validator_app_id=MAINNET_VALIDATOR_APP_ID, user_address=user_address, staking_app_id=MAINNET_STAKING_APP_ID) + super().__init__( + algod_client, + validator_app_id=MAINNET_VALIDATOR_APP_ID, + user_address=user_address, + staking_app_id=MAINNET_STAKING_APP_ID, + ) diff --git a/tinyman/v1/contracts.py b/tinyman/v1/contracts.py index 7e3413d..e9a61ab 100644 --- a/tinyman/v1/contracts.py +++ b/tinyman/v1/contracts.py @@ -4,20 +4,23 @@ import tinyman.v1 from tinyman.utils import get_program -_contracts = json.loads(importlib.resources.read_text(tinyman.v1, 'asc.json')) +_contracts = json.loads(importlib.resources.read_text(tinyman.v1, "asc.json")) -pool_logicsig_def = _contracts['contracts']['pool_logicsig']['logic'] +pool_logicsig_def = _contracts["contracts"]["pool_logicsig"]["logic"] -validator_app_def = _contracts['contracts']['validator_app'] +validator_app_def = _contracts["contracts"]["validator_app"] def get_pool_logicsig(validator_app_id, asset1_id, asset2_id): assets = [asset1_id, asset2_id] asset_id_1 = max(assets) asset_id_2 = min(assets) - program_bytes = get_program(pool_logicsig_def, variables=dict( - validator_app_id=validator_app_id, - asset_id_1=asset_id_1, - asset_id_2=asset_id_2, - )) + program_bytes = get_program( + pool_logicsig_def, + variables=dict( + validator_app_id=validator_app_id, + asset_id_1=asset_id_1, + asset_id_2=asset_id_2, + ), + ) return LogicSig(program=program_bytes) diff --git a/tinyman/v1/fees.py b/tinyman/v1/fees.py index ef67d86..857379e 100644 --- a/tinyman/v1/fees.py +++ b/tinyman/v1/fees.py @@ -4,7 +4,16 @@ from .contracts import get_pool_logicsig -def prepare_redeem_fees_transactions(validator_app_id, asset1_id, asset2_id, liquidity_asset_id, amount, creator, sender, suggested_params): +def prepare_redeem_fees_transactions( + validator_app_id, + asset1_id, + asset2_id, + liquidity_asset_id, + amount, + creator, + sender, + suggested_params, +): pool_logicsig = get_pool_logicsig(validator_app_id, asset1_id, asset2_id) pool_address = pool_logicsig.address() @@ -14,14 +23,16 @@ def prepare_redeem_fees_transactions(validator_app_id, asset1_id, asset2_id, liq sp=suggested_params, receiver=pool_address, amt=2000, - note='fee', + note="fee", ), ApplicationNoOpTxn( sender=pool_address, sp=suggested_params, index=validator_app_id, - app_args=['fees'], - foreign_assets=[asset1_id, liquidity_asset_id] if asset2_id == 0 else [asset1_id, asset2_id, liquidity_asset_id], + app_args=["fees"], + foreign_assets=[asset1_id, liquidity_asset_id] + if asset2_id == 0 + else [asset1_id, asset2_id, liquidity_asset_id], ), AssetTransferTxn( sender=pool_address, @@ -29,7 +40,7 @@ def prepare_redeem_fees_transactions(validator_app_id, asset1_id, asset2_id, liq receiver=creator, amt=int(amount), index=liquidity_asset_id, - ) + ), ] txn_group = TransactionGroup(txns) txn_group.sign_with_logicisg(pool_logicsig) diff --git a/tinyman/v1/mint.py b/tinyman/v1/mint.py index 6157ac1..77a7c52 100644 --- a/tinyman/v1/mint.py +++ b/tinyman/v1/mint.py @@ -4,7 +4,17 @@ from .contracts import get_pool_logicsig -def prepare_mint_transactions(validator_app_id, asset1_id, asset2_id, liquidity_asset_id, asset1_amount, asset2_amount, liquidity_asset_amount, sender, suggested_params): +def prepare_mint_transactions( + validator_app_id, + asset1_id, + asset2_id, + liquidity_asset_id, + asset1_amount, + asset2_amount, + liquidity_asset_amount, + sender, + suggested_params, +): pool_logicsig = get_pool_logicsig(validator_app_id, asset1_id, asset2_id) pool_address = pool_logicsig.address() @@ -14,15 +24,17 @@ def prepare_mint_transactions(validator_app_id, asset1_id, asset2_id, liquidity_ sp=suggested_params, receiver=pool_address, amt=2000, - note='fee', + note="fee", ), ApplicationNoOpTxn( sender=pool_address, sp=suggested_params, index=validator_app_id, - app_args=['mint'], + app_args=["mint"], accounts=[sender], - foreign_assets=[asset1_id, liquidity_asset_id] if asset2_id == 0 else [asset1_id, asset2_id, liquidity_asset_id], + foreign_assets=[asset1_id, liquidity_asset_id] + if asset2_id == 0 + else [asset1_id, asset2_id, liquidity_asset_id], ), AssetTransferTxn( sender=sender, @@ -37,7 +49,9 @@ def prepare_mint_transactions(validator_app_id, asset1_id, asset2_id, liquidity_ receiver=pool_address, amt=int(asset2_amount), index=asset2_id, - ) if asset2_id != 0 else PaymentTxn( + ) + if asset2_id != 0 + else PaymentTxn( sender=sender, sp=suggested_params, receiver=pool_address, diff --git a/tinyman/v1/pools.py b/tinyman/v1/pools.py index a2ca672..21e9dc9 100644 --- a/tinyman/v1/pools.py +++ b/tinyman/v1/pools.py @@ -25,54 +25,62 @@ def get_pool_info(client: AlgodClient, validator_app_id, asset1_id, asset2_id): def get_pool_info_from_account_info(account_info): try: - validator_app_id = account_info['apps-local-state'][0]['id'] + validator_app_id = account_info["apps-local-state"][0]["id"] except IndexError: return {} - validator_app_state = {x['key']: x['value'] for x in account_info['apps-local-state'][0]['key-value']} + validator_app_state = { + x["key"]: x["value"] for x in account_info["apps-local-state"][0]["key-value"] + } - asset1_id = get_state_int(validator_app_state, 'a1') - asset2_id = get_state_int(validator_app_state, 'a2') + asset1_id = get_state_int(validator_app_state, "a1") + asset2_id = get_state_int(validator_app_state, "a2") pool_logicsig = get_pool_logicsig(validator_app_id, asset1_id, asset2_id) pool_address = pool_logicsig.address() - assert(account_info['address'] == pool_address) + assert account_info["address"] == pool_address - asset1_reserves = get_state_int(validator_app_state, 's1') - asset2_reserves = get_state_int(validator_app_state, 's2') - issued_liquidity = get_state_int(validator_app_state, 'ilt') - unclaimed_protocol_fees = get_state_int(validator_app_state, 'p') + asset1_reserves = get_state_int(validator_app_state, "s1") + asset2_reserves = get_state_int(validator_app_state, "s2") + issued_liquidity = get_state_int(validator_app_state, "ilt") + unclaimed_protocol_fees = get_state_int(validator_app_state, "p") - liquidity_asset = account_info['created-assets'][0] - liquidity_asset_id = liquidity_asset['index'] + liquidity_asset = account_info["created-assets"][0] + liquidity_asset_id = liquidity_asset["index"] - outstanding_asset1_amount = get_state_int(validator_app_state, b64encode(b'o' + (asset1_id).to_bytes(8, 'big'))) - outstanding_asset2_amount = get_state_int(validator_app_state, b64encode(b'o' + (asset2_id).to_bytes(8, 'big'))) - outstanding_liquidity_asset_amount = get_state_int(validator_app_state, b64encode(b'o' + (liquidity_asset_id).to_bytes(8, 'big'))) + outstanding_asset1_amount = get_state_int( + validator_app_state, b64encode(b"o" + (asset1_id).to_bytes(8, "big")) + ) + outstanding_asset2_amount = get_state_int( + validator_app_state, b64encode(b"o" + (asset2_id).to_bytes(8, "big")) + ) + outstanding_liquidity_asset_amount = get_state_int( + validator_app_state, b64encode(b"o" + (liquidity_asset_id).to_bytes(8, "big")) + ) pool = { - 'address': pool_address, - 'asset1_id': asset1_id, - 'asset2_id': asset2_id, - 'liquidity_asset_id': liquidity_asset['index'], - 'liquidity_asset_name': liquidity_asset['params']['name'], - 'asset1_reserves': asset1_reserves, - 'asset2_reserves': asset2_reserves, - 'issued_liquidity': issued_liquidity, - 'unclaimed_protocol_fees': unclaimed_protocol_fees, - 'outstanding_asset1_amount': outstanding_asset1_amount, - 'outstanding_asset2_amount': outstanding_asset2_amount, - 'outstanding_liquidity_asset_amount': outstanding_liquidity_asset_amount, - 'validator_app_id': validator_app_id, - 'algo_balance': account_info['amount'], - 'round': account_info['round'], + "address": pool_address, + "asset1_id": asset1_id, + "asset2_id": asset2_id, + "liquidity_asset_id": liquidity_asset["index"], + "liquidity_asset_name": liquidity_asset["params"]["name"], + "asset1_reserves": asset1_reserves, + "asset2_reserves": asset2_reserves, + "issued_liquidity": issued_liquidity, + "unclaimed_protocol_fees": unclaimed_protocol_fees, + "outstanding_asset1_amount": outstanding_asset1_amount, + "outstanding_asset2_amount": outstanding_asset2_amount, + "outstanding_liquidity_asset_amount": outstanding_liquidity_asset_amount, + "validator_app_id": validator_app_id, + "algo_balance": account_info["amount"], + "round": account_info["round"], } return pool def get_excess_asset_key(pool_address, asset_id): a = decode_address(pool_address) - key = b64encode(a + b'e' + (asset_id).to_bytes(8, 'big')) + key = b64encode(a + b"e" + (asset_id).to_bytes(8, "big")) return key @@ -87,14 +95,14 @@ class SwapQuote: @property def amount_out_with_slippage(self) -> AssetAmount: - if self.swap_type == 'fixed-output': + if self.swap_type == "fixed-output": return self.amount_out else: return self.amount_out - (self.amount_out * self.slippage) @property def amount_in_with_slippage(self) -> AssetAmount: - if self.swap_type == 'fixed-input': + if self.swap_type == "fixed-input": return self.amount_in else: return self.amount_in + (self.amount_in * self.slippage) @@ -105,23 +113,27 @@ def price(self) -> float: @property def price_with_slippage(self) -> float: - return self.amount_out_with_slippage.amount / self.amount_in_with_slippage.amount + return ( + self.amount_out_with_slippage.amount / self.amount_in_with_slippage.amount + ) @dataclass class MintQuote: - amounts_in: 'dict[AssetAmount]' + amounts_in: "dict[AssetAmount]" liquidity_asset_amount: AssetAmount slippage: float @property def liquidity_asset_amount_with_slippage(self) -> int: - return self.liquidity_asset_amount - (self.liquidity_asset_amount * self.slippage) + return self.liquidity_asset_amount - ( + self.liquidity_asset_amount * self.slippage + ) @dataclass class BurnQuote: - amounts_out: 'dict[AssetAmount]' + amounts_out: "dict[AssetAmount]" liquidity_asset_amount: AssetAmount slippage: float @@ -134,9 +146,21 @@ def amounts_out_with_slippage(self) -> dict: class Pool: - def __init__(self, client: TinymanClient, asset_a: Asset, asset_b: Asset, info=None, fetch=True, validator_app_id=None) -> None: + def __init__( + self, + client: TinymanClient, + asset_a: Asset, + asset_b: Asset, + info=None, + fetch=True, + validator_app_id=None, + ) -> None: self.client = client - self.validator_app_id = validator_app_id if validator_app_id is not None else client.validator_app_id + self.validator_app_id = ( + validator_app_id + if validator_app_id is not None + else client.validator_app_id + ) if isinstance(asset_a, int): asset_a = client.fetch_asset(asset_a) @@ -168,36 +192,55 @@ def __init__(self, client: TinymanClient, asset_a: Asset, asset_b: Asset, info=N @classmethod def from_account_info(cls, account_info, client=None): info = get_pool_info_from_account_info(account_info) - pool = Pool(client, info['asset1_id'], info['asset2_id'], info, validator_app_id=info['validator_app_id']) + pool = Pool( + client, + info["asset1_id"], + info["asset2_id"], + info, + validator_app_id=info["validator_app_id"], + ) return pool def refresh(self, info=None): if info is None: - info = get_pool_info(self.client.algod, self.validator_app_id, self.asset1.id, self.asset2.id) + info = get_pool_info( + self.client.algod, self.validator_app_id, self.asset1.id, self.asset2.id + ) if not info: return self.update_from_info(info) def update_from_info(self, info): - if info['liquidity_asset_id'] is not None: + if info["liquidity_asset_id"] is not None: self.exists = True - self.liquidity_asset = Asset(info['liquidity_asset_id'], name=info['liquidity_asset_name'], unit_name='TMPOOL11', decimals=6) - self.asset1_reserves = info['asset1_reserves'] - self.asset2_reserves = info['asset2_reserves'] - self.issued_liquidity = info['issued_liquidity'] - self.unclaimed_protocol_fees = info['unclaimed_protocol_fees'] - self.outstanding_asset1_amount = info['outstanding_asset1_amount'] - self.outstanding_asset2_amount = info['outstanding_asset2_amount'] - self.outstanding_liquidity_asset_amount = info['outstanding_liquidity_asset_amount'] - self.last_refreshed_round = info['round'] - - self.algo_balance = info['algo_balance'] + self.liquidity_asset = Asset( + info["liquidity_asset_id"], + name=info["liquidity_asset_name"], + unit_name="TMPOOL11", + decimals=6, + ) + self.asset1_reserves = info["asset1_reserves"] + self.asset2_reserves = info["asset2_reserves"] + self.issued_liquidity = info["issued_liquidity"] + self.unclaimed_protocol_fees = info["unclaimed_protocol_fees"] + self.outstanding_asset1_amount = info["outstanding_asset1_amount"] + self.outstanding_asset2_amount = info["outstanding_asset2_amount"] + self.outstanding_liquidity_asset_amount = info[ + "outstanding_liquidity_asset_amount" + ] + self.last_refreshed_round = info["round"] + + self.algo_balance = info["algo_balance"] self.min_balance = self.get_minimum_balance() if self.asset2.id == 0: - self.asset2_reserves = (self.algo_balance - self.min_balance) - self.outstanding_asset2_amount + self.asset2_reserves = ( + self.algo_balance - self.min_balance + ) - self.outstanding_asset2_amount def get_logicsig(self): - pool_logicsig = get_pool_logicsig(self.validator_app_id, self.asset1.id, self.asset2.id) + pool_logicsig = get_pool_logicsig( + self.validator_app_id, self.asset1.id, self.asset2.id + ) return pool_logicsig @property @@ -216,21 +259,21 @@ def asset2_price(self): def info(self): pool = { - 'address': self.address, - 'asset1_id': self.asset1.id, - 'asset2_id': self.asset2.id, - 'asset1_unit_name': self.asset1.unit_name, - 'asset2_unit_name': self.asset2.unit_name, - 'liquidity_asset_id': self.liquidity_asset.id, - 'liquidity_asset_name': self.liquidity_asset.name, - 'asset1_reserves': self.asset1_reserves, - 'asset2_reserves': self.asset2_reserves, - 'issued_liquidity': self.issued_liquidity, - 'unclaimed_protocol_fees': self.unclaimed_protocol_fees, - 'outstanding_asset1_amount': self.outstanding_asset1_amount, - 'outstanding_asset2_amount': self.outstanding_asset2_amount, - 'outstanding_liquidity_asset_amount': self.outstanding_liquidity_asset_amount, - 'last_refreshed_round': self.last_refreshed_round, + "address": self.address, + "asset1_id": self.asset1.id, + "asset2_id": self.asset2.id, + "asset1_unit_name": self.asset1.unit_name, + "asset2_unit_name": self.asset2.unit_name, + "liquidity_asset_id": self.liquidity_asset.id, + "liquidity_asset_name": self.liquidity_asset.name, + "asset1_reserves": self.asset1_reserves, + "asset2_reserves": self.asset2_reserves, + "issued_liquidity": self.issued_liquidity, + "unclaimed_protocol_fees": self.unclaimed_protocol_fees, + "outstanding_asset1_amount": self.outstanding_asset1_amount, + "outstanding_asset2_amount": self.outstanding_asset2_amount, + "outstanding_liquidity_asset_amount": self.outstanding_liquidity_asset_amount, + "last_refreshed_round": self.last_refreshed_round, } return pool @@ -240,12 +283,14 @@ def convert(self, amount: AssetAmount): elif amount.asset == self.asset2: 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): + def fetch_mint_quote( + self, amount_a: AssetAmount, amount_b: AssetAmount = None, slippage=0.05 + ): 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 not self.exists: - raise Exception('Pool has not been bootstrapped yet!') + raise Exception("Pool has not been bootstrapped yet!") if self.issued_liquidity: if amount1 is None: amount1 = self.convert(amount2) @@ -260,7 +305,7 @@ def fetch_mint_quote(self, amount_a: AssetAmount, amount_b: AssetAmount = None, else: # first mint if not amount1 or not amount2: - raise Exception('Amounts required for both assets for first mint!') + raise Exception("Amounts required for both assets for first mint!") liquidity_asset_amount = math.sqrt(amount1.amount * amount2.amount) - 1000 # don't apply slippage tolerance to first mint slippage = 0 @@ -270,7 +315,9 @@ def fetch_mint_quote(self, amount_a: AssetAmount, amount_b: AssetAmount = None, self.asset1: amount1, self.asset2: amount2, }, - liquidity_asset_amount=AssetAmount(self.liquidity_asset, liquidity_asset_amount), + liquidity_asset_amount=AssetAmount( + self.liquidity_asset, liquidity_asset_amount + ), slippage=slippage, ) return quote @@ -279,8 +326,12 @@ def fetch_burn_quote(self, liquidity_asset_in, slippage=0.05): if isinstance(liquidity_asset_in, int): liquidity_asset_in = AssetAmount(self.liquidity_asset, liquidity_asset_in) self.refresh() - asset1_amount = (liquidity_asset_in.amount * self.asset1_reserves) / self.issued_liquidity - asset2_amount = (liquidity_asset_in.amount * self.asset2_reserves) / self.issued_liquidity + asset1_amount = ( + liquidity_asset_in.amount * self.asset1_reserves + ) / self.issued_liquidity + asset2_amount = ( + liquidity_asset_in.amount * self.asset2_reserves + ) / self.issued_liquidity quote = BurnQuote( amounts_out={ @@ -292,7 +343,9 @@ 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) -> SwapQuote: + def fetch_fixed_input_swap_quote( + self, amount_in: AssetAmount, slippage=0.05 + ) -> SwapQuote: asset_in, asset_in_amount = amount_in.asset, amount_in.amount self.refresh() if asset_in == self.asset1: @@ -305,7 +358,7 @@ def fetch_fixed_input_swap_quote(self, amount_in: AssetAmount, slippage=0.05) -> output_supply = self.asset1_reserves if not input_supply or not output_supply: - raise Exception('Pool has no liquidity!') + raise Exception("Pool has no liquidity!") # k = input_supply * output_supply # ignoring fees, k must remain constant @@ -313,7 +366,9 @@ def fetch_fixed_input_swap_quote(self, amount_in: AssetAmount, slippage=0.05) -> k = input_supply * output_supply asset_in_amount_minus_fee = (asset_in_amount * 997) / 1000 swap_fees = asset_in_amount - asset_in_amount_minus_fee - asset_out_amount = output_supply - (k / (input_supply + asset_in_amount_minus_fee)) + asset_out_amount = output_supply - ( + k / (input_supply + asset_in_amount_minus_fee) + ) amount_out = AssetAmount(asset_out, int(asset_out_amount)) @@ -322,16 +377,18 @@ def fetch_fixed_input_swap_quote(self, amount_in: AssetAmount, slippage=0.05) -> price_impact = abs(round((swap_price / pool_price) - 1, 5)) quote = SwapQuote( - swap_type='fixed-input', + swap_type="fixed-input", amount_in=amount_in, amount_out=amount_out, swap_fees=AssetAmount(amount_in.asset, int(swap_fees)), slippage=slippage, - price_impact=price_impact + price_impact=price_impact, ) return quote - def fetch_fixed_output_swap_quote(self, amount_out: AssetAmount, slippage=0.05) -> SwapQuote: + def fetch_fixed_output_swap_quote( + self, amount_out: AssetAmount, slippage=0.05 + ) -> SwapQuote: asset_out, asset_out_amount = amount_out.asset, amount_out.amount self.refresh() if asset_out == self.asset1: @@ -348,7 +405,9 @@ def fetch_fixed_output_swap_quote(self, amount_out: AssetAmount, slippage=0.05) # (input_supply + asset_in) * (output_supply - amount_out) = k k = input_supply * output_supply - calculated_amount_in_without_fee = (k / (output_supply - asset_out_amount)) - input_supply + calculated_amount_in_without_fee = ( + k / (output_supply - asset_out_amount) + ) - input_supply asset_in_amount = calculated_amount_in_without_fee * 1000 / 997 swap_fees = asset_in_amount - calculated_amount_in_without_fee @@ -359,17 +418,23 @@ def fetch_fixed_output_swap_quote(self, amount_out: AssetAmount, slippage=0.05) price_impact = abs(round((swap_price / pool_price) - 1, 5)) quote = SwapQuote( - swap_type='fixed-output', + swap_type="fixed-output", amount_out=amount_out, amount_in=amount_in, swap_fees=AssetAmount(amount_in.asset, int(swap_fees)), slippage=slippage, - price_impact=price_impact + price_impact=price_impact, ) return quote - def prepare_swap_transactions(self, amount_in: AssetAmount, amount_out: AssetAmount, swap_type, swapper_address=None): + def prepare_swap_transactions( + self, + amount_in: AssetAmount, + amount_out: AssetAmount, + swap_type, + swapper_address=None, + ): swapper_address = swapper_address or self.client.user_address suggested_params = self.client.algod.suggested_params() txn_group = prepare_swap_transactions( @@ -386,7 +451,9 @@ def prepare_swap_transactions(self, amount_in: AssetAmount, amount_out: AssetAmo ) return txn_group - def prepare_swap_transactions_from_quote(self, quote: SwapQuote, swapper_address=None): + def prepare_swap_transactions_from_quote( + self, quote: SwapQuote, swapper_address=None + ): return self.prepare_swap_transactions( amount_in=quote.amount_in_with_slippage, amount_out=quote.amount_out_with_slippage, @@ -408,7 +475,12 @@ def prepare_bootstrap_transactions(self, pooler_address=None): ) return txn_group - def prepare_mint_transactions(self, amounts_in: "dict[Asset, AssetAmount]", liquidity_asset_amount: AssetAmount, pooler_address=None): + def prepare_mint_transactions( + self, + amounts_in: "dict[Asset, AssetAmount]", + liquidity_asset_amount: AssetAmount, + pooler_address=None, + ): pooler_address = pooler_address or self.client.user_address asset1_amount = amounts_in[self.asset1] asset2_amount = amounts_in[self.asset2] @@ -426,16 +498,22 @@ def prepare_mint_transactions(self, amounts_in: "dict[Asset, AssetAmount]", liqu ) return txn_group - def prepare_mint_transactions_from_quote(self, quote: MintQuote, pooler_address=None): + def prepare_mint_transactions_from_quote( + self, quote: MintQuote, pooler_address=None + ): return self.prepare_mint_transactions( amounts_in=quote.amounts_in, liquidity_asset_amount=quote.liquidity_asset_amount_with_slippage, pooler_address=pooler_address, ) - def prepare_burn_transactions(self, liquidity_asset_amount: AssetAmount, amounts_out, pooler_address=None): + def prepare_burn_transactions( + self, liquidity_asset_amount: AssetAmount, amounts_out, pooler_address=None + ): if isinstance(liquidity_asset_amount, int): - liquidity_asset_amount = AssetAmount(self.liquidity_asset, liquidity_asset_amount) + liquidity_asset_amount = AssetAmount( + self.liquidity_asset, liquidity_asset_amount + ) pooler_address = pooler_address or self.client.user_address asset1_amount = amounts_out[self.asset1] asset2_amount = amounts_out[self.asset2] @@ -453,7 +531,9 @@ def prepare_burn_transactions(self, liquidity_asset_amount: AssetAmount, amounts ) return txn_group - def prepare_burn_transactions_from_quote(self, quote: BurnQuote, pooler_address=None): + def prepare_burn_transactions_from_quote( + self, quote: BurnQuote, pooler_address=None + ): return self.prepare_burn_transactions( liquidity_asset_amount=quote.liquidity_asset_amount, amounts_out=quote.amounts_out_with_slippage, @@ -514,39 +594,46 @@ def get_minimum_balance(self): total_byteslices = 0 total = ( - MIN_BALANCE_PER_ACCOUNT - + (MIN_BALANCE_PER_ASSET * num_assets) - + (MIN_BALANCE_PER_APP * (num_created_apps + num_local_apps)) - + (MIN_BALANCE_PER_APP_UINT * total_uints) - + (MIN_BALANCE_PER_APP_BYTESLICE * total_byteslices) + MIN_BALANCE_PER_ACCOUNT + + (MIN_BALANCE_PER_ASSET * num_assets) + + (MIN_BALANCE_PER_APP * (num_created_apps + num_local_apps)) + + (MIN_BALANCE_PER_APP_UINT * total_uints) + + (MIN_BALANCE_PER_APP_BYTESLICE * total_byteslices) ) return total def fetch_excess_amounts(self, user_address=None): user_address = user_address or self.client.user_address - pool_excess = self.client.fetch_excess_amounts(user_address).get(self.address, {}) + pool_excess = self.client.fetch_excess_amounts(user_address).get( + self.address, {} + ) return pool_excess def fetch_pool_position(self, pooler_address=None): pooler_address = pooler_address or self.client.user_address account_info = self.client.algod.account_info(pooler_address) - assets = {a['asset-id']: a for a in account_info['assets']} - liquidity_asset_amount = assets.get(self.liquidity_asset.id, {}).get('amount', 0) + assets = {a["asset-id"]: a for a in account_info["assets"]} + liquidity_asset_amount = assets.get(self.liquidity_asset.id, {}).get( + "amount", 0 + ) quote = self.fetch_burn_quote(liquidity_asset_amount) return { self.asset1: quote.amounts_out[self.asset1], self.asset2: quote.amounts_out[self.asset2], self.liquidity_asset: quote.liquidity_asset_amount, - 'share': (liquidity_asset_amount / self.issued_liquidity), + "share": (liquidity_asset_amount / self.issued_liquidity), } def fetch_state(self, key=None): account_info = self.client.algod.account_info(self.address) try: - _ = account_info['apps-local-state'][0]['id'] + _ = account_info["apps-local-state"][0]["id"] except IndexError: return {} - validator_app_state = {x['key']: x['value'] for x in account_info['apps-local-state'][0]['key-value']} + validator_app_state = { + x["key"]: x["value"] + for x in account_info["apps-local-state"][0]["key-value"] + } if key: return get_state_int(validator_app_state, key) diff --git a/tinyman/v1/redeem.py b/tinyman/v1/redeem.py index 7fd8ec8..b069570 100644 --- a/tinyman/v1/redeem.py +++ b/tinyman/v1/redeem.py @@ -4,7 +4,16 @@ from .contracts import get_pool_logicsig -def prepare_redeem_transactions(validator_app_id, asset1_id, asset2_id, liquidity_asset_id, asset_id, asset_amount, sender, suggested_params): +def prepare_redeem_transactions( + validator_app_id, + asset1_id, + asset2_id, + liquidity_asset_id, + asset_id, + asset_amount, + sender, + suggested_params, +): pool_logicsig = get_pool_logicsig(validator_app_id, asset1_id, asset2_id) pool_address = pool_logicsig.address() @@ -14,15 +23,17 @@ def prepare_redeem_transactions(validator_app_id, asset1_id, asset2_id, liquidit sp=suggested_params, receiver=pool_address, amt=2000, - note='fee', + note="fee", ), ApplicationNoOpTxn( sender=pool_address, sp=suggested_params, index=validator_app_id, - app_args=['redeem'], + app_args=["redeem"], accounts=[sender], - foreign_assets=[asset1_id, liquidity_asset_id] if asset2_id == 0 else [asset1_id, asset2_id, liquidity_asset_id], + foreign_assets=[asset1_id, liquidity_asset_id] + if asset2_id == 0 + else [asset1_id, asset2_id, liquidity_asset_id], ), AssetTransferTxn( sender=pool_address, @@ -30,7 +41,9 @@ def prepare_redeem_transactions(validator_app_id, asset1_id, asset2_id, liquidit receiver=sender, amt=int(asset_amount), index=asset_id, - ) if asset_id != 0 else PaymentTxn( + ) + if asset_id != 0 + else PaymentTxn( sender=pool_address, sp=suggested_params, receiver=sender, diff --git a/tinyman/v1/staking/__init__.py b/tinyman/v1/staking/__init__.py index def26cf..0292a3e 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/v1/staking/__init__.py @@ -7,22 +7,40 @@ from algosdk.constants import PAYMENT_TXN, ASSETTRANSFER_TXN from algosdk.encoding import is_valid_address -from algosdk.future.transaction import ApplicationClearStateTxn, ApplicationCreateTxn, ApplicationOptInTxn, OnComplete, PaymentTxn, StateSchema, ApplicationUpdateTxn, ApplicationNoOpTxn, AssetTransferTxn - -from tinyman.utils import TransactionGroup, apply_delta, bytes_to_int_list, int_list_to_bytes, int_to_bytes, timestamp_to_date_str +from algosdk.future.transaction import ( + ApplicationClearStateTxn, + ApplicationCreateTxn, + ApplicationOptInTxn, + OnComplete, + PaymentTxn, + StateSchema, + ApplicationUpdateTxn, + ApplicationNoOpTxn, + AssetTransferTxn, +) + +from tinyman.utils import ( + TransactionGroup, + apply_delta, + bytes_to_int_list, + int_list_to_bytes, + int_to_bytes, + timestamp_to_date_str, +) from tinyman.v1.staking.constants import DATE_FORMAT def prepare_create_transaction(args, sender, suggested_params): from .contracts import staking_app_def + txn = ApplicationCreateTxn( sender=sender, sp=suggested_params, on_complete=OnComplete.NoOpOC.real, - approval_program=b64decode(staking_app_def['approval_program']['bytecode']), - clear_program=b64decode(staking_app_def['clear_program']['bytecode']), - global_schema=StateSchema(**staking_app_def['global_state_schema']), - local_schema=StateSchema(**staking_app_def['local_state_schema']), + approval_program=b64decode(staking_app_def["approval_program"]["bytecode"]), + clear_program=b64decode(staking_app_def["clear_program"]["bytecode"]), + global_schema=StateSchema(**staking_app_def["global_state_schema"]), + local_schema=StateSchema(**staking_app_def["local_state_schema"]), app_args=args, ) return TransactionGroup([txn]) @@ -30,25 +48,38 @@ def prepare_create_transaction(args, sender, suggested_params): def prepare_update_transaction(app_id: int, sender, suggested_params): from .contracts import staking_app_def + txn = ApplicationUpdateTxn( index=app_id, sender=sender, sp=suggested_params, - approval_program=b64decode(staking_app_def['approval_program']['bytecode']), - clear_program=b64decode(staking_app_def['clear_program']['bytecode']), + approval_program=b64decode(staking_app_def["approval_program"]["bytecode"]), + clear_program=b64decode(staking_app_def["clear_program"]["bytecode"]), ) return TransactionGroup([txn]) -def prepare_commit_transaction(app_id: int, program_id: int, program_account: str, pool_asset_id: int, amount: int, sender: str, suggested_params, required_asset_id: Optional[int] = None): +def prepare_commit_transaction( + app_id: int, + program_id: int, + program_account: str, + pool_asset_id: int, + amount: int, + sender: str, + suggested_params, + required_asset_id: Optional[int] = None, +): commitment_txn = ApplicationNoOpTxn( index=app_id, sender=sender, sp=suggested_params, - app_args=['commit', int_to_bytes(amount)], + app_args=["commit", int_to_bytes(amount)], foreign_assets=[pool_asset_id], accounts=[program_account], - note=b'tinymanStaking/v1:b' + int_to_bytes(program_id) + int_to_bytes(pool_asset_id) + int_to_bytes(amount) + note=b"tinymanStaking/v1:b" + + int_to_bytes(program_id) + + int_to_bytes(pool_asset_id) + + int_to_bytes(amount), ) transactions = [commitment_txn] @@ -57,7 +88,7 @@ def prepare_commit_transaction(app_id: int, program_id: int, program_account: st index=app_id, sender=sender, sp=suggested_params, - app_args=['log_balance'], + app_args=["log_balance"], foreign_assets=[required_asset_id], ) transactions.append(nft_log_balance_txn) @@ -66,23 +97,27 @@ def prepare_commit_transaction(app_id: int, program_id: int, program_account: st def parse_commit_transaction(txn, app_id: int): - if txn.get('application-transaction'): - app_call = txn['application-transaction'] - if app_call['on-completion'] != 'noop': + if txn.get("application-transaction"): + app_call = txn["application-transaction"] + if app_call["on-completion"] != "noop": return - if app_call['application-id'] != app_id: + if app_call["application-id"] != app_id: return - if app_call['application-args'][0] == b64encode(b'commit').decode(): + if app_call["application-args"][0] == b64encode(b"commit").decode(): result = {} try: - note = txn['note'] - result['pooler'] = txn['sender'] - result['program_address'] = app_call['accounts'][0] - result['pool_asset_id'] = app_call['foreign-assets'][0] - result['program_id'] = int.from_bytes(b64decode(note)[19:19 + 8], 'big') - result['amount'] = int.from_bytes(b64decode(app_call['application-args'][1]), 'big') - result['balance'] = int.from_bytes(b64decode(txn['logs'][0])[8:], 'big') - result['round'] = txn['confirmed-round'] + note = txn["note"] + result["pooler"] = txn["sender"] + result["program_address"] = app_call["accounts"][0] + result["pool_asset_id"] = app_call["foreign-assets"][0] + result["program_id"] = int.from_bytes( + b64decode(note)[19 : 19 + 8], "big" + ) + result["amount"] = int.from_bytes( + b64decode(app_call["application-args"][1]), "big" + ) + result["balance"] = int.from_bytes(b64decode(txn["logs"][0])[8:], "big") + result["round"] = txn["confirmed-round"] return result except Exception: return @@ -90,19 +125,19 @@ def parse_commit_transaction(txn, app_id: int): def parse_log_balance_transaction(txn, app_id: int): - if txn.get('application-transaction'): - app_call = txn['application-transaction'] - if app_call['on-completion'] != 'noop': + if txn.get("application-transaction"): + app_call = txn["application-transaction"] + if app_call["on-completion"] != "noop": return - if app_call['application-id'] != app_id: + if app_call["application-id"] != app_id: return - if app_call['application-args'][0] == b64encode(b'log_balance').decode(): + if app_call["application-args"][0] == b64encode(b"log_balance").decode(): result = {} try: - result['pooler'] = txn['sender'] - result['asset_id'] = app_call['foreign-assets'][0] - result['balance'] = int.from_bytes(b64decode(txn['logs'][0])[8:], 'big') - result['round'] = txn['confirmed-round'] + result["pooler"] = txn["sender"] + result["asset_id"] = app_call["foreign-assets"][0] + result["balance"] = int.from_bytes(b64decode(txn["logs"][0])[8:], "big") + result["round"] = txn["confirmed-round"] return result except Exception: return @@ -110,30 +145,30 @@ def parse_log_balance_transaction(txn, app_id: int): def parse_program_config_transaction(txn, app_id: int): - if txn.get('application-transaction'): - app_call = txn['application-transaction'] - if app_call['application-id'] != app_id: + if txn.get("application-transaction"): + app_call = txn["application-transaction"] + if app_call["application-id"] != app_id: return - if app_call['on-completion'] == 'clear': - return ('clear', None) - arg1 = b64decode(app_call['application-args'][0]).decode() - local_delta = txn['local-state-delta'][0]['delta'] + if app_call["on-completion"] == "clear": + return ("clear", None) + arg1 = b64decode(app_call["application-args"][0]).decode() + local_delta = txn["local-state-delta"][0]["delta"] return (arg1, local_delta) return def parse_program_update_transaction(txn, app_id: int): - if txn.get('application-transaction'): - app_call = txn['application-transaction'] - if app_call['on-completion'] != 'noop': + if txn.get("application-transaction"): + app_call = txn["application-transaction"] + if app_call["on-completion"] != "noop": return - if app_call['application-id'] != app_id: + if app_call["application-id"] != app_id: return - if app_call['application-args'][0] == b64encode(b'update').decode(): + if app_call["application-args"][0] == b64encode(b"update").decode(): try: - local_delta = txn['local-state-delta'][0]['delta'] + local_delta = txn["local-state-delta"][0]["delta"] state = apply_delta({}, local_delta) - result = parse_program_state(txn['sender'], state) + result = parse_program_state(txn["sender"], state) return result except Exception: return @@ -142,38 +177,55 @@ def parse_program_update_transaction(txn, app_id: int): def parse_program_state(address, state): result = {} - result['address'] = address - result['id'] = state[b'id'] - result['url'] = state[b'url'] - result['reward_asset_id'] = state[b'reward_asset_id'] - result['reward_period'] = state[b'reward_period'] - result['start_date'] = timestamp_to_date_str(state[b'start_time']) - result['end_date'] = timestamp_to_date_str(state[b'end_time']) - result['pools'] = [] - asset_ids = bytes_to_int_list(state[b'assets']) - result['asset_ids'] = asset_ids - mins = bytes_to_int_list(state[b'mins']) - result['mins'] = mins + result["address"] = address + result["id"] = state[b"id"] + result["url"] = state[b"url"] + result["reward_asset_id"] = state[b"reward_asset_id"] + result["reward_period"] = state[b"reward_period"] + result["start_date"] = timestamp_to_date_str(state[b"start_time"]) + result["end_date"] = timestamp_to_date_str(state[b"end_time"]) + result["pools"] = [] + asset_ids = bytes_to_int_list(state[b"assets"]) + result["asset_ids"] = asset_ids + mins = bytes_to_int_list(state[b"mins"]) + result["mins"] = mins empty_rewards_bytes = int_list_to_bytes([0] * 15) rewards = [] for i in range(1, 8): - r = bytes_to_int_list(state.get(f'r{i}'.encode(), empty_rewards_bytes)) + r = bytes_to_int_list(state.get(f"r{i}".encode(), empty_rewards_bytes)) start = r[0] amounts = r[1:] if start: - rewards.append({'start_date': timestamp_to_date_str(start), 'amounts': amounts}) - result['reward_amounts_dict'] = rewards + rewards.append( + {"start_date": timestamp_to_date_str(start), "amounts": amounts} + ) + result["reward_amounts_dict"] = rewards for i in range(len(asset_ids)): if asset_ids[i] > 0: - result['pools'].append({ - 'asset_id': asset_ids[i], - 'min_amount': mins[i], - 'reward_amounts': {x['start_date']: x['amounts'][i] for x in rewards}, - }) + result["pools"].append( + { + "asset_id": asset_ids[i], + "min_amount": mins[i], + "reward_amounts": { + x["start_date"]: x["amounts"][i] for x in rewards + }, + } + ) return result -def prepare_setup_transaction(app_id: int, url: str, reward_asset_id: int, reward_period: int, start_time: int, end_time: int, asset_ids: List[int], min_amounts: List[int], sender, suggested_params): +def prepare_setup_transaction( + app_id: int, + url: str, + reward_asset_id: int, + reward_period: int, + start_time: int, + end_time: int, + asset_ids: List[int], + min_amounts: List[int], + sender, + suggested_params, +): assets = [0] * 14 mins = [0] * 14 for i in range(len(asset_ids)): @@ -185,7 +237,7 @@ def prepare_setup_transaction(app_id: int, url: str, reward_asset_id: int, rewar sp=suggested_params, # setup, url, reward_asset_id, reward_period, start_time, end_time, int[14]{asset_id_1, asset_id_2, ...} app_args=[ - 'setup', + "setup", url, int_to_bytes(reward_asset_id), int_to_bytes(reward_period), @@ -200,11 +252,15 @@ def prepare_setup_transaction(app_id: int, url: str, reward_asset_id: int, rewar def prepare_clear_state_transaction(app_id, sender, suggested_params): - clear_txn = ApplicationClearStateTxn(index=app_id, sender=sender, sp=suggested_params) + clear_txn = ApplicationClearStateTxn( + index=app_id, sender=sender, sp=suggested_params + ) return TransactionGroup([clear_txn]) -def prepare_update_rewards_transaction(app_id: int, reward_amounts_dict: dict, sender, suggested_params): +def prepare_update_rewards_transaction( + app_id: int, reward_amounts_dict: dict, sender, suggested_params +): r = [ [0] * 15, [0] * 15, @@ -224,7 +280,7 @@ def prepare_update_rewards_transaction(app_id: int, reward_amounts_dict: dict, s sp=suggested_params, # ("update_rewards", int[15]{rewards_first_valid_time, rewards_asset_1, rewards_asset_2, ...}, int[15]{rewards_first_valid_time, rewards_asset_1, rewards_asset_2, ...}, ...) app_args=[ - 'update_rewards', + "update_rewards", int_list_to_bytes(r[0]), int_list_to_bytes(r[1]), int_list_to_bytes(r[2]), @@ -235,25 +291,36 @@ def prepare_update_rewards_transaction(app_id: int, reward_amounts_dict: dict, s return TransactionGroup([txn]) -def prepare_end_program_transaction(app_id: int, end_time: int, sender, suggested_params): +def prepare_end_program_transaction( + app_id: int, end_time: int, sender, suggested_params +): txn = ApplicationNoOpTxn( index=app_id, sender=sender, sp=suggested_params, app_args=[ - 'end_program', + "end_program", int_to_bytes(end_time), ], ) return TransactionGroup([txn]) -def prepare_payment_transaction(staker_address: str, reward_asset_id: int, amount: int, metadata: dict, sender, suggested_params): +def prepare_payment_transaction( + staker_address: str, + reward_asset_id: int, + amount: int, + metadata: dict, + sender, + suggested_params, +): note = generate_note_from_metadata(metadata) # Compose a lease key from the distribution key (date, pool_address) and staker_address # This is to prevent accidently submitting multiple payments for the same staker for the same cycles # Note: the lease is only ensured unique between first_valid & last_valid - lease_data = json.dumps([metadata['rewards']['distribution'], staker_address]).encode() + lease_data = json.dumps( + [metadata["rewards"]["distribution"], staker_address] + ).encode() lease = sha256(lease_data).digest() if reward_asset_id == 0: txn = PaymentTxn( @@ -278,7 +345,15 @@ def prepare_payment_transaction(staker_address: str, reward_asset_id: int, amoun return txn -def prepare_reward_metadata_for_payment(distribution_date: str, program_id: int, pool_address: str, pool_asset_id: int, pool_name: str, first_cycle: str, last_cycle: str): +def prepare_reward_metadata_for_payment( + distribution_date: str, + program_id: int, + pool_address: str, + pool_asset_id: int, + pool_name: str, + first_cycle: str, + last_cycle: str, +): data = { "rewards": { "distribution": f"{pool_asset_id}_{program_id}_{distribution_date}", @@ -295,12 +370,20 @@ def prepare_reward_metadata_for_payment(distribution_date: str, program_id: int, def generate_note_from_metadata(metadata): - note = b'tinymanStaking/v2:j' + json.dumps(metadata, sort_keys=True).encode() + note = b"tinymanStaking/v2:j" + json.dumps(metadata, sort_keys=True).encode() return note def get_note_prefix_for_distribution(distribution_date, pool_address): - metadata = prepare_reward_metadata_for_payment(distribution_date, program_id=None, pool_address=pool_address, pool_asset_id=None, pool_name=None, first_cycle=None, last_cycle=None) + metadata = prepare_reward_metadata_for_payment( + distribution_date, + program_id=None, + pool_address=pool_address, + pool_asset_id=None, + pool_name=None, + first_cycle=None, + last_cycle=None, + ) note = generate_note_from_metadata(metadata) prefix = note.split(b', "distribution_date"')[0] return prefix @@ -351,7 +434,7 @@ def parse_reward_payment_transaction(txn): else: return - note = b64decode(txn['note']) + note = b64decode(txn["note"]) try: note_version = get_note_version(note) except ValueError: @@ -393,8 +476,16 @@ def parse_reward_payment_transaction(txn): ) -def _parse_reward_payment_transaction_v1(*, payment_data, txn, reward_asset_id, transfer_amount, staker_address): - if not {"distribution", "pool_address", "pool_name", "pool_asset_id", "rewards"} <= set(payment_data): +def _parse_reward_payment_transaction_v1( + *, payment_data, txn, reward_asset_id, transfer_amount, staker_address +): + if not { + "distribution", + "pool_address", + "pool_name", + "pool_asset_id", + "rewards", + } <= set(payment_data): return if not isinstance(payment_data["rewards"], list): @@ -423,10 +514,12 @@ def _parse_reward_payment_transaction_v1(*, payment_data, txn, reward_asset_id, rewards = [] try: for cycle, reward_amount in payment_data["rewards"]: - rewards.append({ - "cycle": datetime.strptime(cycle, DATE_FORMAT).date(), - "amount": int(reward_amount) - }) + rewards.append( + { + "cycle": datetime.strptime(cycle, DATE_FORMAT).date(), + "amount": int(reward_amount), + } + ) except ValueError: return @@ -451,12 +544,25 @@ def _parse_reward_payment_transaction_v1(*, payment_data, txn, reward_asset_id, return result -def _parse_reward_payment_transaction_v2(*, payment_data, txn, reward_asset_id, transfer_amount, staker_address): - if not {"distribution", "pool_address", "pool_name", "pool_asset_id", "program_id", "distribution_date", "first_cycle", "last_cycle"} <= set(payment_data): +def _parse_reward_payment_transaction_v2( + *, payment_data, txn, reward_asset_id, transfer_amount, staker_address +): + if not { + "distribution", + "pool_address", + "pool_name", + "pool_asset_id", + "program_id", + "distribution_date", + "first_cycle", + "last_cycle", + } <= set(payment_data): return try: - pool_asset_id, program_id, distribution_date = payment_data["distribution"].split("_") + pool_asset_id, program_id, distribution_date = payment_data[ + "distribution" + ].split("_") pool_asset_id = int(pool_asset_id) program_id = int(program_id) except ValueError: diff --git a/tinyman/v1/staking/contracts.py b/tinyman/v1/staking/contracts.py index d34c6a6..4814d03 100644 --- a/tinyman/v1/staking/contracts.py +++ b/tinyman/v1/staking/contracts.py @@ -3,6 +3,6 @@ import tinyman.v1.staking -_contracts = json.loads(importlib.resources.read_text(tinyman.v1.staking, 'asc.json')) +_contracts = json.loads(importlib.resources.read_text(tinyman.v1.staking, "asc.json")) -staking_app_def = _contracts['contracts']['staking_app'] +staking_app_def = _contracts["contracts"]["staking_app"] diff --git a/tinyman/v1/swap.py b/tinyman/v1/swap.py index 7594e90..3e85cc4 100644 --- a/tinyman/v1/swap.py +++ b/tinyman/v1/swap.py @@ -4,13 +4,24 @@ from .contracts import get_pool_logicsig -def prepare_swap_transactions(validator_app_id, asset1_id, asset2_id, liquidity_asset_id, asset_in_id, asset_in_amount, asset_out_amount, swap_type, sender, suggested_params): +def prepare_swap_transactions( + validator_app_id, + asset1_id, + asset2_id, + liquidity_asset_id, + asset_in_id, + asset_in_amount, + asset_out_amount, + swap_type, + sender, + suggested_params, +): pool_logicsig = get_pool_logicsig(validator_app_id, asset1_id, asset2_id) pool_address = pool_logicsig.address() swap_types = { - 'fixed-input': 'fi', - 'fixed-output': 'fo', + "fixed-input": "fi", + "fixed-output": "fo", } asset_out_id = asset2_id if asset_in_id == asset1_id else asset1_id @@ -21,15 +32,17 @@ def prepare_swap_transactions(validator_app_id, asset1_id, asset2_id, liquidity_ sp=suggested_params, receiver=pool_address, amt=2000, - note='fee', + note="fee", ), ApplicationNoOpTxn( sender=pool_address, sp=suggested_params, index=validator_app_id, - app_args=['swap', swap_types[swap_type]], + app_args=["swap", swap_types[swap_type]], accounts=[sender], - foreign_assets=[asset1_id, liquidity_asset_id] if asset2_id == 0 else [asset1_id, asset2_id, liquidity_asset_id], + foreign_assets=[asset1_id, liquidity_asset_id] + if asset2_id == 0 + else [asset1_id, asset2_id, liquidity_asset_id], ), AssetTransferTxn( sender=sender, @@ -37,7 +50,9 @@ def prepare_swap_transactions(validator_app_id, asset1_id, asset2_id, liquidity_ receiver=pool_address, amt=int(asset_in_amount), index=asset_in_id, - ) if asset_in_id != 0 else PaymentTxn( + ) + if asset_in_id != 0 + else PaymentTxn( sender=sender, sp=suggested_params, receiver=pool_address, @@ -49,7 +64,9 @@ def prepare_swap_transactions(validator_app_id, asset1_id, asset2_id, liquidity_ receiver=sender, amt=int(asset_out_amount), index=asset_out_id, - ) if asset_out_id != 0 else PaymentTxn( + ) + if asset_out_id != 0 + else PaymentTxn( sender=pool_address, sp=suggested_params, receiver=sender, From f8b3e1aaedceb72c06973aa196622d42853c8206 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Mon, 26 Sep 2022 15:07:39 +0300 Subject: [PATCH 31/73] remove deprecated wait_for_confirmation --- tinyman/utils.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/tinyman/utils.py b/tinyman/utils.py index ba3b483..50d00a5 100644 --- a/tinyman/utils.py +++ b/tinyman/utils.py @@ -1,15 +1,12 @@ from base64 import b64decode, b64encode -import warnings from datetime import datetime from algosdk.future.transaction import ( LogicSigTransaction, assign_group_id, - wait_for_confirmation as wait_for_confirmation_algosdk, + wait_for_confirmation, ) from algosdk.error import AlgodHTTPError -warnings.simplefilter("always", DeprecationWarning) - def get_program(definition, variables=None): """ @@ -60,22 +57,7 @@ def sign_and_submit_transactions( signed_transactions[i] = txn.sign(sender_sk) txid = client.send_transactions(signed_transactions) - txinfo = wait_for_confirmation_algosdk(client, txid) - txinfo["txid"] = txid - return txinfo - - -def wait_for_confirmation(client, txid): - """ - Deprecated. - Use algosdk if you are importing wait_for_confirmation individually. - """ - warnings.warn( - "tinyman.utils.wait_for_confirmation is deprecated. Use algosdk.future.transaction.wait_for_confirmation instead if you are importing individually.", - DeprecationWarning, - stacklevel=2, - ) - txinfo = wait_for_confirmation_algosdk(client, txid) + txinfo = wait_for_confirmation(client, txid) txinfo["txid"] = txid return txinfo @@ -174,7 +156,7 @@ def submit(self, algod, wait=False): except AlgodHTTPError as e: raise Exception(str(e)) if wait: - txinfo = wait_for_confirmation_algosdk(algod, txid) + txinfo = wait_for_confirmation(algod, txid) txinfo["txid"] = txid return txinfo return {"txid": txid} From c7f4b297918dba31b1f798cfb910e5d0f6992207 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 29 Sep 2022 12:28:40 +0300 Subject: [PATCH 32/73] v2 wip --- .gitignore | 6 + README.md | 10 +- examples/__init__.py | 0 examples/staking/commitments.py | 2 +- examples/staking/make_commitment.py | 2 +- examples/{ => v1}/add_liquidity1.py | 0 examples/{ => v1}/pooling1.py | 0 examples/{ => v1}/swapping1.py | 0 .../{ => v1}/swapping1_less_convenience.py | 0 examples/v2/__init__.py | 0 examples/v2/tutorial/01_generate_account.py | 35 + examples/v2/tutorial/02_create_assets.py | 41 + examples/v2/tutorial/03_bootstrap_pool.py | 32 + .../v2/tutorial/04_add_initial_liquidity.py | 61 ++ .../v2/tutorial/05_add_flexible_liquidity.py | 71 ++ .../v2/tutorial/06_add_single_liquidity.py | 71 ++ examples/v2/tutorial/07_remove_liquidity.py | 54 ++ .../08_single_asset_remove_liquidity.py | 57 ++ examples/v2/tutorial/09_fixed_input_swap.py | 48 ++ examples/v2/tutorial/10_fixed_output_swap.py | 48 ++ examples/v2/tutorial/__init__.py | 0 examples/v2/tutorial/common.py | 75 ++ tests/__init__.py | 8 + tests/v2/__init__.py | 0 tests/v2/test_bootstrap.py | 84 +++ tinyman/client.py | 66 ++ tinyman/{v1 => }/optin.py | 0 tinyman/{v1 => }/staking/__init__.py | 2 +- tinyman/{v1 => }/staking/asc.json | 0 tinyman/staking/constants.py | 4 + tinyman/{v1 => }/staking/contracts.py | 2 +- tinyman/utils.py | 50 +- tinyman/v1/client.py | 69 +- tinyman/v1/constants.py | 2 - tinyman/v1/contracts.py | 29 +- tinyman/v1/pools.py | 2 +- tinyman/v1/staking/constants.py | 1 - tinyman/v2/__init__.py | 0 tinyman/v2/add_liquidity.py | 194 +++++ tinyman/v2/bootstrap.py | 53 ++ tinyman/v2/client.py | 38 + tinyman/v2/contracts.py | 26 + tinyman/v2/formulas.py | 191 +++++ tinyman/v2/pools.py | 700 ++++++++++++++++++ tinyman/v2/quotes.py | 103 +++ tinyman/v2/remove_liquidity.py | 108 +++ tinyman/v2/swap.py | 75 ++ tinyman/v2/utils.py | 16 + 48 files changed, 2336 insertions(+), 100 deletions(-) create mode 100644 examples/__init__.py rename examples/{ => v1}/add_liquidity1.py (100%) rename examples/{ => v1}/pooling1.py (100%) rename examples/{ => v1}/swapping1.py (100%) rename examples/{ => v1}/swapping1_less_convenience.py (100%) create mode 100644 examples/v2/__init__.py create mode 100644 examples/v2/tutorial/01_generate_account.py create mode 100644 examples/v2/tutorial/02_create_assets.py create mode 100644 examples/v2/tutorial/03_bootstrap_pool.py create mode 100644 examples/v2/tutorial/04_add_initial_liquidity.py create mode 100644 examples/v2/tutorial/05_add_flexible_liquidity.py create mode 100644 examples/v2/tutorial/06_add_single_liquidity.py create mode 100644 examples/v2/tutorial/07_remove_liquidity.py create mode 100644 examples/v2/tutorial/08_single_asset_remove_liquidity.py create mode 100644 examples/v2/tutorial/09_fixed_input_swap.py create mode 100644 examples/v2/tutorial/10_fixed_output_swap.py create mode 100644 examples/v2/tutorial/__init__.py create mode 100644 examples/v2/tutorial/common.py create mode 100644 tests/__init__.py create mode 100644 tests/v2/__init__.py create mode 100644 tests/v2/test_bootstrap.py create mode 100644 tinyman/client.py rename tinyman/{v1 => }/optin.py (100%) rename tinyman/{v1 => }/staking/__init__.py (99%) rename tinyman/{v1 => }/staking/asc.json (100%) create mode 100644 tinyman/staking/constants.py rename tinyman/{v1 => }/staking/contracts.py (87%) delete mode 100644 tinyman/v1/staking/constants.py create mode 100644 tinyman/v2/__init__.py create mode 100644 tinyman/v2/add_liquidity.py create mode 100644 tinyman/v2/bootstrap.py create mode 100644 tinyman/v2/client.py create mode 100644 tinyman/v2/contracts.py create mode 100644 tinyman/v2/formulas.py create mode 100644 tinyman/v2/pools.py create mode 100644 tinyman/v2/quotes.py create mode 100644 tinyman/v2/remove_liquidity.py create mode 100644 tinyman/v2/swap.py create mode 100644 tinyman/v2/utils.py diff --git a/.gitignore b/.gitignore index 75cb1b3..ad886c2 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,9 @@ dmypy.json # Pyre type checker .pyre/ + +# TODO: Remove +tinyman/v2/asc.json +tinyman/v2/constants.py +account.json +assets.json diff --git a/README.md b/README.md index 2b6645b..aa7505e 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ print(f'TINYUSDC per ALGO (worst case): {quote.price_with_slippage}') ## Examples ### Basic Swapping -[swapping1.py](examples/swapping1.py) +[swapping1.py](examples/v1/swapping1.py) This example demonstrates basic functionality including: * retrieving Pool details * getting a swap quote @@ -58,16 +58,16 @@ This example demonstrates basic functionality including: * checking excess amounts * preparing redeem transactions -[swapping1_less_convenience.py](examples/swapping1_less_convenience.py) -This example has exactly the same functionality as [swapping1.py](examples/swapping1.py) but is purposely more verbose, using less convenience functions. +[swapping1_less_convenience.py](examples/v1/swapping1_less_convenience.py) +This example has exactly the same functionality as [swapping1.py](examples/v1/swapping1.py) but is purposely more verbose, using less convenience functions. ### Basic Pooling -[pooling1.py](examples/pooling1.py) +[pooling1.py](examples/v1/pooling1.py) This example demonstrates retrieving the current pool position/share for an address. ### Basic Add Liquidity (Minting) -[add_liquidity1.py](examples/add_liquidity1.py) +[add_liquidity1.py](examples/v1/add_liquidity1.py) This example demonstrates add liquidity to an existing pool. ### Basic Burning diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/staking/commitments.py b/examples/staking/commitments.py index fb5691b..ebb0f53 100644 --- a/examples/staking/commitments.py +++ b/examples/staking/commitments.py @@ -1,5 +1,5 @@ import requests -from tinyman.v1.staking import parse_commit_transaction +from tinyman.staking import parse_commit_transaction app_id = 51948952 result = requests.get( diff --git a/examples/staking/make_commitment.py b/examples/staking/make_commitment.py index 2796e43..7d1699a 100644 --- a/examples/staking/make_commitment.py +++ b/examples/staking/make_commitment.py @@ -4,7 +4,7 @@ from tinyman.v1.client import TinymanTestnetClient -from tinyman.v1.staking import prepare_commit_transaction +from tinyman.staking import prepare_commit_transaction # Hardcoding account keys is not a great practice. This is for demonstration purposes only. # See the README & Docs for alternative signing methods. diff --git a/examples/add_liquidity1.py b/examples/v1/add_liquidity1.py similarity index 100% rename from examples/add_liquidity1.py rename to examples/v1/add_liquidity1.py diff --git a/examples/pooling1.py b/examples/v1/pooling1.py similarity index 100% rename from examples/pooling1.py rename to examples/v1/pooling1.py diff --git a/examples/swapping1.py b/examples/v1/swapping1.py similarity index 100% rename from examples/swapping1.py rename to examples/v1/swapping1.py diff --git a/examples/swapping1_less_convenience.py b/examples/v1/swapping1_less_convenience.py similarity index 100% rename from examples/swapping1_less_convenience.py rename to examples/v1/swapping1_less_convenience.py diff --git a/examples/v2/__init__.py b/examples/v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/v2/tutorial/01_generate_account.py b/examples/v2/tutorial/01_generate_account.py new file mode 100644 index 0000000..494ceab --- /dev/null +++ b/examples/v2/tutorial/01_generate_account.py @@ -0,0 +1,35 @@ +import json +import os + +from algosdk.account import generate_account +from algosdk.mnemonic import from_private_key + +from examples.v2.tutorial.common import get_account_file_path + +account_file_path = get_account_file_path() + +try: + size = os.path.getsize(account_file_path) +except FileNotFoundError: + size = 0 +else: + if size > 0: + raise Exception(f"The file({account_file_path}) is not empty") + +private_key, address = generate_account() +mnemonic = from_private_key(private_key) + +account_data = { + "address": address, + "private_key": private_key, + "mnemonic": mnemonic, +} + +with open(account_file_path, "w", encoding="utf-8") as f: + json.dump(account_data, f, ensure_ascii=False, indent=4) + +print(f"Generated Account: {address}") +# Fund the account +print( + f"Go to https://bank.testnet.algorand.network/?account={address} and fund your account." +) diff --git a/examples/v2/tutorial/02_create_assets.py b/examples/v2/tutorial/02_create_assets.py new file mode 100644 index 0000000..de52f8b --- /dev/null +++ b/examples/v2/tutorial/02_create_assets.py @@ -0,0 +1,41 @@ +# This sample is provided for demonstration purposes only. +# It is not intended for production use. +# This example does not constitute trading advice. +import json +import os + +from examples.v2.tutorial.common import ( + get_account, + get_algod, + get_assets_file_path, + create_asset, +) +from tinyman.v2.client import TinymanV2TestnetClient + + +assets_file_path = get_assets_file_path() + +try: + size = os.path.getsize(assets_file_path) +except FileNotFoundError: + size = 0 +else: + if size > 0: + raise Exception(f"The file({assets_file_path}) is not empty") + +account = get_account() +algod = get_algod() +client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) + +ASSET_A_ID = create_asset(algod, account["address"], account["private_key"]) +ASSET_B_ID = create_asset(algod, account["address"], account["private_key"]) + +assets_data = {"ids": [ASSET_A_ID, ASSET_B_ID]} + +with open(assets_file_path, "w", encoding="utf-8") as f: + json.dump(assets_data, f, ensure_ascii=False, indent=4) + +print(f"Generated Assets: {[ASSET_A_ID, ASSET_B_ID]}") +print("View on Algoexplorer:") +print(f"https://testnet.algoexplorer.io/asset/{ASSET_A_ID}") +print(f"https://testnet.algoexplorer.io/asset/{ASSET_B_ID}") diff --git a/examples/v2/tutorial/03_bootstrap_pool.py b/examples/v2/tutorial/03_bootstrap_pool.py new file mode 100644 index 0000000..8290c7b --- /dev/null +++ b/examples/v2/tutorial/03_bootstrap_pool.py @@ -0,0 +1,32 @@ +# This sample is provided for demonstration purposes only. +# It is not intended for production use. +# This example does not constitute trading advice. +from pprint import pprint +from urllib.parse import quote_plus + +from examples.v2.tutorial.common import get_account, get_algod, get_assets +from tinyman.v2.client import TinymanV2TestnetClient + + +account = get_account() +algod = get_algod() +client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) + +ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] + +ASSET_A = client.fetch_asset(ASSET_A_ID) +ASSET_B = client.fetch_asset(ASSET_B_ID) + + +pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) +print(pool) + +txn_group = pool.prepare_bootstrap_transactions() +txn_group.sign_with_private_key(account["address"], account["private_key"]) +txinfo = txn_group.submit(algod, wait=True) +print("Transaction Info") +pprint(txinfo) + +print( + f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" +) diff --git a/examples/v2/tutorial/04_add_initial_liquidity.py b/examples/v2/tutorial/04_add_initial_liquidity.py new file mode 100644 index 0000000..5b0d77e --- /dev/null +++ b/examples/v2/tutorial/04_add_initial_liquidity.py @@ -0,0 +1,61 @@ +# This sample is provided for demonstration purposes only. +# It is not intended for production use. +# This example does not constitute trading advice. +from pprint import pprint +from urllib.parse import quote_plus + +from tinyman.assets import AssetAmount + +from examples.v2.tutorial.common import get_account, get_algod, get_assets +from tinyman.v2.client import TinymanV2TestnetClient + + +account = get_account() +algod = get_algod() +client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) + +ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] +ASSET_A = client.fetch_asset(ASSET_A_ID) +ASSET_B = client.fetch_asset(ASSET_B_ID) +pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) + +# Bootstrap the pool +txn_group = pool.prepare_bootstrap_transactions() +txn_group.sign_with_private_key(account["address"], account["private_key"]) +txn_group.submit(algod, wait=True) + +# Refresh the pool and get pool token asset id +pool.refresh() + +# Opt-in to the pool token +txn_group_1 = pool.prepare_pool_token_asset_optin_transactions() + +# Add initial liquidity +txn_group_2 = pool.prepare_add_liquidity_transactions( + amounts_in={ + pool.asset_1: AssetAmount(pool.asset_1, 10_000_000), + pool.asset_2: AssetAmount(pool.asset_2, 10_000_000), + }, + min_pool_token_asset_amount=None, +) + +# You can merge the transaction groups +txn_group = txn_group_1 + txn_group_2 + +# Submit +txn_group.sign_with_private_key(account["address"], account["private_key"]) + +txinfo = txn_group.submit(algod, wait=True) +print("Transaction Info") +pprint(txinfo) + +print( + f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" +) + +pool.refresh() +pool_position = pool.fetch_pool_position() +share = pool_position["share"] * 100 +print(f"Pool Tokens: {pool_position[pool.liquidity_asset]}") +print(f"Assets: {pool_position[ASSET_A]}, {pool_position[ASSET_B]}") +print(f"Share of pool: {share:.3f}%") diff --git a/examples/v2/tutorial/05_add_flexible_liquidity.py b/examples/v2/tutorial/05_add_flexible_liquidity.py new file mode 100644 index 0000000..a007f4e --- /dev/null +++ b/examples/v2/tutorial/05_add_flexible_liquidity.py @@ -0,0 +1,71 @@ +# This sample is provided for demonstration purposes only. +# It is not intended for production use. +# This example does not constitute trading advice. +from pprint import pprint +from urllib.parse import quote_plus + +from tinyman.assets import AssetAmount + +from examples.v2.tutorial.common import get_account, get_algod, get_assets +from tinyman.v2.client import TinymanV2TestnetClient + + +account = get_account() +algod = get_algod() +client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) + +ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] +ASSET_A = client.fetch_asset(ASSET_A_ID) +ASSET_B = client.fetch_asset(ASSET_B_ID) +pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) + +assert pool.exists, "Pool has not been bootstrapped yet!" +assert pool.issued_pool_tokens, "Pool has no liquidity" + +# Add flexible liquidity (advanced) +# txn_group = pool.prepare_add_liquidity_transactions( +# amounts_in={ +# pool.asset_1: AssetAmount(pool.asset_1, 5_000_000), +# pool.asset_2: AssetAmount(pool.asset_2, 7_000_000) +# }, +# min_pool_token_asset_amount="Do your own calculation" +# ) + +quote = pool.fetch_add_liquidity_quote( + amount_a=AssetAmount(pool.asset_1, 10_000_000), + amount_b=AssetAmount(pool.asset_2, 5_000_000), + slippage=0, # TODO: 0.05 +) + +print("\nAdd Liquidity Quote:") +print(quote) + +print("\nInternal Swap Quote:") +print(quote.internal_swap_quote) + +txn_group = pool.prepare_add_liquidity_transactions_from_quote(quote=quote) + +if not client.asset_is_opted_in(asset_id=pool.pool_token_asset.id): + # Opt-in to the pool token + opt_in_txn_group = pool.prepare_pool_token_asset_optin_transactions() + # You can merge the transaction groups + txn_group = txn_group + opt_in_txn_group + +# Sign +txn_group.sign_with_private_key(account["address"], account["private_key"]) + +# Submit +txinfo = txn_group.submit(algod, wait=True) +print("Transaction Info") +pprint(txinfo) + +print( + f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" +) + +pool.refresh() +pool_position = pool.fetch_pool_position() +share = pool_position["share"] * 100 +print(f"Pool Tokens: {pool_position[pool.liquidity_asset]}") +print(f"Assets: {pool_position[ASSET_A]}, {pool_position[ASSET_B]}") +print(f"Share of pool: {share:.3f}%") diff --git a/examples/v2/tutorial/06_add_single_liquidity.py b/examples/v2/tutorial/06_add_single_liquidity.py new file mode 100644 index 0000000..611a6ca --- /dev/null +++ b/examples/v2/tutorial/06_add_single_liquidity.py @@ -0,0 +1,71 @@ +# This sample is provided for demonstration purposes only. +# It is not intended for production use. +# This example does not constitute trading advice. +from pprint import pprint +from urllib.parse import quote_plus + +from tinyman.assets import AssetAmount + +from examples.v2.tutorial.common import get_account, get_algod, get_assets +from tinyman.v2.client import TinymanV2TestnetClient + + +account = get_account() +algod = get_algod() +client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) + +ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] +ASSET_A = client.fetch_asset(ASSET_A_ID) +ASSET_B = client.fetch_asset(ASSET_B_ID) +pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) + +assert pool.exists, "Pool has not been bootstrapped yet!" +assert pool.issued_pool_tokens, "Pool has no liquidity" + +# Add flexible liquidity (advanced) +# txn_group = pool.prepare_add_liquidity_transactions( +# amounts_in={ +# pool.asset_1: AssetAmount(pool.asset_1, 5_000_000), +# pool.asset_2: AssetAmount(pool.asset_2, 7_000_000) +# }, +# min_pool_token_asset_amount="Do your own calculation" +# ) + +quote = pool.fetch_add_liquidity_quote( + amount_a=AssetAmount(pool.asset_1, 10_000_000), + slippage=0, # TODO: 0.05 +) + +print("\nAdd Liquidity Quote:") +print(quote) + +print("\nInternal Swap Quote:") +print(quote.internal_swap_quote) + +txn_group = pool.prepare_add_liquidity_transactions_from_quote(quote=quote) + +if not client.asset_is_opted_in(asset_id=pool.pool_token_asset.id): + # Opt-in to the pool token + opt_in_txn_group = pool.prepare_pool_token_asset_optin_transactions() + # You can merge the transaction groups + txn_group = txn_group + opt_in_txn_group + +# Sign +txn_group.sign_with_private_key(account["address"], account["private_key"]) + +# Submit +txinfo = txn_group.submit(algod, wait=True) +print("Transaction Info") +pprint(txinfo) + +print( + f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" +) + +pool.refresh() + +pool_position = pool.fetch_pool_position() +share = pool_position["share"] * 100 +print(f"Pool Tokens: {pool_position[pool.liquidity_asset]}") +print(f"Assets: {pool_position[ASSET_A]}, {pool_position[ASSET_B]}") +print(f"Share of pool: {share:.3f}%") diff --git a/examples/v2/tutorial/07_remove_liquidity.py b/examples/v2/tutorial/07_remove_liquidity.py new file mode 100644 index 0000000..aa86356 --- /dev/null +++ b/examples/v2/tutorial/07_remove_liquidity.py @@ -0,0 +1,54 @@ +# This sample is provided for demonstration purposes only. +# It is not intended for production use. +# This example does not constitute trading advice. +from pprint import pprint +from urllib.parse import quote_plus + +from examples.v2.tutorial.common import get_account, get_algod, get_assets +from tinyman.v2.client import TinymanV2TestnetClient + + +account = get_account() +algod = get_algod() +client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) + +ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] +ASSET_A = client.fetch_asset(ASSET_A_ID) +ASSET_B = client.fetch_asset(ASSET_B_ID) +pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) + +assert pool.exists, "Pool has not been bootstrapped yet!" +assert pool.issued_pool_tokens, "Pool has no liquidity" + +position = pool.fetch_pool_position() +pool_token_asset_in = position[pool.pool_token_asset].amount // 4 + +quote = pool.fetch_remove_liquidity_quote( + pool_token_asset_in=pool_token_asset_in, + slippage=0, # TODO: 0.05 +) + +print("\nRemove Liquidity Quote:") +print(quote) + +txn_group = pool.prepare_remove_liquidity_transactions_from_quote(quote=quote) + +# Sign +txn_group.sign_with_private_key(account["address"], account["private_key"]) + +# Submit +txinfo = txn_group.submit(algod, wait=True) +print("Transaction Info") +pprint(txinfo) + +print( + f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" +) + +pool.refresh() + +pool_position = pool.fetch_pool_position() +share = pool_position["share"] * 100 +print(f"Pool Tokens: {pool_position[pool.liquidity_asset]}") +print(f"Assets: {pool_position[ASSET_A]}, {pool_position[ASSET_B]}") +print(f"Share of pool: {share:.3f}%") diff --git a/examples/v2/tutorial/08_single_asset_remove_liquidity.py b/examples/v2/tutorial/08_single_asset_remove_liquidity.py new file mode 100644 index 0000000..f2ec10b --- /dev/null +++ b/examples/v2/tutorial/08_single_asset_remove_liquidity.py @@ -0,0 +1,57 @@ +# This sample is provided for demonstration purposes only. +# It is not intended for production use. +# This example does not constitute trading advice. +from pprint import pprint +from urllib.parse import quote_plus + +from examples.v2.tutorial.common import get_account, get_algod, get_assets +from tinyman.v2.client import TinymanV2TestnetClient + + +account = get_account() +algod = get_algod() +client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) + +ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] +ASSET_A = client.fetch_asset(ASSET_A_ID) +ASSET_B = client.fetch_asset(ASSET_B_ID) +pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) + +assert pool.exists, "Pool has not been bootstrapped yet!" +assert pool.issued_pool_tokens, "Pool has no liquidity" + +position = pool.fetch_pool_position() +pool_token_asset_in = position[pool.pool_token_asset].amount // 8 + +quote = pool.fetch_single_asset_remove_liquidity_quote( + pool_token_asset_in=pool_token_asset_in, + output_asset=pool.asset_1, + slippage=0, # TODO: 0.05 +) + +print("\nSingle Asset Remove Liquidity Quote:") +print(quote) + +print("\nInternal Swap Quote:") +print(quote.internal_swap_quote) + +txn_group = pool.prepare_remove_liquidity_transactions_from_quote(quote=quote) + +# Sign +txn_group.sign_with_private_key(account["address"], account["private_key"]) + +# Submit +txinfo = txn_group.submit(algod, wait=True) +print("Transaction Info") +pprint(txinfo) + +print( + f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" +) + +pool.refresh() +pool_position = pool.fetch_pool_position() +share = pool_position["share"] * 100 +print(f"Pool Tokens: {pool_position[pool.liquidity_asset]}") +print(f"Assets: {pool_position[ASSET_A]}, {pool_position[ASSET_B]}") +print(f"Share of pool: {share:.3f}%") diff --git a/examples/v2/tutorial/09_fixed_input_swap.py b/examples/v2/tutorial/09_fixed_input_swap.py new file mode 100644 index 0000000..9a2c96f --- /dev/null +++ b/examples/v2/tutorial/09_fixed_input_swap.py @@ -0,0 +1,48 @@ +# This sample is provided for demonstration purposes only. +# It is not intended for production use. +# This example does not constitute trading advice. +from pprint import pprint +from urllib.parse import quote_plus + +from tinyman.assets import AssetAmount + +from examples.v2.tutorial.common import get_account, get_algod, get_assets +from tinyman.v2.client import TinymanV2TestnetClient + + +account = get_account() +algod = get_algod() +client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) + +ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] +ASSET_A = client.fetch_asset(ASSET_A_ID) +ASSET_B = client.fetch_asset(ASSET_B_ID) +pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) + +assert pool.exists, "Pool has not been bootstrapped yet!" +assert pool.issued_pool_tokens, "Pool has no liquidity" + +position = pool.fetch_pool_position() +amount_in = AssetAmount(pool.asset_1, 1_000_000) + +quote = pool.fetch_fixed_input_swap_quote( + amount_in=amount_in, + slippage=0, # TODO: 0.05 +) + +print("\nSwap Quote:") +print(quote) + +txn_group = pool.prepare_swap_transactions_from_quote(quote=quote) + +# Sign +txn_group.sign_with_private_key(account["address"], account["private_key"]) + +# Submit +txinfo = txn_group.submit(algod, wait=True) +print("Transaction Info") +pprint(txinfo) + +print( + f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" +) diff --git a/examples/v2/tutorial/10_fixed_output_swap.py b/examples/v2/tutorial/10_fixed_output_swap.py new file mode 100644 index 0000000..638c959 --- /dev/null +++ b/examples/v2/tutorial/10_fixed_output_swap.py @@ -0,0 +1,48 @@ +# This sample is provided for demonstration purposes only. +# It is not intended for production use. +# This example does not constitute trading advice. +from pprint import pprint +from urllib.parse import quote_plus + +from tinyman.assets import AssetAmount + +from examples.v2.tutorial.common import get_account, get_algod, get_assets +from tinyman.v2.client import TinymanV2TestnetClient + + +account = get_account() +algod = get_algod() +client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) + +ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] +ASSET_A = client.fetch_asset(ASSET_A_ID) +ASSET_B = client.fetch_asset(ASSET_B_ID) +pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) + +assert pool.exists, "Pool has not been bootstrapped yet!" +assert pool.issued_pool_tokens, "Pool has no liquidity" + +position = pool.fetch_pool_position() +amount_out = AssetAmount(pool.asset_1, 1_000_000) + +quote = pool.fetch_fixed_output_swap_quote( + amount_out=amount_out, + slippage=0, # TODO: 0.05 +) + +print("\nSwap Quote:") +print(quote) + +txn_group = pool.prepare_swap_transactions_from_quote(quote=quote) + +# Sign +txn_group.sign_with_private_key(account["address"], account["private_key"]) + +# Submit +txinfo = txn_group.submit(algod, wait=True) +print("Transaction Info") +pprint(txinfo) + +print( + f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" +) diff --git a/examples/v2/tutorial/__init__.py b/examples/v2/tutorial/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/v2/tutorial/common.py b/examples/v2/tutorial/common.py new file mode 100644 index 0000000..63b583a --- /dev/null +++ b/examples/v2/tutorial/common.py @@ -0,0 +1,75 @@ +import json +import os +import string +import random +from pprint import pprint + +from algosdk.future.transaction import AssetCreateTxn, wait_for_confirmation +from algosdk.v2client.algod import AlgodClient + + +def get_account_file_path(filename="account.json"): + dir_path = os.path.dirname(os.path.realpath(__file__)) + file_path = os.path.join(dir_path, filename) + return file_path + + +def get_account(filename="account.json"): + file_path = get_account_file_path(filename) + try: + with open(file_path, "r", encoding="utf-8") as f: + account = json.loads(f.read()) + except FileNotFoundError: + raise Exception("Please run generate_account.py to generate a test account.") + + return account + + +def get_assets_file_path(filename="assets.json"): + dir_path = os.path.dirname(os.path.realpath(__file__)) + file_path = os.path.join(dir_path, filename) + return file_path + + +def get_assets(filename="assets.json"): + file_path = get_account_file_path(filename) + try: + with open(file_path, "r", encoding="utf-8") as f: + assets = json.loads(f.read()) + except FileNotFoundError: + raise Exception("Please run generate_account.py to generate a test account.") + + return assets + + +def get_algod(): + # return AlgodClient( + # "", "http://localhost:8080", headers={"User-Agent": "algosdk"} + # ) + return AlgodClient("", "https://testnet-api.algonode.network") + + +def create_asset(algod, sender, private_key): + sp = algod.suggested_params() + asset_name = "".join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(8) + ) + max_total = 2**64 - 1 + txn = AssetCreateTxn( + sender=sender, + sp=sp, + total=max_total, + decimals=6, + default_frozen=False, + unit_name=asset_name, + asset_name=asset_name, + ) + signed_txn = txn.sign(private_key) + transaction_id = algod.send_transaction(signed_txn) + print(f"Asset Creation Transaction ID: {transaction_id}") + result = wait_for_confirmation(algod, transaction_id) + print("Asset Creation Result:") + pprint(result) + asset_id = result["asset-index"] + print(f"Created Asset ID: {asset_id}") + return asset_id diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..f58854a --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,8 @@ +from algosdk.future.transaction import SuggestedParams + + +def get_suggested_params(): + sp = SuggestedParams( + fee=1000, first=1, last=1000, min_fee=1000, flat_fee=True, gh="test" + ) + return sp diff --git a/tests/v2/__init__.py b/tests/v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/v2/test_bootstrap.py b/tests/v2/test_bootstrap.py new file mode 100644 index 0000000..051ef83 --- /dev/null +++ b/tests/v2/test_bootstrap.py @@ -0,0 +1,84 @@ +import unittest + +from algosdk.account import generate_account +from algosdk.future.transaction import PaymentTxn, ApplicationOptInTxn +from algosdk.logic import get_application_address + +from tests import get_suggested_params +from tinyman.v2.bootstrap import prepare_bootstrap_transactions +from tinyman.v2.constants import BOOTSTRAP_APP_ARGUMENT +from tinyman.v2.contracts import get_pool_logicsig + + +class BootstrapTestCase(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.VALIDATOR_APP_ID = 12345 + + def test_asa_asa_pair(self): + _, sender = generate_account() + + suggested_params = get_suggested_params() + txn_group = prepare_bootstrap_transactions( + validator_app_id=self.VALIDATOR_APP_ID, + asset_1_id=10, + asset_2_id=8, + sender=sender, + app_call_fee=suggested_params.min_fee * 7, + required_algo=500_000, + suggested_params=suggested_params, + ) + + transactions = txn_group.transactions + self.assertEqual(len(transactions), 2) + + self.assertTrue(isinstance(transactions[0], PaymentTxn)) + self.assertEqual(transactions[0].amt, 500_000) + self.assertEqual(transactions[0].sender, sender) + self.assertEqual( + transactions[0].receiver, + get_pool_logicsig(self.VALIDATOR_APP_ID, 10, 8).address(), + ) + self.assertEqual(transactions[0].rekey_to, None) + + self.assertTrue(isinstance(transactions[1], ApplicationOptInTxn)) + self.assertEqual(transactions[1].index, self.VALIDATOR_APP_ID) + self.assertEqual( + transactions[1].sender, + get_pool_logicsig(self.VALIDATOR_APP_ID, 10, 8).address(), + ) + self.assertEqual(transactions[1].fee, suggested_params.min_fee * 7) + self.assertEqual(transactions[1].app_args, [BOOTSTRAP_APP_ARGUMENT]) + self.assertEqual(transactions[1].foreign_assets, [10, 8]) + self.assertEqual( + transactions[1].rekey_to, get_application_address(self.VALIDATOR_APP_ID) + ) + + def test_pool_is_already_funded(self): + _, sender = generate_account() + + suggested_params = get_suggested_params() + txn_group = prepare_bootstrap_transactions( + validator_app_id=self.VALIDATOR_APP_ID, + asset_1_id=10, + asset_2_id=8, + sender=sender, + app_call_fee=suggested_params.min_fee * 6, + required_algo=0, + suggested_params=suggested_params, + ) + + transactions = txn_group.transactions + self.assertEqual(len(transactions), 1) + self.assertTrue(isinstance(transactions[0], ApplicationOptInTxn)) + self.assertEqual(transactions[0].index, self.VALIDATOR_APP_ID) + self.assertEqual( + transactions[0].sender, + get_pool_logicsig(self.VALIDATOR_APP_ID, 10, 8).address(), + ) + self.assertEqual(transactions[0].fee, suggested_params.min_fee * 6) + self.assertEqual(transactions[0].app_args, [BOOTSTRAP_APP_ARGUMENT]) + self.assertEqual(transactions[0].foreign_assets, [10, 8]) + self.assertEqual( + transactions[0].rekey_to, get_application_address(self.VALIDATOR_APP_ID) + ) diff --git a/tinyman/client.py b/tinyman/client.py new file mode 100644 index 0000000..063f3b8 --- /dev/null +++ b/tinyman/client.py @@ -0,0 +1,66 @@ +from algosdk.v2client.algod import AlgodClient +from algosdk.future.transaction import wait_for_confirmation +from tinyman.assets import Asset +from tinyman.optin import prepare_asset_optin_transactions + + +class BaseTinymanClient: + def __init__( + self, + algod_client: AlgodClient, + validator_app_id: int, + user_address=None, + staking_app_id: int = None, + ): + self.algod = algod_client + self.validator_app_id = validator_app_id + self.staking_app_id = staking_app_id + self.assets_cache = {} + self.user_address = user_address + + def fetch_pool(self, *args, **kwargs): + raise NotImplementedError() + + def fetch_asset(self, asset_id): + if asset_id not in self.assets_cache: + asset = Asset(asset_id) + asset.fetch(self.algod) + self.assets_cache[asset_id] = asset + return self.assets_cache[asset_id] + + def submit(self, transaction_group, wait=False): + txid = self.algod.send_transactions(transaction_group.signed_transactions) + if wait: + txinfo = wait_for_confirmation(self.algod, txid) + txinfo["txid"] = txid + return txinfo + return {"txid": txid} + + def prepare_asset_optin_transactions( + self, asset_id, user_address=None, suggested_params=None + ): + user_address = user_address or self.user_address + if suggested_params is None: + suggested_params = self.algod.suggested_params() + txn_group = prepare_asset_optin_transactions( + asset_id=asset_id, + sender=user_address, + suggested_params=suggested_params, + ) + return txn_group + + def is_opted_in(self, user_address=None): + user_address = user_address or self.user_address + account_info = self.algod.account_info(user_address) + for a in account_info.get("apps-local-state", []): + if a["id"] == self.validator_app_id: + return True + return False + + def asset_is_opted_in(self, asset_id, user_address=None): + user_address = user_address or self.user_address + account_info = self.algod.account_info(user_address) + for a in account_info.get("assets", []): + if a["asset-id"] == asset_id: + return True + return False diff --git a/tinyman/v1/optin.py b/tinyman/optin.py similarity index 100% rename from tinyman/v1/optin.py rename to tinyman/optin.py diff --git a/tinyman/v1/staking/__init__.py b/tinyman/staking/__init__.py similarity index 99% rename from tinyman/v1/staking/__init__.py rename to tinyman/staking/__init__.py index 0292a3e..fc1d92e 100644 --- a/tinyman/v1/staking/__init__.py +++ b/tinyman/staking/__init__.py @@ -27,7 +27,7 @@ int_to_bytes, timestamp_to_date_str, ) -from tinyman.v1.staking.constants import DATE_FORMAT +from tinyman.staking.constants import DATE_FORMAT def prepare_create_transaction(args, sender, suggested_params): diff --git a/tinyman/v1/staking/asc.json b/tinyman/staking/asc.json similarity index 100% rename from tinyman/v1/staking/asc.json rename to tinyman/staking/asc.json diff --git a/tinyman/staking/constants.py b/tinyman/staking/constants.py new file mode 100644 index 0000000..a82efa7 --- /dev/null +++ b/tinyman/staking/constants.py @@ -0,0 +1,4 @@ +DATE_FORMAT = "%Y%m%d" + +TESTNET_STAKING_APP_ID = 51948952 +MAINNET_STAKING_APP_ID = 649588853 diff --git a/tinyman/v1/staking/contracts.py b/tinyman/staking/contracts.py similarity index 87% rename from tinyman/v1/staking/contracts.py rename to tinyman/staking/contracts.py index 4814d03..ee9c7c3 100644 --- a/tinyman/v1/staking/contracts.py +++ b/tinyman/staking/contracts.py @@ -1,7 +1,7 @@ import importlib.resources import json -import tinyman.v1.staking +import tinyman.staking _contracts = json.loads(importlib.resources.read_text(tinyman.v1.staking, "asc.json")) diff --git a/tinyman/utils.py b/tinyman/utils.py index 50d00a5..95e13a1 100644 --- a/tinyman/utils.py +++ b/tinyman/utils.py @@ -8,28 +8,6 @@ from algosdk.error import AlgodHTTPError -def get_program(definition, variables=None): - """ - Return a byte array to be used in LogicSig. - """ - template = definition["bytecode"] - template_bytes = list(b64decode(template)) - - offset = 0 - for v in sorted(definition["variables"], key=lambda v: v["index"]): - name = v["name"].split("TMPL_")[-1].lower() - value = variables[name] - start = v["index"] - offset - end = start + v["length"] - value_encoded = encode_value(value, v["type"]) - value_encoded_len = len(value_encoded) - diff = v["length"] - value_encoded_len - offset += diff - template_bytes[start:end] = list(value_encoded) - - return bytes(template_bytes) - - def encode_value(value, type): if type == "int": return encode_varint(value) @@ -71,6 +49,8 @@ def int_list_to_bytes(nums): def bytes_to_int(b): + if type(b) == str: + b = b64decode(b) return int.from_bytes(b, "big") @@ -130,14 +110,30 @@ def timestamp_to_date_str(t): return d.strftime("%Y-%m-%d") +def calculate_price_impact( + input_supply, output_supply, swap_input_amount, swap_output_amount +): + swap_price = swap_output_amount / swap_input_amount + pool_price = output_supply / input_supply + price_impact = abs(round((swap_price / pool_price) - 1, 5)) + return price_impact + + class TransactionGroup: def __init__(self, transactions): transactions = assign_group_id(transactions) self.transactions = transactions self.signed_transactions = [None for _ in self.transactions] - def sign(self, user): - user.sign_transaction_group(self) + @property + def id(self): + try: + byte_group_id = self.transactions[0].group + except IndexError: + return + + group_id = b64encode(byte_group_id).decode("utf-8") + return group_id def sign_with_logicisg(self, logicsig): address = logicsig.address() @@ -160,3 +156,9 @@ def submit(self, algod, wait=False): txinfo["txid"] = txid return txinfo return {"txid": txid} + + def __add__(self, other): + transactions = self.transactions + other.transactions + for txn in transactions: + txn.group = None + return TransactionGroup(transactions) diff --git a/tinyman/v1/client.py b/tinyman/v1/client.py index 50d5768..88a4908 100644 --- a/tinyman/v1/client.py +++ b/tinyman/v1/client.py @@ -1,51 +1,26 @@ from base64 import b64decode from algosdk.v2client.algod import AlgodClient from algosdk.encoding import encode_address -from algosdk.future.transaction import wait_for_confirmation -from tinyman.assets import Asset, AssetAmount -from .optin import prepare_app_optin_transactions, prepare_asset_optin_transactions -from .constants import ( - TESTNET_VALIDATOR_APP_ID, - MAINNET_VALIDATOR_APP_ID, +from tinyman.assets import AssetAmount +from tinyman.client import BaseTinymanClient +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, +) -class TinymanClient: - def __init__( - self, - algod_client: AlgodClient, - validator_app_id: int, - user_address=None, - staking_app_id: int = None, - ): - self.algod = algod_client - self.validator_app_id = validator_app_id - self.staking_app_id = staking_app_id - self.assets_cache = {} - self.user_address = user_address +class TinymanClient(BaseTinymanClient): def fetch_pool(self, asset1, asset2, fetch=True): from .pools import Pool return Pool(self, asset1, asset2, fetch=fetch) - def fetch_asset(self, asset_id): - if asset_id not in self.assets_cache: - asset = Asset(asset_id) - asset.fetch(self.algod) - self.assets_cache[asset_id] = asset - return self.assets_cache[asset_id] - - def submit(self, transaction_group, wait=False): - txid = self.algod.send_transactions(transaction_group.signed_transactions) - if wait: - txinfo = wait_for_confirmation(self.algod, txid) - txinfo["txid"] = txid - return txinfo - return {"txid": txid} - def prepare_app_optin_transactions(self, user_address=None): user_address = user_address or self.user_address suggested_params = self.algod.suggested_params() @@ -56,16 +31,6 @@ def prepare_app_optin_transactions(self, user_address=None): ) return txn_group - def prepare_asset_optin_transactions(self, asset_id, user_address=None): - user_address = user_address or self.user_address - suggested_params = self.algod.suggested_params() - txn_group = prepare_asset_optin_transactions( - asset_id=asset_id, - sender=user_address, - suggested_params=suggested_params, - ) - return txn_group - def fetch_excess_amounts(self, user_address=None): user_address = user_address or self.user_address account_info = self.algod.account_info(user_address) @@ -97,22 +62,6 @@ def fetch_excess_amounts(self, user_address=None): return pools - def is_opted_in(self, user_address=None): - user_address = user_address or self.user_address - account_info = self.algod.account_info(user_address) - for a in account_info.get("apps-local-state", []): - if a["id"] == self.validator_app_id: - return True - return False - - def asset_is_opted_in(self, asset_id, user_address=None): - user_address = user_address or self.user_address - account_info = self.algod.account_info(user_address) - for a in account_info.get("assets", []): - if a["asset-id"] == asset_id: - return True - return False - class TinymanTestnetClient(TinymanClient): def __init__(self, algod_client: AlgodClient, user_address=None): diff --git a/tinyman/v1/constants.py b/tinyman/v1/constants.py index 93ef6b4..0ef6e71 100644 --- a/tinyman/v1/constants.py +++ b/tinyman/v1/constants.py @@ -6,11 +6,9 @@ TESTNET_VALIDATOR_APP_ID_V1_0 = 21580889 TESTNET_VALIDATOR_APP_ID_V1_1 = 62368684 -TESTNET_STAKING_APP_ID = 51948952 MAINNET_VALIDATOR_APP_ID_V1_0 = 350338509 MAINNET_VALIDATOR_APP_ID_V1_1 = 552635992 -MAINNET_STAKING_APP_ID = 649588853 TESTNET_VALIDATOR_APP_ID = TESTNET_VALIDATOR_APP_ID_V1_1 MAINNET_VALIDATOR_APP_ID = MAINNET_VALIDATOR_APP_ID_V1_1 diff --git a/tinyman/v1/contracts.py b/tinyman/v1/contracts.py index e9a61ab..53e8a26 100644 --- a/tinyman/v1/contracts.py +++ b/tinyman/v1/contracts.py @@ -1,8 +1,9 @@ import json import importlib.resources -from algosdk.future.transaction import LogicSig +from algosdk.future.transaction import LogicSigAccount import tinyman.v1 -from tinyman.utils import get_program +from base64 import b64decode +from tinyman.utils import encode_value _contracts = json.loads(importlib.resources.read_text(tinyman.v1, "asc.json")) @@ -11,6 +12,28 @@ validator_app_def = _contracts["contracts"]["validator_app"] +def get_program(definition, variables=None): + """ + Return a byte array to be used in LogicSig. + """ + template = definition["bytecode"] + template_bytes = list(b64decode(template)) + + offset = 0 + for v in sorted(definition["variables"], key=lambda v: v["index"]): + name = v["name"].split("TMPL_")[-1].lower() + value = variables[name] + start = v["index"] - offset + end = start + v["length"] + value_encoded = encode_value(value, v["type"]) + value_encoded_len = len(value_encoded) + diff = v["length"] - value_encoded_len + offset += diff + template_bytes[start:end] = list(value_encoded) + + return bytes(template_bytes) + + def get_pool_logicsig(validator_app_id, asset1_id, asset2_id): assets = [asset1_id, asset2_id] asset_id_1 = max(assets) @@ -23,4 +46,4 @@ def get_pool_logicsig(validator_app_id, asset1_id, asset2_id): asset_id_2=asset_id_2, ), ) - return LogicSig(program=program_bytes) + return LogicSigAccount(program=program_bytes) diff --git a/tinyman/v1/pools.py b/tinyman/v1/pools.py index 21e9dc9..134fe87 100644 --- a/tinyman/v1/pools.py +++ b/tinyman/v1/pools.py @@ -11,7 +11,7 @@ from .mint import prepare_mint_transactions from .burn import prepare_burn_transactions from .redeem import prepare_redeem_transactions -from .optin import prepare_asset_optin_transactions +from tinyman.optin import prepare_asset_optin_transactions from .fees import prepare_redeem_fees_transactions from .client import TinymanClient diff --git a/tinyman/v1/staking/constants.py b/tinyman/v1/staking/constants.py deleted file mode 100644 index 64fa003..0000000 --- a/tinyman/v1/staking/constants.py +++ /dev/null @@ -1 +0,0 @@ -DATE_FORMAT = "%Y%m%d" diff --git a/tinyman/v2/__init__.py b/tinyman/v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tinyman/v2/add_liquidity.py b/tinyman/v2/add_liquidity.py new file mode 100644 index 0000000..8f6b030 --- /dev/null +++ b/tinyman/v2/add_liquidity.py @@ -0,0 +1,194 @@ +from typing import Optional + +from algosdk.future.transaction import ( + ApplicationNoOpTxn, + PaymentTxn, + AssetTransferTxn, + SuggestedParams, +) + +from tinyman.utils import TransactionGroup +from .constants import ( + ADD_LIQUIDITY_APP_ARGUMENT, + ADD_LIQUIDITY_FLEXIBLE_MODE_APP_ARGUMENT, + ADD_LIQUIDITY_SINGLE_MODE_APP_ARGUMENT, + ADD_INITIAL_LIQUIDITY_APP_ARGUMENT, +) +from .contracts import get_pool_logicsig + + +def prepare_flexible_add_liquidity_transactions( + validator_app_id: int, + asset_1_id: int, + asset_2_id: int, + pool_token_asset_id: int, + asset_1_amount: int, + asset_2_amount: int, + min_pool_token_asset_amount: int, + sender: str, + suggested_params: SuggestedParams, +): + pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) + pool_address = pool_logicsig.address() + + txns = [ + AssetTransferTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + amt=int(asset_1_amount), + index=asset_1_id, + ), + AssetTransferTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + amt=int(asset_2_amount), + index=asset_2_id, + ) + if asset_2_id != 0 + else PaymentTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + amt=int(asset_2_amount), + ), + ApplicationNoOpTxn( + sender=sender, + sp=suggested_params, + index=validator_app_id, + app_args=[ + ADD_LIQUIDITY_APP_ARGUMENT, + ADD_LIQUIDITY_FLEXIBLE_MODE_APP_ARGUMENT, + int(min_pool_token_asset_amount), + ], + foreign_assets=[pool_token_asset_id], + accounts=[pool_address], + ), + ] + + min_fee = suggested_params.min_fee + app_call_fee = min_fee * 3 + txns[-1].fee = app_call_fee + + txn_group = TransactionGroup(txns) + return txn_group + + +def prepare_single_asset_add_liquidity_transactions( + validator_app_id: int, + asset_1_id: int, + asset_2_id: int, + pool_token_asset_id: int, + min_pool_token_asset_amount: int, + sender: str, + suggested_params: SuggestedParams, + asset_1_amount: Optional[int] = None, + asset_2_amount: Optional[int] = None, +): + pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) + pool_address = pool_logicsig.address() + + assert bool(asset_1_amount) != bool( + asset_2_amount + ), "If you want to add asset 1 and asset 2 at the same time, please use flexible add liquidity." + + if asset_1_amount: + asset_in_id = asset_1_id + asset_in_amount = asset_1_amount + + elif asset_2_amount: + asset_in_id = asset_2_id + asset_in_amount = asset_2_amount + + else: + raise Exception("Invalid asset_1_amount and asset_2_amount") + + txns = [ + AssetTransferTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + amt=int(asset_in_amount), + index=asset_in_id, + ) + if asset_in_id != 0 + else PaymentTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + amt=int(asset_in_amount), + ), + ApplicationNoOpTxn( + sender=sender, + sp=suggested_params, + index=validator_app_id, + app_args=[ + ADD_LIQUIDITY_APP_ARGUMENT, + ADD_LIQUIDITY_SINGLE_MODE_APP_ARGUMENT, + int(min_pool_token_asset_amount), + ], + foreign_assets=[pool_token_asset_id], + accounts=[pool_address], + ), + ] + + min_fee = suggested_params.min_fee + app_call_fee = min_fee * 3 + txns[-1].fee = app_call_fee + + txn_group = TransactionGroup(txns) + return txn_group + + +def prepare_initial_add_liquidity_transactions( + validator_app_id: int, + asset_1_id: int, + asset_2_id: int, + pool_token_asset_id: int, + asset_1_amount: int, + asset_2_amount: int, + sender: str, + suggested_params: SuggestedParams, +): + pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) + pool_address = pool_logicsig.address() + + txns = [ + AssetTransferTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + amt=int(asset_1_amount), + index=asset_1_id, + ), + AssetTransferTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + amt=int(asset_2_amount), + index=asset_2_id, + ) + if asset_2_id != 0 + else PaymentTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + amt=int(asset_2_amount), + ), + ApplicationNoOpTxn( + sender=sender, + sp=suggested_params, + index=validator_app_id, + app_args=[ADD_INITIAL_LIQUIDITY_APP_ARGUMENT], + foreign_assets=[pool_token_asset_id], + accounts=[pool_address], + ), + ] + + min_fee = suggested_params.min_fee + app_call_fee = min_fee * 2 + txns[-1].fee = app_call_fee + + txn_group = TransactionGroup(txns) + return txn_group diff --git a/tinyman/v2/bootstrap.py b/tinyman/v2/bootstrap.py new file mode 100644 index 0000000..279b11e --- /dev/null +++ b/tinyman/v2/bootstrap.py @@ -0,0 +1,53 @@ +from algosdk.future.transaction import ( + ApplicationOptInTxn, + PaymentTxn, + SuggestedParams, +) +from algosdk.logic import get_application_address + +from tinyman.utils import TransactionGroup +from .constants import BOOTSTRAP_APP_ARGUMENT +from .contracts import get_pool_logicsig + + +def prepare_bootstrap_transactions( + validator_app_id: int, + asset_1_id: int, + asset_2_id: int, + sender: str, + app_call_fee: int, + required_algo: int, + suggested_params: SuggestedParams, +): + pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) + pool_address = pool_logicsig.address() + assert asset_1_id > asset_2_id + + txns = list() + + # Fund pool account to cover minimum balance and fee requirements + if required_algo: + txns.append( + PaymentTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + amt=int(required_algo), + ) + ) + + # Bootstrap (Opt-in) App Call + bootstrap_app_call = ApplicationOptInTxn( + sender=pool_address, + sp=suggested_params, + index=validator_app_id, + app_args=[BOOTSTRAP_APP_ARGUMENT], + foreign_assets=[asset_1_id, asset_2_id], + rekey_to=get_application_address(validator_app_id), + ) + bootstrap_app_call.fee = app_call_fee + txns.append(bootstrap_app_call) + + txn_group = TransactionGroup(txns) + txn_group.sign_with_logicisg(pool_logicsig) + return txn_group diff --git a/tinyman/v2/client.py b/tinyman/v2/client.py new file mode 100644 index 0000000..7e3eae2 --- /dev/null +++ b/tinyman/v2/client.py @@ -0,0 +1,38 @@ +from algosdk.v2client.algod import AlgodClient +from tinyman.client import BaseTinymanClient +from tinyman.staking.constants import ( + TESTNET_STAKING_APP_ID, + MAINNET_STAKING_APP_ID, +) + +from tinyman.v2.constants import ( + TESTNET_VALIDATOR_APP_ID, + MAINNET_VALIDATOR_APP_ID, +) + + +class TinymanV2Client(BaseTinymanClient): + def fetch_pool(self, asset_1, asset_2, fetch=True): + from .pools import Pool + + return Pool(self, asset_1, asset_2, fetch=fetch) + + +class TinymanV2TestnetClient(TinymanV2Client): + def __init__(self, algod_client: AlgodClient, user_address=None): + super().__init__( + algod_client, + validator_app_id=TESTNET_VALIDATOR_APP_ID, + user_address=user_address, + staking_app_id=TESTNET_STAKING_APP_ID, + ) + + +class TinymanV2MainnetClient(TinymanV2Client): + def __init__(self, algod_client: AlgodClient, user_address=None): + super().__init__( + algod_client, + validator_app_id=MAINNET_VALIDATOR_APP_ID, + user_address=user_address, + staking_app_id=MAINNET_STAKING_APP_ID, + ) diff --git a/tinyman/v2/contracts.py b/tinyman/v2/contracts.py new file mode 100644 index 0000000..0e44d57 --- /dev/null +++ b/tinyman/v2/contracts.py @@ -0,0 +1,26 @@ +import json +import importlib.resources +from base64 import b64decode + +from algosdk.future.transaction import LogicSigAccount + +import tinyman.v1 + + +_contracts = json.loads(importlib.resources.read_text(tinyman.v2, "asc.json")) + +pool_logicsig_def = _contracts["contracts"]["pool_logicsig"]["logic"] + +# validator_app_def = _contracts["contracts"]["validator_app"] + + +def get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id): + assets = [asset_1_id, asset_2_id] + asset_1_id = max(assets) + asset_2_id = min(assets) + + program = bytearray(b64decode(pool_logicsig_def["bytecode"])) + program[3:11] = validator_app_id.to_bytes(8, "big") + program[11:19] = asset_1_id.to_bytes(8, "big") + program[19:27] = asset_2_id.to_bytes(8, "big") + return LogicSigAccount(program) diff --git a/tinyman/v2/formulas.py b/tinyman/v2/formulas.py new file mode 100644 index 0000000..f78d1e2 --- /dev/null +++ b/tinyman/v2/formulas.py @@ -0,0 +1,191 @@ +import math + +from tinyman.assets import AssetAmount +from tinyman.utils import calculate_price_impact +from tinyman.v2.constants import LOCKED_POOL_TOKENS +from tinyman.v2.quotes import InternalSwapQuote + + +def calculate_protocol_fee_amount( + total_fee_amount: int, protocol_fee_ratio: int +) -> int: + protocol_fee_amount = total_fee_amount // protocol_fee_ratio + return protocol_fee_amount + + +def calculate_poolers_fee_amount(total_fee_amount: int, protocol_fee_ratio: int) -> int: + protocol_fee_amount = calculate_protocol_fee_amount( + total_fee_amount, protocol_fee_ratio + ) + poolers_fee_amount = total_fee_amount - protocol_fee_amount + return poolers_fee_amount + + +def calculate_fixed_input_fee_amount(input_amount: int, total_fee_share: int) -> int: + total_fee_amount = (input_amount * total_fee_share) // 10000 + return total_fee_amount + + +def calculate_fixed_output_fee_amounts(swap_amount: int, total_fee_share: int) -> int: + input_amount = (swap_amount * 10000) // (10000 - total_fee_share) + total_fee_amount = input_amount - swap_amount + return total_fee_amount + + +def get_internal_swap_fee_amount(swap_amount, total_fee_share) -> int: + total_fee_amount = int((swap_amount * total_fee_share) / (10_000 - total_fee_share)) + return total_fee_amount + + +def get_initial_add_liquidity(asset_1_amount, asset_2_amount) -> int: + assert ( + not asset_1_amount or not asset_2_amount + ), "Both assets are required for the initial add liquidity" + + pool_token_asset_amount = ( + int(math.sqrt(asset_1_amount * asset_2_amount)) - LOCKED_POOL_TOKENS + ) + return pool_token_asset_amount + + +def calculate_remove_liquidity_output_amounts( + pool_token_asset_amount, asset_1_reserves, asset_2_reserves, issued_pool_tokens +) -> (int, int): + asset_1_output_amount = int( + (pool_token_asset_amount * asset_1_reserves) / issued_pool_tokens + ) + asset_2_output_amount = int( + (pool_token_asset_amount * asset_2_reserves) / issued_pool_tokens + ) + return asset_1_output_amount, asset_2_output_amount + + +def get_subsequent_add_liquidity(pool, asset_1_amount, asset_2_amount): + # TODO: Remove pool input and don't return quote here. + old_k = pool.asset_1_reserves * pool.asset_2_reserves + new_asset_1_reserves = pool.asset_1_reserves + asset_1_amount + new_asset_2_reserves = pool.asset_2_reserves + asset_2_amount + new_k = new_asset_1_reserves * new_asset_2_reserves + new_issued_pool_tokens = int( + math.sqrt(int((new_k * (pool.issued_pool_tokens**2)) / old_k)) + ) + + pool_token_asset_amount = new_issued_pool_tokens - pool.issued_pool_tokens + calculated_asset_1_amount = int( + (pool_token_asset_amount * new_asset_1_reserves) / new_issued_pool_tokens + ) + calculated_asset_2_amount = int( + (pool_token_asset_amount * new_asset_2_reserves) / new_issued_pool_tokens + ) + + asset_1_swap_amount = asset_1_amount - calculated_asset_1_amount + asset_2_swap_amount = asset_2_amount - calculated_asset_2_amount + + if asset_1_swap_amount > asset_2_swap_amount: + swap_in_amount_without_fee = asset_1_swap_amount + swap_out_amount = -min(asset_2_swap_amount, 0) + swap_in_asset = pool.asset_1 + swap_out_asset = pool.asset_2 + + total_fee_amount = get_internal_swap_fee_amount( + swap_in_amount_without_fee, + pool.total_fee_share, + ) + fee_as_pool_tokens = int( + total_fee_amount * new_issued_pool_tokens / (new_asset_1_reserves * 2) + ) + swap_in_amount = swap_in_amount_without_fee + total_fee_amount + pool_token_asset_amount = pool_token_asset_amount - fee_as_pool_tokens + else: + swap_in_amount_without_fee = asset_2_swap_amount + swap_out_amount = -min(asset_1_swap_amount, 0) + swap_in_asset = pool.asset_2 + swap_out_asset = pool.asset_1 + + total_fee_amount = get_internal_swap_fee_amount( + swap_in_amount_without_fee, + pool.total_fee_share, + ) + fee_as_pool_tokens = int( + total_fee_amount * new_issued_pool_tokens / (new_asset_2_reserves * 2) + ) + swap_in_amount = swap_in_amount_without_fee + total_fee_amount + pool_token_asset_amount = pool_token_asset_amount - fee_as_pool_tokens + + price_impact = calculate_price_impact( + input_supply=pool.asset_1_reserves + if swap_in_asset == pool.asset_1 + else pool.asset_2_reserves, + output_supply=pool.asset_1_reserves + if swap_out_asset == pool.asset_1 + else pool.asset_2_reserves, + swap_input_amount=swap_in_amount, + swap_output_amount=swap_out_amount, + ) + + internal_swap_quote = InternalSwapQuote( + amount_in=AssetAmount(swap_in_asset, swap_in_amount), + amount_out=AssetAmount(swap_out_asset, swap_out_amount), + swap_fees=AssetAmount(swap_in_asset, int(total_fee_amount)), + price_impact=price_impact, + ) + + return pool_token_asset_amount, internal_swap_quote + + +def calculate_output_amount_of_fixed_input_swap( + input_supply: int, output_supply: int, swap_amount: int +) -> int: + k = input_supply * output_supply + output_amount = output_supply - int(k / (input_supply + swap_amount)) + output_amount -= 1 + return output_amount + + +def calculate_swap_amount_of_fixed_output_swap( + input_supply: int, output_supply: int, output_amount: int +) -> int: + k = input_supply * output_supply + swap_amount = int(k / (output_supply - output_amount)) - input_supply + swap_amount += 1 + return swap_amount + + +def calculate_fixed_input_swap( + input_supply: int, output_supply: int, swap_input_amount: int, total_fee_share: int +) -> (int, int, int, float): + total_fee_amount = calculate_fixed_input_fee_amount( + input_amount=swap_input_amount, total_fee_share=total_fee_share + ) + swap_amount = swap_input_amount - total_fee_amount + swap_output_amount = calculate_output_amount_of_fixed_input_swap( + input_supply, output_supply, swap_amount + ) + + price_impact = calculate_price_impact( + input_supply=input_supply, + output_supply=output_supply, + swap_input_amount=swap_input_amount, + swap_output_amount=swap_output_amount, + ) + return swap_output_amount, total_fee_amount, price_impact + + +def calculate_fixed_output_swap( + input_supply: int, output_supply: int, swap_output_amount: int, total_fee_share: int +): + swap_amount = calculate_swap_amount_of_fixed_output_swap( + input_supply, output_supply, swap_output_amount + ) + total_fee_amount = calculate_fixed_output_fee_amounts( + swap_amount=swap_amount, total_fee_share=total_fee_share + ) + swap_input_amount = swap_amount + total_fee_amount + + price_impact = calculate_price_impact( + input_supply=input_supply, + output_supply=output_supply, + swap_input_amount=swap_input_amount, + swap_output_amount=swap_output_amount, + ) + return swap_input_amount, total_fee_amount, price_impact diff --git a/tinyman/v2/pools.py b/tinyman/v2/pools.py new file mode 100644 index 0000000..d0cec3d --- /dev/null +++ b/tinyman/v2/pools.py @@ -0,0 +1,700 @@ +from typing import Optional + +from algosdk.v2client.algod import AlgodClient + +from tinyman.assets import Asset, AssetAmount +from tinyman.optin import prepare_asset_optin_transactions +from tinyman.utils import get_state_int, get_state_bytes, bytes_to_int +from .add_liquidity import ( + prepare_initial_add_liquidity_transactions, + prepare_single_asset_add_liquidity_transactions, + prepare_flexible_add_liquidity_transactions, +) +from .bootstrap import prepare_bootstrap_transactions +from .client import TinymanV2Client +from .constants import MIN_POOL_BALANCE_ASA_ALGO_PAIR, MIN_POOL_BALANCE_ASA_ASA_PAIR +from .contracts import get_pool_logicsig +from .formulas import ( + get_subsequent_add_liquidity, + get_initial_add_liquidity, + calculate_fixed_input_swap, + calculate_remove_liquidity_output_amounts, + calculate_fixed_output_swap, +) +from .quotes import ( + AddLiquidityQuote, + RemoveLiquidityQuote, + InternalSwapQuote, + SingleAssetRemoveLiquidityQuote, + SwapQuote, +) +from .remove_liquidity import ( + prepare_remove_liquidity_transactions, + prepare_single_asset_remove_liquidity_transactions, +) +from .swap import prepare_swap_transactions + + +def get_pool_info(client: AlgodClient, validator_app_id, asset_1_id, asset_2_id): + pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) + pool_address = pool_logicsig.address() + account_info = client.account_info(pool_address) + return get_pool_info_from_account_info(account_info) + + +def get_pool_info_from_account_info(account_info): + try: + validator_app_id = account_info["apps-local-state"][0]["id"] + except IndexError: + return {} + validator_app_state = { + x["key"]: x["value"] for x in account_info["apps-local-state"][0]["key-value"] + } + + asset_1_id = get_state_int(validator_app_state, "asset_1_id") + asset_2_id = get_state_int(validator_app_state, "asset_2_id") + + pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) + pool_address = pool_logicsig.address() + + assert account_info["address"] == pool_address + + pool_token_asset_id = get_state_int(validator_app_state, "pool_token_asset_id") + issued_pool_tokens = get_state_int(validator_app_state, "issued_pool_tokens") + + # reserves + asset_1_reserves = get_state_int(validator_app_state, "asset_1_reserves") + asset_2_reserves = get_state_int(validator_app_state, "asset_2_reserves") + + # fees + asset_1_protocol_fees = get_state_int(validator_app_state, "asset_1_protocol_fees") + asset_2_protocol_fees = get_state_int(validator_app_state, "asset_2_protocol_fees") + + # fee rates + total_fee_share = get_state_int(validator_app_state, "total_fee_share") + protocol_fee_ratio = get_state_int(validator_app_state, "protocol_fee_ratio") + + # oracle + asset_1_cumulative_price = bytes_to_int( + get_state_bytes(validator_app_state, "asset_1_cumulative_price") + ) + asset_2_cumulative_price = bytes_to_int( + get_state_bytes(validator_app_state, "asset_2_cumulative_price") + ) + cumulative_price_update_timestamp = get_state_int( + validator_app_state, "cumulative_price_update_timestamp" + ) + + pool = { + "address": pool_address, + "asset_1_id": asset_1_id, + "asset_2_id": asset_2_id, + "pool_token_asset_id": pool_token_asset_id, + "asset_1_reserves": asset_1_reserves, + "asset_2_reserves": asset_2_reserves, + "issued_pool_tokens": issued_pool_tokens, + "asset_1_protocol_fees": asset_1_protocol_fees, + "asset_2_protocol_fees": asset_2_protocol_fees, + "asset_1_cumulative_price": asset_1_cumulative_price, + "asset_2_cumulative_price": asset_2_cumulative_price, + "cumulative_price_update_timestamp": cumulative_price_update_timestamp, + "total_fee_share": total_fee_share, + "protocol_fee_ratio": protocol_fee_ratio, + "validator_app_id": validator_app_id, + "algo_balance": account_info["amount"], + "round": account_info["round"], + } + return pool + + +class Pool: + def __init__( + self, + client: TinymanV2Client, + asset_a: Asset, + asset_b: Asset, + info=None, + fetch=True, + validator_app_id=None, + ) -> None: + self.client = client + self.validator_app_id = ( + validator_app_id + if validator_app_id is not None + else client.validator_app_id + ) + + if isinstance(asset_a, int): + asset_a = client.fetch_asset(asset_a) + if isinstance(asset_b, int): + asset_b = client.fetch_asset(asset_b) + + if asset_a.id > asset_b.id: + self.asset_1 = asset_a + self.asset_2 = asset_b + else: + self.asset_1 = asset_b + self.asset_2 = asset_a + + self.exists = None + self.pool_token_asset: Asset = None + self.asset_1_reserves = None + self.asset_2_reserves = None + self.issued_pool_tokens = None + self.asset_1_protocol_fees = None + self.asset_2_protocol_fees = None + self.total_fee_share = None + self.protocol_fee_ratio = None + self.last_refreshed_round = None + self.algo_balance = None + + if fetch: + self.refresh() + elif info is not None: + self.update_from_info(info) + + 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}" + + @classmethod + def from_account_info(cls, account_info, client=None): + info = get_pool_info_from_account_info(account_info) + pool = Pool( + client, + info["asset_1_id"], + info["asset_2_id"], + info, + validator_app_id=info["validator_app_id"], + ) + return pool + + def refresh(self, info=None): + if info is None: + info = get_pool_info( + self.client.algod, + self.validator_app_id, + self.asset_1.id, + self.asset_2.id, + ) + if not info: + return + self.update_from_info(info) + + def update_from_info(self, info): + if info["pool_token_asset_id"] is not None: + self.exists = True + + self.pool_token_asset = self.client.fetch_asset(info["pool_token_asset_id"]) + self.asset_1_reserves = info["asset_1_reserves"] + self.asset_2_reserves = info["asset_2_reserves"] + self.issued_pool_tokens = info["issued_pool_tokens"] + self.asset_1_protocol_fees = info["asset_1_protocol_fees"] + self.asset_2_protocol_fees = info["asset_2_protocol_fees"] + self.total_fee_share = info["total_fee_share"] + self.protocol_fee_ratio = info["protocol_fee_ratio"] + self.last_refreshed_round = info["round"] + self.algo_balance = info["algo_balance"] + + def get_logicsig(self): + pool_logicsig = get_pool_logicsig( + self.validator_app_id, self.asset_1.id, self.asset_2.id + ) + return pool_logicsig + + @property + def address(self): + logicsig = self.get_logicsig() + pool_address = logicsig.address() + return pool_address + + @property + def asset_1_price(self): + assert self.issued_pool_tokens + + return self.asset_2_reserves / self.asset_1_reserves + + @property + def asset_2_price(self): + assert self.issued_pool_tokens + + return self.asset_1_reserves / self.asset_2_reserves + + def info(self): + assert self.exists + + pool = { + "address": self.address, + "asset_1_id": self.asset_1.id, + "asset_2_id": self.asset_2.id, + "asset_1_unit_name": self.asset_1.unit_name, + "asset_2_unit_name": self.asset_2.unit_name, + "pool_token_asset_id": self.pool_token_asset.id, + "pool_token_asset_name": self.pool_token_asset.name, + "asset_1_reserves": self.asset_1_reserves, + "asset_2_reserves": self.asset_2_reserves, + "issued_pool_tokens": self.issued_pool_tokens, + "asset_1_protocol_fees": self.asset_1_protocol_fees, + "asset_2_protocol_fees": self.asset_2_protocol_fees, + "total_fee_share": self.total_fee_share, + "protocol_fee_ratio": self.protocol_fee_ratio, + "last_refreshed_round": self.last_refreshed_round, + } + return pool + + def convert(self, amount: AssetAmount): + assert self.issued_pool_tokens + + if amount.asset == self.asset_1: + return AssetAmount(self.asset_2, int(amount.amount * self.asset_1_price)) + elif amount.asset == self.asset_2: + return AssetAmount(self.asset_1, int(amount.amount * self.asset_2_price)) + + def prepare_bootstrap_transactions(self, pooler_address=None): + pooler_address = pooler_address or self.client.user_address + suggested_params = self.client.algod.suggested_params() + + if self.asset_2.id == 0: + pool_minimum_balance = MIN_POOL_BALANCE_ASA_ALGO_PAIR + inner_transaction_count = 5 + else: + pool_minimum_balance = MIN_POOL_BALANCE_ASA_ASA_PAIR + inner_transaction_count = 6 + + app_call_fee = (inner_transaction_count + 1) * suggested_params.min_fee + required_algo = pool_minimum_balance + app_call_fee + required_algo += ( + 100_000 # to fund minimum balance increase because of asset creation + ) + + pool_account_info = self.client.algod.account_info(self.address) + pool_algo_balance = pool_account_info["amount"] + required_algo = max(required_algo - pool_algo_balance, 0) + + txn_group = prepare_bootstrap_transactions( + validator_app_id=self.validator_app_id, + asset_1_id=self.asset_1.id, + asset_2_id=self.asset_2.id, + sender=pooler_address, + suggested_params=suggested_params, + app_call_fee=app_call_fee, + required_algo=required_algo, + ) + return txn_group + + def fetch_add_liquidity_quote( + self, amount_a: AssetAmount, amount_b: AssetAmount = None, slippage=0.05 + ): + if amount_b is None: + amount_b = AssetAmount( + self.asset_2 if amount_a.asset == self.asset_1 else self.asset_1, 0 + ) + + amount_1 = amount_a if amount_a.asset == self.asset_1 else amount_b + amount_2 = amount_a if amount_a.asset == self.asset_2 else amount_b + self.refresh() + + if not self.exists: + raise Exception("Pool has not been bootstrapped yet!") + + if self.issued_pool_tokens: + initial = False + pool_token_asset_amount, internal_swap_quote = get_subsequent_add_liquidity( + pool=self, + asset_1_amount=amount_1.amount if amount_1 else 0, + asset_2_amount=amount_2.amount if amount_2 else 0, + ) + + else: + initial = True + slippage = 0 + pool_token_asset_amount = get_initial_add_liquidity( + asset_1_amount=amount_1.amount, asset_2_amount=amount_2.amount + ) + internal_swap_quote = None + + quote = AddLiquidityQuote( + amounts_in={ + self.asset_1: amount_1, + self.asset_2: amount_2, + }, + pool_token_asset_amount=AssetAmount( + self.pool_token_asset, pool_token_asset_amount + ), + slippage=slippage, + initial=initial, + internal_swap_quote=internal_swap_quote, + ) + return quote + + def prepare_add_liquidity_transactions( + self, + amounts_in: "dict[Asset, AssetAmount]", + min_pool_token_asset_amount: Optional[int], + pooler_address=None, + ): + # TODO: Remove magics + assert self.exists + + pooler_address = pooler_address or self.client.user_address + asset_1_amount = amounts_in[self.asset_1] + asset_2_amount = amounts_in[self.asset_2] + suggested_params = self.client.algod.suggested_params() + + if self.issued_pool_tokens: + assert min_pool_token_asset_amount is not None + + if asset_1_amount.amount and asset_2_amount.amount: + txn_group = prepare_flexible_add_liquidity_transactions( + validator_app_id=self.validator_app_id, + asset_1_id=self.asset_1.id, + asset_2_id=self.asset_2.id, + pool_token_asset_id=self.pool_token_asset.id, + asset_1_amount=asset_1_amount.amount, + asset_2_amount=asset_2_amount.amount, + min_pool_token_asset_amount=min_pool_token_asset_amount, + sender=pooler_address, + suggested_params=suggested_params, + ) + else: + txn_group = prepare_single_asset_add_liquidity_transactions( + validator_app_id=self.validator_app_id, + asset_1_id=self.asset_1.id, + asset_2_id=self.asset_2.id, + pool_token_asset_id=self.pool_token_asset.id, + asset_1_amount=asset_1_amount.amount, + asset_2_amount=asset_2_amount.amount, + min_pool_token_asset_amount=min_pool_token_asset_amount, + sender=pooler_address, + suggested_params=suggested_params, + ) + else: + txn_group = prepare_initial_add_liquidity_transactions( + validator_app_id=self.validator_app_id, + asset_1_id=self.asset_1.id, + asset_2_id=self.asset_2.id, + pool_token_asset_id=self.pool_token_asset.id, + asset_1_amount=asset_1_amount.amount, + asset_2_amount=asset_2_amount.amount, + sender=pooler_address, + suggested_params=suggested_params, + ) + + return txn_group + + def prepare_add_liquidity_transactions_from_quote( + self, quote: AddLiquidityQuote, pooler_address=None + ): + return self.prepare_add_liquidity_transactions( + amounts_in=quote.amounts_in, + min_pool_token_asset_amount=None + if quote.initial + else quote.min_pool_token_asset_amount_with_slippage, + pooler_address=pooler_address, + ) + + def fetch_remove_liquidity_quote(self, pool_token_asset_in, slippage=0.05): + if isinstance(pool_token_asset_in, int): + pool_token_asset_in = AssetAmount( + self.pool_token_asset, pool_token_asset_in + ) + + self.refresh() + ( + asset_1_output_amount, + asset_2_output_amount, + ) = calculate_remove_liquidity_output_amounts( + pool_token_asset_amount=pool_token_asset_in.amount, + asset_1_reserves=self.asset_1_reserves, + asset_2_reserves=self.asset_2_reserves, + issued_pool_tokens=self.issued_pool_tokens, + ) + quote = RemoveLiquidityQuote( + amounts_out={ + self.asset_1: AssetAmount(self.asset_1, asset_1_output_amount), + self.asset_2: AssetAmount(self.asset_2, asset_2_output_amount), + }, + pool_token_asset_amount=pool_token_asset_in, + slippage=slippage, + ) + return quote + + def fetch_single_asset_remove_liquidity_quote( + self, pool_token_asset_in, output_asset, slippage=0.05 + ): + if isinstance(pool_token_asset_in, int): + pool_token_asset_in = AssetAmount( + self.pool_token_asset, pool_token_asset_in + ) + + self.refresh() + ( + asset_1_output_amount, + asset_2_output_amount, + ) = calculate_remove_liquidity_output_amounts( + pool_token_asset_amount=pool_token_asset_in.amount, + asset_1_reserves=self.asset_1_reserves, + asset_2_reserves=self.asset_2_reserves, + issued_pool_tokens=self.issued_pool_tokens, + ) + + if output_asset == self.asset_1: + ( + swap_output_amount, + total_fee_amount, + price_impact, + ) = calculate_fixed_input_swap( + input_supply=self.asset_2_reserves - asset_2_output_amount, + output_supply=self.asset_1_reserves - asset_1_output_amount, + swap_input_amount=asset_2_output_amount, + total_fee_share=self.total_fee_share, + ) + internal_swap_quote = InternalSwapQuote( + amount_in=AssetAmount(self.asset_2, asset_2_output_amount), + amount_out=AssetAmount(self.asset_1, swap_output_amount), + swap_fees=AssetAmount(self.asset_2, int(total_fee_amount)), + price_impact=price_impact, + ) + quote = SingleAssetRemoveLiquidityQuote( + amount_out=AssetAmount( + self.asset_1, asset_1_output_amount + swap_output_amount + ), + pool_token_asset_amount=pool_token_asset_in, + slippage=slippage, + internal_swap_quote=internal_swap_quote, + ) + elif output_asset == self.asset_2: + ( + swap_output_amount, + total_fee_amount, + price_impact, + ) = calculate_fixed_input_swap( + input_supply=self.asset_1_reserves - asset_1_output_amount, + output_supply=self.asset_2_reserves - asset_2_output_amount, + swap_input_amount=asset_1_output_amount, + total_fee_share=self.total_fee_share, + ) + internal_swap_quote = InternalSwapQuote( + amount_in=AssetAmount(self.asset_1, asset_1_output_amount), + amount_out=AssetAmount(self.asset_2, swap_output_amount), + swap_fees=AssetAmount(self.asset_1, int(total_fee_amount)), + price_impact=price_impact, + ) + quote = SingleAssetRemoveLiquidityQuote( + amount_out=AssetAmount( + self.asset_2, asset_2_output_amount + swap_output_amount + ), + pool_token_asset_amount=pool_token_asset_in, + slippage=slippage, + internal_swap_quote=internal_swap_quote, + ) + else: + assert False + + return quote + + def prepare_remove_liquidity_transactions( + self, pool_token_asset_amount: AssetAmount, amounts_out, pooler_address=None + ): + if isinstance(pool_token_asset_amount, int): + pool_token_asset_amount = AssetAmount( + self.pool_token_asset, pool_token_asset_amount + ) + + pooler_address = pooler_address or self.client.user_address + asset_1_amount = amounts_out[self.asset_1] + asset_2_amount = amounts_out[self.asset_2] + suggested_params = self.client.algod.suggested_params() + txn_group = prepare_remove_liquidity_transactions( + validator_app_id=self.validator_app_id, + asset_1_id=self.asset_1.id, + asset_2_id=self.asset_2.id, + pool_token_asset_id=self.pool_token_asset.id, + min_asset_1_amount=asset_1_amount.amount, + min_asset_2_amount=asset_2_amount.amount, + pool_token_asset_amount=pool_token_asset_amount.amount, + sender=pooler_address, + suggested_params=suggested_params, + ) + return txn_group + + def prepare_single_asset_remove_liquidity_transactions( + self, + pool_token_asset_amount: AssetAmount, + amount_out: AssetAmount, + pooler_address: Optional[str] = None, + ): + if isinstance(pool_token_asset_amount, int): + pool_token_asset_amount = AssetAmount( + self.pool_token_asset, pool_token_asset_amount + ) + + pooler_address = pooler_address or self.client.user_address + suggested_params = self.client.algod.suggested_params() + txn_group = prepare_single_asset_remove_liquidity_transactions( + validator_app_id=self.validator_app_id, + asset_1_id=self.asset_1.id, + asset_2_id=self.asset_2.id, + pool_token_asset_id=self.pool_token_asset.id, + output_asset_id=amount_out.asset.id, + min_output_asset_amount=amount_out.amount, + pool_token_asset_amount=pool_token_asset_amount.amount, + sender=pooler_address, + suggested_params=suggested_params, + ) + return txn_group + + def prepare_remove_liquidity_transactions_from_quote( + self, + quote: [RemoveLiquidityQuote, SingleAssetRemoveLiquidityQuote], + pooler_address=None, + ): + pooler_address = pooler_address or self.client.user_address + + if isinstance(quote, SingleAssetRemoveLiquidityQuote): + return self.prepare_single_asset_remove_liquidity_transactions( + pool_token_asset_amount=quote.pool_token_asset_amount, + amount_out=quote.amount_out_with_slippage, + pooler_address=pooler_address, + ) + elif isinstance(quote, RemoveLiquidityQuote): + return self.prepare_remove_liquidity_transactions( + pool_token_asset_amount=quote.pool_token_asset_amount, + amounts_out={ + self.asset_1: quote.amounts_out_with_slippage[self.asset_1], + self.asset_2: quote.amounts_out_with_slippage[self.asset_2], + }, + pooler_address=pooler_address, + ) + + assert False + + def fetch_fixed_input_swap_quote( + self, amount_in: AssetAmount, slippage=0.05 + ) -> SwapQuote: + self.refresh() + assert self.issued_pool_tokens + + if amount_in.asset == self.asset_1: + asset_out = self.asset_2 + input_supply = self.asset_1_reserves + output_supply = self.asset_2_reserves + elif amount_in.asset == self.asset_1: + asset_out = self.asset_1 + input_supply = self.asset_2_reserves + output_supply = self.asset_1_reserves + else: + raise False + + if not input_supply or not output_supply: + raise Exception("Pool has no liquidity!") + + swap_output_amount, total_fee_amount, price_impact = calculate_fixed_input_swap( + input_supply=input_supply, + output_supply=output_supply, + swap_input_amount=amount_in.amount, + total_fee_share=self.total_fee_share, + ) + amount_out = AssetAmount(asset_out, swap_output_amount) + + quote = SwapQuote( + swap_type="fixed-input", + amount_in=amount_in, + amount_out=amount_out, + swap_fees=AssetAmount(amount_in.asset, total_fee_amount), + slippage=slippage, + price_impact=price_impact, + ) + return quote + + def fetch_fixed_output_swap_quote( + self, amount_out: AssetAmount, slippage=0.05 + ) -> SwapQuote: + self.refresh() + assert self.issued_pool_tokens + + if amount_out.asset == self.asset_1: + asset_in = self.asset_2 + input_supply = self.asset_2_reserves + output_supply = self.asset_1_reserves + elif amount_out.asset == self.asset_2: + asset_in = self.asset_1 + input_supply = self.asset_1_reserves + output_supply = self.asset_2_reserves + else: + assert False + + swap_input_amount, total_fee_amount, price_impact = calculate_fixed_output_swap( + input_supply=input_supply, + output_supply=output_supply, + swap_output_amount=amount_out.amount, + total_fee_share=self.total_fee_share, + ) + amount_in = AssetAmount(asset_in, swap_input_amount) + + quote = SwapQuote( + swap_type="fixed-output", + amount_out=amount_out, + amount_in=amount_in, + swap_fees=AssetAmount(amount_in.asset, total_fee_amount), + slippage=slippage, + price_impact=price_impact, + ) + return quote + + def prepare_swap_transactions( + self, + amount_in: AssetAmount, + amount_out: AssetAmount, + swap_type, + swapper_address=None, + ): + swapper_address = swapper_address or self.client.user_address + suggested_params = self.client.algod.suggested_params() + + txn_group = prepare_swap_transactions( + validator_app_id=self.validator_app_id, + asset_1_id=self.asset_1.id, + asset_2_id=self.asset_2.id, + asset_in_id=amount_in.asset.id, + asset_in_amount=amount_in.amount, + asset_out_amount=amount_out.amount, + swap_type=swap_type, + sender=swapper_address, + suggested_params=suggested_params, + ) + return txn_group + + def prepare_swap_transactions_from_quote( + self, quote: SwapQuote, swapper_address=None + ): + return self.prepare_swap_transactions( + amount_in=quote.amount_in_with_slippage, + amount_out=quote.amount_out_with_slippage, + swap_type=quote.swap_type, + swapper_address=swapper_address, + ) + + def prepare_pool_token_asset_optin_transactions(self, user_address=None): + user_address = user_address or self.client.user_address + suggested_params = self.client.algod.suggested_params() + txn_group = prepare_asset_optin_transactions( + asset_id=self.pool_token_asset.id, + sender=user_address, + suggested_params=suggested_params, + ) + return txn_group + + def fetch_pool_position(self, pooler_address=None): + pooler_address = pooler_address or self.client.user_address + account_info = self.client.algod.account_info(pooler_address) + assets = {a["asset-id"]: a for a in account_info["assets"]} + pool_token_asset_amount = assets.get(self.pool_token_asset.id, {}).get( + "amount", 0 + ) + quote = self.fetch_remove_liquidity_quote(pool_token_asset_amount) + return { + self.asset_1: quote.amounts_out[self.asset_1], + self.asset_2: quote.amounts_out[self.asset_2], + self.pool_token_asset: quote.pool_token_asset_amount, + "share": (pool_token_asset_amount / self.issued_pool_tokens), + } diff --git a/tinyman/v2/quotes.py b/tinyman/v2/quotes.py new file mode 100644 index 0000000..b7a527e --- /dev/null +++ b/tinyman/v2/quotes.py @@ -0,0 +1,103 @@ +import math +from dataclasses import dataclass + +from tinyman.assets import AssetAmount, Asset + + +@dataclass +class SwapQuote: + swap_type: str + amount_in: AssetAmount + amount_out: AssetAmount + swap_fees: AssetAmount + slippage: float + price_impact: float + + @property + def amount_out_with_slippage(self) -> AssetAmount: + if self.swap_type == "fixed-output": + return self.amount_out + + amount_with_slippage = self.amount_out.amount - int( + self.amount_out.amount * self.slippage + ) + return AssetAmount(self.amount_out.asset, amount_with_slippage) + + @property + def amount_in_with_slippage(self) -> AssetAmount: + if self.swap_type == "fixed-input": + return self.amount_in + + amount_with_slippage = self.amount_in.amount + int( + self.amount_in.amount * self.slippage + ) + return AssetAmount(self.amount_in.asset, amount_with_slippage) + + @property + def price(self) -> float: + return self.amount_out.amount / self.amount_in.amount + + @property + def price_with_slippage(self) -> float: + return ( + self.amount_out_with_slippage.amount / self.amount_in_with_slippage.amount + ) + + +@dataclass +class InternalSwapQuote: + amount_in: AssetAmount + amount_out: AssetAmount + swap_fees: AssetAmount + price_impact: float + + @property + def price(self) -> float: + return self.amount_out.amount / self.amount_in.amount + + +@dataclass +class AddLiquidityQuote: + amounts_in: dict[Asset, AssetAmount] + pool_token_asset_amount: AssetAmount + slippage: float + initial: bool = False + internal_swap_quote: InternalSwapQuote = None + + @property + def min_pool_token_asset_amount_with_slippage(self) -> int: + return self.pool_token_asset_amount.amount - math.ceil( + self.pool_token_asset_amount.amount * self.slippage + ) + + +@dataclass +class RemoveLiquidityQuote: + amounts_out: dict[Asset, AssetAmount] + pool_token_asset_amount: AssetAmount + slippage: float + + @property + def amounts_out_with_slippage(self) -> dict[Asset, AssetAmount]: + out = {} + for asset, asset_amount in self.amounts_out.items(): + amount_with_slippage = asset_amount.amount - int( + (asset_amount.amount * self.slippage) + ) + out[asset] = AssetAmount(asset, amount_with_slippage) + return out + + +@dataclass +class SingleAssetRemoveLiquidityQuote: + amount_out: AssetAmount + pool_token_asset_amount: AssetAmount + slippage: float + internal_swap_quote: InternalSwapQuote = None + + @property + def amount_out_with_slippage(self) -> AssetAmount: + amount_with_slippage = self.amount_out.amount - int( + self.amount_out.amount * self.slippage + ) + return AssetAmount(self.amount_out.asset, amount_with_slippage) diff --git a/tinyman/v2/remove_liquidity.py b/tinyman/v2/remove_liquidity.py new file mode 100644 index 0000000..a666ad6 --- /dev/null +++ b/tinyman/v2/remove_liquidity.py @@ -0,0 +1,108 @@ +from algosdk.future.transaction import ( + ApplicationNoOpTxn, + AssetTransferTxn, + SuggestedParams, +) + +from tinyman.utils import TransactionGroup +from .constants import REMOVE_LIQUIDITY_APP_ARGUMENT +from .contracts import get_pool_logicsig + + +def prepare_remove_liquidity_transactions( + validator_app_id: int, + asset_1_id: int, + asset_2_id: int, + pool_token_asset_id: int, + min_asset_1_amount: int, + min_asset_2_amount: int, + pool_token_asset_amount: int, + sender: str, + suggested_params: SuggestedParams, +): + pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) + pool_address = pool_logicsig.address() + + txns = [ + AssetTransferTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + index=pool_token_asset_id, + amt=pool_token_asset_amount, + ), + ApplicationNoOpTxn( + sender=sender, + sp=suggested_params, + index=validator_app_id, + app_args=[ + REMOVE_LIQUIDITY_APP_ARGUMENT, + min_asset_1_amount, + min_asset_2_amount, + ], + foreign_assets=[asset_1_id, asset_2_id], + accounts=[pool_address], + ), + ] + + # App call contains 2 inner transactions + min_fee = suggested_params.min_fee + app_call_fee = min_fee * 3 + txns[-1].fee = app_call_fee + + txn_group = TransactionGroup(txns) + return txn_group + + +def prepare_single_asset_remove_liquidity_transactions( + validator_app_id: int, + asset_1_id: int, + asset_2_id: int, + pool_token_asset_id: int, + output_asset_id: int, + min_output_asset_amount: int, + pool_token_asset_amount: int, + sender: str, + suggested_params: SuggestedParams, +): + pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) + pool_address = pool_logicsig.address() + + if output_asset_id == asset_1_id: + min_asset_1_amount = min_output_asset_amount + min_asset_2_amount = 0 + elif output_asset_id == asset_2_id: + min_asset_1_amount = 0 + min_asset_2_amount = min_output_asset_amount + else: + assert False + + txns = [ + AssetTransferTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + index=pool_token_asset_id, + amt=pool_token_asset_amount, + ), + ApplicationNoOpTxn( + sender=sender, + sp=suggested_params, + index=validator_app_id, + app_args=[ + REMOVE_LIQUIDITY_APP_ARGUMENT, + min_asset_1_amount, + min_asset_2_amount, + ], + foreign_assets=[output_asset_id], + accounts=[pool_address], + ), + ] + + # App call contains 2 inner transactions + min_fee = suggested_params.min_fee + app_call_fee = min_fee * 3 + txns[-1].fee = app_call_fee + + txn_group = TransactionGroup(txns) + return txn_group diff --git a/tinyman/v2/swap.py b/tinyman/v2/swap.py new file mode 100644 index 0000000..2eb0a97 --- /dev/null +++ b/tinyman/v2/swap.py @@ -0,0 +1,75 @@ +from algosdk.future.transaction import ( + ApplicationNoOpTxn, + PaymentTxn, + AssetTransferTxn, + SuggestedParams, +) + +from tinyman.utils import TransactionGroup +from .constants import ( + SWAP_APP_ARGUMENT, + FIXED_INPUT_APP_ARGUMENT, + FIXED_OUTPUT_APP_ARGUMENT, +) +from .contracts import get_pool_logicsig + + +def prepare_swap_transactions( + validator_app_id: int, + asset_1_id: int, + asset_2_id: int, + asset_in_id: int, + asset_in_amount: int, + asset_out_amount: int, + swap_type: [str, bytes], + sender: str, + suggested_params: SuggestedParams, +): + pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) + pool_address = pool_logicsig.address() + + txns = [ + AssetTransferTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + index=asset_in_id, + amt=int(asset_in_amount), + ) + if asset_in_id != 0 + else PaymentTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + amt=int(asset_in_amount), + ), + ApplicationNoOpTxn( + sender=sender, + sp=suggested_params, + index=validator_app_id, + app_args=[SWAP_APP_ARGUMENT, swap_type, int(asset_out_amount)], + foreign_assets=[asset_1_id, asset_2_id], + accounts=[pool_address], + ), + ] + + 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 contains 1 inner transaction + app_call_fee = min_fee * 2 + elif swap_type == FIXED_OUTPUT_APP_ARGUMENT: + # App call contains 2 inner transaction2 + app_call_fee = min_fee * 3 + else: + raise NotImplementedError() + + txns[-1].fee = app_call_fee + txn_group = TransactionGroup(txns) + return txn_group diff --git a/tinyman/v2/utils.py b/tinyman/v2/utils.py new file mode 100644 index 0000000..9a3e874 --- /dev/null +++ b/tinyman/v2/utils.py @@ -0,0 +1,16 @@ +from base64 import b64decode + + +def decode_logs(logs): + decoded_logs = dict() + for log in logs: + if type(log) == str: + log = b64decode(log.encode()) + if b"%i" in log: + i = log.index(b"%i") + s = log[0:i].decode() + value = int.from_bytes(log[i + 2 :], "big") + decoded_logs[s] = value + else: + raise NotImplementedError() + return decoded_logs From 599ba6635fccf53ba3f477ee388e46b5cb23125e Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Mon, 3 Oct 2022 16:00:11 +0300 Subject: [PATCH 33/73] improve add liquidity functions --- .gitignore | 4 +- examples/v2/tutorial/02_create_assets.py | 7 + examples/v2/tutorial/03_bootstrap_pool.py | 10 +- .../v2/tutorial/04_add_initial_liquidity.py | 28 +- .../v2/tutorial/05_add_flexible_liquidity.py | 15 +- .../v2/tutorial/06_add_single_liquidity.py | 14 +- examples/v2/tutorial/07_remove_liquidity.py | 8 +- .../08_single_asset_remove_liquidity.py | 10 +- examples/v2/tutorial/09_fixed_input_swap.py | 6 +- examples/v2/tutorial/10_fixed_output_swap.py | 6 +- tinyman/v2/add_liquidity.py | 8 +- tinyman/v2/bootstrap.py | 2 +- tinyman/v2/contracts.py | 4 +- tinyman/v2/exceptions.py | 14 + tinyman/v2/formulas.py | 90 ++-- tinyman/v2/pools.py | 448 +++++++++++++----- tinyman/v2/quotes.py | 23 +- tinyman/v2/remove_liquidity.py | 4 +- tinyman/v2/swap.py | 2 +- tinyman/v2/utils.py | 2 +- 20 files changed, 462 insertions(+), 243 deletions(-) create mode 100644 tinyman/v2/exceptions.py diff --git a/.gitignore b/.gitignore index ad886c2..ad18a6c 100644 --- a/.gitignore +++ b/.gitignore @@ -134,5 +134,5 @@ dmypy.json # TODO: Remove tinyman/v2/asc.json tinyman/v2/constants.py -account.json -assets.json +account*.json +assets*.json diff --git a/examples/v2/tutorial/02_create_assets.py b/examples/v2/tutorial/02_create_assets.py index de52f8b..cb96611 100644 --- a/examples/v2/tutorial/02_create_assets.py +++ b/examples/v2/tutorial/02_create_assets.py @@ -27,6 +27,13 @@ algod = get_algod() client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) +account_info = algod.account_info(account["address"]) +if not account_info["amount"]: + print( + f"Go to https://bank.testnet.algorand.network/?account={account['address']} and fund your account." + ) + exit(1) + ASSET_A_ID = create_asset(algod, account["address"], account["private_key"]) ASSET_B_ID = create_asset(algod, account["address"], account["private_key"]) diff --git a/examples/v2/tutorial/03_bootstrap_pool.py b/examples/v2/tutorial/03_bootstrap_pool.py index 8290c7b..f1ab5cd 100644 --- a/examples/v2/tutorial/03_bootstrap_pool.py +++ b/examples/v2/tutorial/03_bootstrap_pool.py @@ -12,18 +12,24 @@ algod = get_algod() client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) +# Fetch assets ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] - ASSET_A = client.fetch_asset(ASSET_A_ID) ASSET_B = client.fetch_asset(ASSET_B_ID) - +# Fetch the pool pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) print(pool) +# Get transaction group txn_group = pool.prepare_bootstrap_transactions() + +# Sign txn_group.sign_with_private_key(account["address"], account["private_key"]) + +# Submit transactions to the network and wait for confirmation txinfo = txn_group.submit(algod, wait=True) + print("Transaction Info") pprint(txinfo) diff --git a/examples/v2/tutorial/04_add_initial_liquidity.py b/examples/v2/tutorial/04_add_initial_liquidity.py index 5b0d77e..86fb6ce 100644 --- a/examples/v2/tutorial/04_add_initial_liquidity.py +++ b/examples/v2/tutorial/04_add_initial_liquidity.py @@ -19,32 +19,26 @@ ASSET_B = client.fetch_asset(ASSET_B_ID) pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) -# Bootstrap the pool -txn_group = pool.prepare_bootstrap_transactions() -txn_group.sign_with_private_key(account["address"], account["private_key"]) -txn_group.submit(algod, wait=True) - -# Refresh the pool and get pool token asset id -pool.refresh() - # Opt-in to the pool token txn_group_1 = pool.prepare_pool_token_asset_optin_transactions() -# Add initial liquidity -txn_group_2 = pool.prepare_add_liquidity_transactions( - amounts_in={ - pool.asset_1: AssetAmount(pool.asset_1, 10_000_000), - pool.asset_2: AssetAmount(pool.asset_2, 10_000_000), - }, - min_pool_token_asset_amount=None, +# Get initial add liquidity quote +quote = pool.fetch_initial_add_liquidity_quote( + amount_a=AssetAmount(pool.asset_1, 10_000_000), + amount_b=AssetAmount(pool.asset_2, 10_000_000), ) +print("Quote:") +print(quote) + +txn_group_2 = pool.prepare_add_liquidity_transactions_from_quote(quote) # You can merge the transaction groups txn_group = txn_group_1 + txn_group_2 -# Submit +# Sign txn_group.sign_with_private_key(account["address"], account["private_key"]) +# Submit transactions to the network and wait for confirmation txinfo = txn_group.submit(algod, wait=True) print("Transaction Info") pprint(txinfo) @@ -56,6 +50,6 @@ pool.refresh() pool_position = pool.fetch_pool_position() share = pool_position["share"] * 100 -print(f"Pool Tokens: {pool_position[pool.liquidity_asset]}") +print(f"Pool Tokens: {pool_position[pool.pool_token_asset]}") print(f"Assets: {pool_position[ASSET_A]}, {pool_position[ASSET_B]}") print(f"Share of pool: {share:.3f}%") diff --git a/examples/v2/tutorial/05_add_flexible_liquidity.py b/examples/v2/tutorial/05_add_flexible_liquidity.py index a007f4e..9e60d80 100644 --- a/examples/v2/tutorial/05_add_flexible_liquidity.py +++ b/examples/v2/tutorial/05_add_flexible_liquidity.py @@ -19,9 +19,6 @@ ASSET_B = client.fetch_asset(ASSET_B_ID) pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) -assert pool.exists, "Pool has not been bootstrapped yet!" -assert pool.issued_pool_tokens, "Pool has no liquidity" - # Add flexible liquidity (advanced) # txn_group = pool.prepare_add_liquidity_transactions( # amounts_in={ @@ -31,18 +28,14 @@ # min_pool_token_asset_amount="Do your own calculation" # ) -quote = pool.fetch_add_liquidity_quote( +quote = pool.fetch_flexible_add_liquidity_quote( amount_a=AssetAmount(pool.asset_1, 10_000_000), amount_b=AssetAmount(pool.asset_2, 5_000_000), - slippage=0, # TODO: 0.05 ) -print("\nAdd Liquidity Quote:") +print("\nQuote:") print(quote) -print("\nInternal Swap Quote:") -print(quote.internal_swap_quote) - txn_group = pool.prepare_add_liquidity_transactions_from_quote(quote=quote) if not client.asset_is_opted_in(asset_id=pool.pool_token_asset.id): @@ -54,7 +47,7 @@ # Sign txn_group.sign_with_private_key(account["address"], account["private_key"]) -# Submit +# Submit transactions to the network and wait for confirmation txinfo = txn_group.submit(algod, wait=True) print("Transaction Info") pprint(txinfo) @@ -66,6 +59,6 @@ pool.refresh() pool_position = pool.fetch_pool_position() share = pool_position["share"] * 100 -print(f"Pool Tokens: {pool_position[pool.liquidity_asset]}") +print(f"Pool Tokens: {pool_position[pool.pool_token_asset]}") print(f"Assets: {pool_position[ASSET_A]}, {pool_position[ASSET_B]}") print(f"Share of pool: {share:.3f}%") diff --git a/examples/v2/tutorial/06_add_single_liquidity.py b/examples/v2/tutorial/06_add_single_liquidity.py index 611a6ca..a5e0bb1 100644 --- a/examples/v2/tutorial/06_add_single_liquidity.py +++ b/examples/v2/tutorial/06_add_single_liquidity.py @@ -19,9 +19,6 @@ ASSET_B = client.fetch_asset(ASSET_B_ID) pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) -assert pool.exists, "Pool has not been bootstrapped yet!" -assert pool.issued_pool_tokens, "Pool has no liquidity" - # Add flexible liquidity (advanced) # txn_group = pool.prepare_add_liquidity_transactions( # amounts_in={ @@ -31,17 +28,13 @@ # min_pool_token_asset_amount="Do your own calculation" # ) -quote = pool.fetch_add_liquidity_quote( +quote = pool.fetch_single_asset_add_liquidity_quote( amount_a=AssetAmount(pool.asset_1, 10_000_000), - slippage=0, # TODO: 0.05 ) print("\nAdd Liquidity Quote:") print(quote) -print("\nInternal Swap Quote:") -print(quote.internal_swap_quote) - txn_group = pool.prepare_add_liquidity_transactions_from_quote(quote=quote) if not client.asset_is_opted_in(asset_id=pool.pool_token_asset.id): @@ -53,7 +46,7 @@ # Sign txn_group.sign_with_private_key(account["address"], account["private_key"]) -# Submit +# Submit transactions to the network and wait for confirmation txinfo = txn_group.submit(algod, wait=True) print("Transaction Info") pprint(txinfo) @@ -63,9 +56,8 @@ ) pool.refresh() - pool_position = pool.fetch_pool_position() share = pool_position["share"] * 100 -print(f"Pool Tokens: {pool_position[pool.liquidity_asset]}") +print(f"Pool Tokens: {pool_position[pool.pool_token_asset]}") print(f"Assets: {pool_position[ASSET_A]}, {pool_position[ASSET_B]}") print(f"Share of pool: {share:.3f}%") diff --git a/examples/v2/tutorial/07_remove_liquidity.py b/examples/v2/tutorial/07_remove_liquidity.py index aa86356..2f17c21 100644 --- a/examples/v2/tutorial/07_remove_liquidity.py +++ b/examples/v2/tutorial/07_remove_liquidity.py @@ -17,15 +17,11 @@ ASSET_B = client.fetch_asset(ASSET_B_ID) pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) -assert pool.exists, "Pool has not been bootstrapped yet!" -assert pool.issued_pool_tokens, "Pool has no liquidity" - position = pool.fetch_pool_position() pool_token_asset_in = position[pool.pool_token_asset].amount // 4 quote = pool.fetch_remove_liquidity_quote( pool_token_asset_in=pool_token_asset_in, - slippage=0, # TODO: 0.05 ) print("\nRemove Liquidity Quote:") @@ -36,7 +32,7 @@ # Sign txn_group.sign_with_private_key(account["address"], account["private_key"]) -# Submit +# Submit transactions to the network and wait for confirmation txinfo = txn_group.submit(algod, wait=True) print("Transaction Info") pprint(txinfo) @@ -49,6 +45,6 @@ pool_position = pool.fetch_pool_position() share = pool_position["share"] * 100 -print(f"Pool Tokens: {pool_position[pool.liquidity_asset]}") +print(f"Pool Tokens: {pool_position[pool.pool_token_asset]}") print(f"Assets: {pool_position[ASSET_A]}, {pool_position[ASSET_B]}") print(f"Share of pool: {share:.3f}%") diff --git a/examples/v2/tutorial/08_single_asset_remove_liquidity.py b/examples/v2/tutorial/08_single_asset_remove_liquidity.py index f2ec10b..291edc0 100644 --- a/examples/v2/tutorial/08_single_asset_remove_liquidity.py +++ b/examples/v2/tutorial/08_single_asset_remove_liquidity.py @@ -17,9 +17,6 @@ ASSET_B = client.fetch_asset(ASSET_B_ID) pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) -assert pool.exists, "Pool has not been bootstrapped yet!" -assert pool.issued_pool_tokens, "Pool has no liquidity" - position = pool.fetch_pool_position() pool_token_asset_in = position[pool.pool_token_asset].amount // 8 @@ -32,15 +29,12 @@ print("\nSingle Asset Remove Liquidity Quote:") print(quote) -print("\nInternal Swap Quote:") -print(quote.internal_swap_quote) - txn_group = pool.prepare_remove_liquidity_transactions_from_quote(quote=quote) # Sign txn_group.sign_with_private_key(account["address"], account["private_key"]) -# Submit +# Submit transactions to the network and wait for confirmation txinfo = txn_group.submit(algod, wait=True) print("Transaction Info") pprint(txinfo) @@ -52,6 +46,6 @@ pool.refresh() pool_position = pool.fetch_pool_position() share = pool_position["share"] * 100 -print(f"Pool Tokens: {pool_position[pool.liquidity_asset]}") +print(f"Pool Tokens: {pool_position[pool.pool_token_asset]}") print(f"Assets: {pool_position[ASSET_A]}, {pool_position[ASSET_B]}") print(f"Share of pool: {share:.3f}%") diff --git a/examples/v2/tutorial/09_fixed_input_swap.py b/examples/v2/tutorial/09_fixed_input_swap.py index 9a2c96f..0f2a5fe 100644 --- a/examples/v2/tutorial/09_fixed_input_swap.py +++ b/examples/v2/tutorial/09_fixed_input_swap.py @@ -19,15 +19,11 @@ ASSET_B = client.fetch_asset(ASSET_B_ID) pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) -assert pool.exists, "Pool has not been bootstrapped yet!" -assert pool.issued_pool_tokens, "Pool has no liquidity" - position = pool.fetch_pool_position() amount_in = AssetAmount(pool.asset_1, 1_000_000) quote = pool.fetch_fixed_input_swap_quote( amount_in=amount_in, - slippage=0, # TODO: 0.05 ) print("\nSwap Quote:") @@ -38,7 +34,7 @@ # Sign txn_group.sign_with_private_key(account["address"], account["private_key"]) -# Submit +# Submit transactions to the network and wait for confirmation txinfo = txn_group.submit(algod, wait=True) print("Transaction Info") pprint(txinfo) diff --git a/examples/v2/tutorial/10_fixed_output_swap.py b/examples/v2/tutorial/10_fixed_output_swap.py index 638c959..4a1dc31 100644 --- a/examples/v2/tutorial/10_fixed_output_swap.py +++ b/examples/v2/tutorial/10_fixed_output_swap.py @@ -19,15 +19,11 @@ ASSET_B = client.fetch_asset(ASSET_B_ID) pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) -assert pool.exists, "Pool has not been bootstrapped yet!" -assert pool.issued_pool_tokens, "Pool has no liquidity" - position = pool.fetch_pool_position() amount_out = AssetAmount(pool.asset_1, 1_000_000) quote = pool.fetch_fixed_output_swap_quote( amount_out=amount_out, - slippage=0, # TODO: 0.05 ) print("\nSwap Quote:") @@ -38,7 +34,7 @@ # Sign txn_group.sign_with_private_key(account["address"], account["private_key"]) -# Submit +# Submit transactions to the network and wait for confirmation txinfo = txn_group.submit(algod, wait=True) print("Transaction Info") pprint(txinfo) diff --git a/tinyman/v2/add_liquidity.py b/tinyman/v2/add_liquidity.py index 8f6b030..39a555e 100644 --- a/tinyman/v2/add_liquidity.py +++ b/tinyman/v2/add_liquidity.py @@ -27,7 +27,7 @@ def prepare_flexible_add_liquidity_transactions( min_pool_token_asset_amount: int, sender: str, suggested_params: SuggestedParams, -): +) -> TransactionGroup: pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) pool_address = pool_logicsig.address() @@ -85,7 +85,7 @@ def prepare_single_asset_add_liquidity_transactions( suggested_params: SuggestedParams, asset_1_amount: Optional[int] = None, asset_2_amount: Optional[int] = None, -): +) -> TransactionGroup: pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) pool_address = pool_logicsig.address() @@ -102,7 +102,7 @@ def prepare_single_asset_add_liquidity_transactions( asset_in_amount = asset_2_amount else: - raise Exception("Invalid asset_1_amount and asset_2_amount") + assert False txns = [ AssetTransferTxn( @@ -150,7 +150,7 @@ def prepare_initial_add_liquidity_transactions( asset_2_amount: int, sender: str, suggested_params: SuggestedParams, -): +) -> TransactionGroup: pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) pool_address = pool_logicsig.address() diff --git a/tinyman/v2/bootstrap.py b/tinyman/v2/bootstrap.py index 279b11e..2bba469 100644 --- a/tinyman/v2/bootstrap.py +++ b/tinyman/v2/bootstrap.py @@ -18,7 +18,7 @@ def prepare_bootstrap_transactions( app_call_fee: int, required_algo: int, suggested_params: SuggestedParams, -): +) -> TransactionGroup: pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) pool_address = pool_logicsig.address() assert asset_1_id > asset_2_id diff --git a/tinyman/v2/contracts.py b/tinyman/v2/contracts.py index 0e44d57..69614a0 100644 --- a/tinyman/v2/contracts.py +++ b/tinyman/v2/contracts.py @@ -14,7 +14,9 @@ # validator_app_def = _contracts["contracts"]["validator_app"] -def get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id): +def get_pool_logicsig( + validator_app_id: int, asset_1_id: int, asset_2_id: int +) -> LogicSigAccount: assets = [asset_1_id, asset_2_id] asset_1_id = max(assets) asset_2_id = min(assets) diff --git a/tinyman/v2/exceptions.py b/tinyman/v2/exceptions.py new file mode 100644 index 0000000..97f122b --- /dev/null +++ b/tinyman/v2/exceptions.py @@ -0,0 +1,14 @@ +class BootstrapIsRequired(Exception): + pass + + +class AlreadyBootstrapped(Exception): + pass + + +class PoolHasNoLiquidity(Exception): + pass + + +class PoolAlreadyHasLiquidity(Exception): + pass diff --git a/tinyman/v2/formulas.py b/tinyman/v2/formulas.py index f78d1e2..394feed 100644 --- a/tinyman/v2/formulas.py +++ b/tinyman/v2/formulas.py @@ -1,9 +1,7 @@ import math -from tinyman.assets import AssetAmount from tinyman.utils import calculate_price_impact from tinyman.v2.constants import LOCKED_POOL_TOKENS -from tinyman.v2.quotes import InternalSwapQuote def calculate_protocol_fee_amount( @@ -32,14 +30,14 @@ def calculate_fixed_output_fee_amounts(swap_amount: int, total_fee_share: int) - return total_fee_amount -def get_internal_swap_fee_amount(swap_amount, total_fee_share) -> int: +def calculate_internal_swap_fee_amount(swap_amount: int, total_fee_share: int) -> int: total_fee_amount = int((swap_amount * total_fee_share) / (10_000 - total_fee_share)) return total_fee_amount -def get_initial_add_liquidity(asset_1_amount, asset_2_amount) -> int: - assert ( - not asset_1_amount or not asset_2_amount +def calculate_initial_add_liquidity(asset_1_amount: int, asset_2_amount: int) -> int: + assert bool(asset_1_amount) and bool( + asset_2_amount ), "Both assets are required for the initial add liquidity" pool_token_asset_amount = ( @@ -49,7 +47,10 @@ def get_initial_add_liquidity(asset_1_amount, asset_2_amount) -> int: def calculate_remove_liquidity_output_amounts( - pool_token_asset_amount, asset_1_reserves, asset_2_reserves, issued_pool_tokens + pool_token_asset_amount: int, + asset_1_reserves: int, + asset_2_reserves: int, + issued_pool_tokens: int, ) -> (int, int): asset_1_output_amount = int( (pool_token_asset_amount * asset_1_reserves) / issued_pool_tokens @@ -60,17 +61,25 @@ def calculate_remove_liquidity_output_amounts( return asset_1_output_amount, asset_2_output_amount -def get_subsequent_add_liquidity(pool, asset_1_amount, asset_2_amount): - # TODO: Remove pool input and don't return quote here. - old_k = pool.asset_1_reserves * pool.asset_2_reserves - new_asset_1_reserves = pool.asset_1_reserves + asset_1_amount - new_asset_2_reserves = pool.asset_2_reserves + asset_2_amount +def calculate_subsequent_add_liquidity( + asset_1_reserves: int, + asset_2_reserves: int, + issued_pool_tokens: int, + total_fee_share: int, + asset_1_amount: int, + asset_2_amount: int, +) -> (int, bool, int, int, int, float): + assert asset_1_reserves and asset_2_reserves and issued_pool_tokens + + old_k = asset_1_reserves * asset_2_reserves + new_asset_1_reserves = asset_1_reserves + asset_1_amount + new_asset_2_reserves = asset_2_reserves + asset_2_amount new_k = new_asset_1_reserves * new_asset_2_reserves new_issued_pool_tokens = int( - math.sqrt(int((new_k * (pool.issued_pool_tokens**2)) / old_k)) + math.sqrt(int((new_k * (issued_pool_tokens**2)) / old_k)) ) - pool_token_asset_amount = new_issued_pool_tokens - pool.issued_pool_tokens + pool_token_asset_amount = new_issued_pool_tokens - issued_pool_tokens calculated_asset_1_amount = int( (pool_token_asset_amount * new_asset_1_reserves) / new_issued_pool_tokens ) @@ -84,54 +93,51 @@ def get_subsequent_add_liquidity(pool, asset_1_amount, asset_2_amount): if asset_1_swap_amount > asset_2_swap_amount: swap_in_amount_without_fee = asset_1_swap_amount swap_out_amount = -min(asset_2_swap_amount, 0) - swap_in_asset = pool.asset_1 - swap_out_asset = pool.asset_2 + swap_from_asset_1_to_asset_2 = True - total_fee_amount = get_internal_swap_fee_amount( + swap_total_fee_amount = calculate_internal_swap_fee_amount( swap_in_amount_without_fee, - pool.total_fee_share, + total_fee_share, ) fee_as_pool_tokens = int( - total_fee_amount * new_issued_pool_tokens / (new_asset_1_reserves * 2) + swap_total_fee_amount * new_issued_pool_tokens / (new_asset_1_reserves * 2) ) - swap_in_amount = swap_in_amount_without_fee + total_fee_amount + swap_in_amount = swap_in_amount_without_fee + swap_total_fee_amount pool_token_asset_amount = pool_token_asset_amount - fee_as_pool_tokens else: swap_in_amount_without_fee = asset_2_swap_amount swap_out_amount = -min(asset_1_swap_amount, 0) - swap_in_asset = pool.asset_2 - swap_out_asset = pool.asset_1 + swap_from_asset_1_to_asset_2 = False - total_fee_amount = get_internal_swap_fee_amount( + swap_total_fee_amount = calculate_internal_swap_fee_amount( swap_in_amount_without_fee, - pool.total_fee_share, + total_fee_share, ) fee_as_pool_tokens = int( - total_fee_amount * new_issued_pool_tokens / (new_asset_2_reserves * 2) + swap_total_fee_amount * new_issued_pool_tokens / (new_asset_2_reserves * 2) ) - swap_in_amount = swap_in_amount_without_fee + total_fee_amount + swap_in_amount = swap_in_amount_without_fee + swap_total_fee_amount pool_token_asset_amount = pool_token_asset_amount - fee_as_pool_tokens - price_impact = calculate_price_impact( - input_supply=pool.asset_1_reserves - if swap_in_asset == pool.asset_1 - else pool.asset_2_reserves, - output_supply=pool.asset_1_reserves - if swap_out_asset == pool.asset_1 - else pool.asset_2_reserves, + swap_price_impact = calculate_price_impact( + input_supply=asset_1_reserves + if swap_from_asset_1_to_asset_2 + else asset_2_reserves, + output_supply=asset_2_reserves + if swap_from_asset_1_to_asset_2 + else asset_1_reserves, swap_input_amount=swap_in_amount, swap_output_amount=swap_out_amount, ) - - internal_swap_quote = InternalSwapQuote( - amount_in=AssetAmount(swap_in_asset, swap_in_amount), - amount_out=AssetAmount(swap_out_asset, swap_out_amount), - swap_fees=AssetAmount(swap_in_asset, int(total_fee_amount)), - price_impact=price_impact, + return ( + pool_token_asset_amount, + swap_from_asset_1_to_asset_2, + swap_in_amount, + swap_out_amount, + swap_total_fee_amount, + swap_price_impact, ) - return pool_token_asset_amount, internal_swap_quote - def calculate_output_amount_of_fixed_input_swap( input_supply: int, output_supply: int, swap_amount: int @@ -173,7 +179,7 @@ def calculate_fixed_input_swap( def calculate_fixed_output_swap( input_supply: int, output_supply: int, swap_output_amount: int, total_fee_share: int -): +) -> (int, int, float): swap_amount = calculate_swap_amount_of_fixed_output_swap( input_supply, output_supply, swap_output_amount ) diff --git a/tinyman/v2/pools.py b/tinyman/v2/pools.py index d0cec3d..cc03b31 100644 --- a/tinyman/v2/pools.py +++ b/tinyman/v2/pools.py @@ -1,10 +1,11 @@ from typing import Optional +from algosdk.future.transaction import LogicSigAccount from algosdk.v2client.algod import AlgodClient from tinyman.assets import Asset, AssetAmount from tinyman.optin import prepare_asset_optin_transactions -from tinyman.utils import get_state_int, get_state_bytes, bytes_to_int +from tinyman.utils import get_state_int, get_state_bytes, bytes_to_int, TransactionGroup from .add_liquidity import ( prepare_initial_add_liquidity_transactions, prepare_single_asset_add_liquidity_transactions, @@ -14,15 +15,23 @@ from .client import TinymanV2Client from .constants import MIN_POOL_BALANCE_ASA_ALGO_PAIR, MIN_POOL_BALANCE_ASA_ASA_PAIR from .contracts import get_pool_logicsig +from .exceptions import ( + AlreadyBootstrapped, + BootstrapIsRequired, + PoolHasNoLiquidity, + PoolAlreadyHasLiquidity, +) from .formulas import ( - get_subsequent_add_liquidity, - get_initial_add_liquidity, + calculate_subsequent_add_liquidity, + calculate_initial_add_liquidity, calculate_fixed_input_swap, calculate_remove_liquidity_output_amounts, calculate_fixed_output_swap, ) from .quotes import ( - AddLiquidityQuote, + FlexibleAddLiquidityQuote, + SingleAssetAddLiquidityQuote, + InitialAddLiquidityQuote, RemoveLiquidityQuote, InternalSwapQuote, SingleAssetRemoveLiquidityQuote, @@ -35,14 +44,16 @@ from .swap import prepare_swap_transactions -def get_pool_info(client: AlgodClient, validator_app_id, asset_1_id, asset_2_id): +def get_pool_info( + client: AlgodClient, validator_app_id: int, asset_1_id: int, asset_2_id: int +) -> dict: pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) pool_address = pool_logicsig.address() account_info = client.account_info(pool_address) return get_pool_info_from_account_info(account_info) -def get_pool_info_from_account_info(account_info): +def get_pool_info_from_account_info(account_info: dict) -> dict: try: validator_app_id = account_info["apps-local-state"][0]["id"] except IndexError: @@ -111,8 +122,8 @@ class Pool: def __init__( self, client: TinymanV2Client, - asset_a: Asset, - asset_b: Asset, + asset_a: [Asset, int], + asset_b: [Asset, int], info=None, fetch=True, validator_app_id=None, @@ -157,7 +168,9 @@ 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}" @classmethod - def from_account_info(cls, account_info, client=None): + def from_account_info( + cls, account_info: dict, client: Optional[TinymanV2Client] = None + ): info = get_pool_info_from_account_info(account_info) pool = Pool( client, @@ -168,7 +181,7 @@ def from_account_info(cls, account_info, client=None): ) return pool - def refresh(self, info=None): + def refresh(self, info: Optional[dict] = None) -> None: if info is None: info = get_pool_info( self.client.algod, @@ -180,7 +193,7 @@ def refresh(self, info=None): return self.update_from_info(info) - def update_from_info(self, info): + def update_from_info(self, info: dict) -> None: if info["pool_token_asset_id"] is not None: self.exists = True @@ -195,32 +208,35 @@ def update_from_info(self, info): self.last_refreshed_round = info["round"] self.algo_balance = info["algo_balance"] - def get_logicsig(self): + def get_logicsig(self) -> LogicSigAccount: pool_logicsig = get_pool_logicsig( self.validator_app_id, self.asset_1.id, self.asset_2.id ) return pool_logicsig @property - def address(self): + def address(self) -> str: logicsig = self.get_logicsig() pool_address = logicsig.address() return pool_address @property - def asset_1_price(self): - assert self.issued_pool_tokens + def asset_1_price(self) -> float: + if not self.issued_pool_tokens: + raise PoolHasNoLiquidity() return self.asset_2_reserves / self.asset_1_reserves @property - def asset_2_price(self): - assert self.issued_pool_tokens + def asset_2_price(self) -> float: + if not self.issued_pool_tokens: + raise PoolHasNoLiquidity() return self.asset_1_reserves / self.asset_2_reserves - def info(self): - assert self.exists + def info(self) -> dict: + if not self.exists: + raise BootstrapIsRequired() pool = { "address": self.address, @@ -241,15 +257,25 @@ def info(self): } return pool - def convert(self, amount: AssetAmount): - assert self.issued_pool_tokens + def convert(self, amount: AssetAmount) -> AssetAmount: + if not self.issued_pool_tokens: + raise PoolHasNoLiquidity() if amount.asset == self.asset_1: return AssetAmount(self.asset_2, int(amount.amount * self.asset_1_price)) elif amount.asset == self.asset_2: return AssetAmount(self.asset_1, int(amount.amount * self.asset_2_price)) - def prepare_bootstrap_transactions(self, pooler_address=None): + raise NotImplementedError() + + def prepare_bootstrap_transactions( + self, pooler_address: Optional[str] = None + ) -> TransactionGroup: + self.refresh() + + if self.exists: + raise AlreadyBootstrapped() + pooler_address = pooler_address or self.client.user_address suggested_params = self.client.algod.suggested_params() @@ -281,38 +307,57 @@ def prepare_bootstrap_transactions(self, pooler_address=None): ) return txn_group - def fetch_add_liquidity_quote( - self, amount_a: AssetAmount, amount_b: AssetAmount = None, slippage=0.05 - ): - if amount_b is None: - amount_b = AssetAmount( - self.asset_2 if amount_a.asset == self.asset_1 else self.asset_1, 0 - ) + def fetch_flexible_add_liquidity_quote( + self, amount_a: AssetAmount, amount_b: AssetAmount, slippage: float = 0.05 + ) -> FlexibleAddLiquidityQuote: + assert {self.asset_1, self.asset_2} == { + amount_a.asset, + amount_b.asset, + }, "Pool assets and given assets don't match." amount_1 = amount_a if amount_a.asset == self.asset_1 else amount_b amount_2 = amount_a if amount_a.asset == self.asset_2 else amount_b self.refresh() if not self.exists: - raise Exception("Pool has not been bootstrapped yet!") + raise BootstrapIsRequired() - if self.issued_pool_tokens: - initial = False - pool_token_asset_amount, internal_swap_quote = get_subsequent_add_liquidity( - pool=self, - asset_1_amount=amount_1.amount if amount_1 else 0, - asset_2_amount=amount_2.amount if amount_2 else 0, - ) + if not self.issued_pool_tokens: + raise PoolHasNoLiquidity() - else: - initial = True - slippage = 0 - pool_token_asset_amount = get_initial_add_liquidity( - asset_1_amount=amount_1.amount, asset_2_amount=amount_2.amount - ) - internal_swap_quote = None + ( + pool_token_asset_amount, + swap_from_asset_1_to_asset_2, + swap_in_amount, + swap_out_amount, + swap_total_fee_amount, + swap_price_impact, + ) = calculate_subsequent_add_liquidity( + asset_1_reserves=self.asset_1_reserves, + asset_2_reserves=self.asset_2_reserves, + issued_pool_tokens=self.issued_pool_tokens, + total_fee_share=self.total_fee_share, + asset_1_amount=amount_1.amount, + asset_2_amount=amount_2.amount, + ) - quote = AddLiquidityQuote( + internal_swap_quote = InternalSwapQuote( + amount_in=AssetAmount( + self.asset_1 if swap_from_asset_1_to_asset_2 else self.asset_2, + swap_in_amount, + ), + amount_out=AssetAmount( + self.asset_2 if swap_from_asset_1_to_asset_2 else self.asset_1, + swap_out_amount, + ), + swap_fees=AssetAmount( + self.asset_1 if swap_from_asset_1_to_asset_2 else self.asset_2, + int(swap_total_fee_amount), + ), + price_impact=swap_price_impact, + ) + + quote = FlexibleAddLiquidityQuote( amounts_in={ self.asset_1: amount_1, self.asset_2: amount_2, @@ -321,78 +366,221 @@ def fetch_add_liquidity_quote( self.pool_token_asset, pool_token_asset_amount ), slippage=slippage, - initial=initial, internal_swap_quote=internal_swap_quote, ) return quote - def prepare_add_liquidity_transactions( + def fetch_single_asset_add_liquidity_quote( + self, amount_a: AssetAmount, slippage: float = 0.05 + ) -> SingleAssetAddLiquidityQuote: + self.refresh() + if not self.exists: + raise BootstrapIsRequired() + + if not self.issued_pool_tokens: + raise PoolHasNoLiquidity() + + if amount_a.asset == self.asset_1: + ( + pool_token_asset_amount, + swap_from_asset_1_to_asset_2, + swap_in_amount, + swap_out_amount, + swap_total_fee_amount, + swap_price_impact, + ) = calculate_subsequent_add_liquidity( + asset_1_reserves=self.asset_1_reserves, + asset_2_reserves=self.asset_2_reserves, + issued_pool_tokens=self.issued_pool_tokens, + total_fee_share=self.total_fee_share, + asset_1_amount=amount_a.amount, + asset_2_amount=0, + ) + elif amount_a.asset == self.asset_2: + ( + pool_token_asset_amount, + swap_from_asset_1_to_asset_2, + swap_in_amount, + swap_out_amount, + swap_total_fee_amount, + swap_price_impact, + ) = calculate_subsequent_add_liquidity( + asset_1_reserves=self.asset_1_reserves, + asset_2_reserves=self.asset_2_reserves, + issued_pool_tokens=self.issued_pool_tokens, + total_fee_share=self.total_fee_share, + asset_1_amount=0, + asset_2_amount=amount_a.amount, + ) + else: + assert False, "Given asset doesn't belong to the pool assets." + + internal_swap_quote = InternalSwapQuote( + amount_in=AssetAmount( + self.asset_1 if swap_from_asset_1_to_asset_2 else self.asset_2, + swap_in_amount, + ), + amount_out=AssetAmount( + self.asset_2 if swap_from_asset_1_to_asset_2 else self.asset_1, + swap_out_amount, + ), + swap_fees=AssetAmount( + self.asset_1 if swap_from_asset_1_to_asset_2 else self.asset_2, + int(swap_total_fee_amount), + ), + price_impact=swap_price_impact, + ) + quote = SingleAssetAddLiquidityQuote( + amount_in=amount_a, + pool_token_asset_amount=AssetAmount( + self.pool_token_asset, pool_token_asset_amount + ), + slippage=slippage, + internal_swap_quote=internal_swap_quote, + ) + return quote + + def fetch_initial_add_liquidity_quote( self, - amounts_in: "dict[Asset, AssetAmount]", - min_pool_token_asset_amount: Optional[int], - pooler_address=None, - ): - # TODO: Remove magics - assert self.exists + amount_a: AssetAmount, + amount_b: AssetAmount, + ) -> InitialAddLiquidityQuote: + assert {self.asset_1, self.asset_2} == { + amount_a.asset, + amount_b.asset, + }, "Pool assets and given assets don't match." + + amount_1 = amount_a if amount_a.asset == self.asset_1 else amount_b + amount_2 = amount_a if amount_a.asset == self.asset_2 else amount_b + self.refresh() + if not self.exists: + raise BootstrapIsRequired() + + if self.issued_pool_tokens: + raise PoolAlreadyHasLiquidity() + + pool_token_asset_amount = calculate_initial_add_liquidity( + asset_1_amount=amount_1.amount, asset_2_amount=amount_2.amount + ) + quote = InitialAddLiquidityQuote( + amounts_in={ + self.asset_1: amount_1, + self.asset_2: amount_2, + }, + pool_token_asset_amount=AssetAmount( + self.pool_token_asset, pool_token_asset_amount + ), + ) + return quote + + def prepare_flexible_add_liquidity_transactions( + self, + amounts_in: dict[Asset, AssetAmount], + min_pool_token_asset_amount: int, + pooler_address: Optional[str] = None, + ) -> TransactionGroup: pooler_address = pooler_address or self.client.user_address asset_1_amount = amounts_in[self.asset_1] asset_2_amount = amounts_in[self.asset_2] suggested_params = self.client.algod.suggested_params() - if self.issued_pool_tokens: - assert min_pool_token_asset_amount is not None - - if asset_1_amount.amount and asset_2_amount.amount: - txn_group = prepare_flexible_add_liquidity_transactions( - validator_app_id=self.validator_app_id, - asset_1_id=self.asset_1.id, - asset_2_id=self.asset_2.id, - pool_token_asset_id=self.pool_token_asset.id, - asset_1_amount=asset_1_amount.amount, - asset_2_amount=asset_2_amount.amount, - min_pool_token_asset_amount=min_pool_token_asset_amount, - sender=pooler_address, - suggested_params=suggested_params, - ) - else: - txn_group = prepare_single_asset_add_liquidity_transactions( - validator_app_id=self.validator_app_id, - asset_1_id=self.asset_1.id, - asset_2_id=self.asset_2.id, - pool_token_asset_id=self.pool_token_asset.id, - asset_1_amount=asset_1_amount.amount, - asset_2_amount=asset_2_amount.amount, - min_pool_token_asset_amount=min_pool_token_asset_amount, - sender=pooler_address, - suggested_params=suggested_params, - ) - else: - txn_group = prepare_initial_add_liquidity_transactions( - validator_app_id=self.validator_app_id, - asset_1_id=self.asset_1.id, - asset_2_id=self.asset_2.id, - pool_token_asset_id=self.pool_token_asset.id, - asset_1_amount=asset_1_amount.amount, - asset_2_amount=asset_2_amount.amount, - sender=pooler_address, - suggested_params=suggested_params, - ) + txn_group = prepare_flexible_add_liquidity_transactions( + validator_app_id=self.validator_app_id, + asset_1_id=self.asset_1.id, + asset_2_id=self.asset_2.id, + pool_token_asset_id=self.pool_token_asset.id, + asset_1_amount=asset_1_amount.amount, + asset_2_amount=asset_2_amount.amount, + min_pool_token_asset_amount=min_pool_token_asset_amount, + sender=pooler_address, + suggested_params=suggested_params, + ) + return txn_group + + def prepare_single_asset_add_liquidity_transactions( + self, + amount_in: AssetAmount, + min_pool_token_asset_amount: Optional[int], + pooler_address: Optional[str] = None, + ) -> TransactionGroup: + pooler_address = pooler_address or self.client.user_address + suggested_params = self.client.algod.suggested_params() + txn_group = prepare_single_asset_add_liquidity_transactions( + validator_app_id=self.validator_app_id, + asset_1_id=self.asset_1.id, + asset_2_id=self.asset_2.id, + pool_token_asset_id=self.pool_token_asset.id, + asset_1_amount=amount_in.amount + if amount_in.asset == self.asset_1 + else None, + asset_2_amount=amount_in.amount + if amount_in.asset == self.asset_2 + else None, + min_pool_token_asset_amount=min_pool_token_asset_amount, + sender=pooler_address, + suggested_params=suggested_params, + ) return txn_group - def prepare_add_liquidity_transactions_from_quote( - self, quote: AddLiquidityQuote, pooler_address=None - ): - return self.prepare_add_liquidity_transactions( - amounts_in=quote.amounts_in, - min_pool_token_asset_amount=None - if quote.initial - else quote.min_pool_token_asset_amount_with_slippage, - pooler_address=pooler_address, + def prepare_initial_add_liquidity_transactions( + self, + amounts_in: dict[Asset, AssetAmount], + pooler_address: Optional[str] = None, + ) -> TransactionGroup: + pooler_address = pooler_address or self.client.user_address + asset_1_amount = amounts_in[self.asset_1] + asset_2_amount = amounts_in[self.asset_2] + suggested_params = self.client.algod.suggested_params() + + txn_group = prepare_initial_add_liquidity_transactions( + validator_app_id=self.validator_app_id, + asset_1_id=self.asset_1.id, + asset_2_id=self.asset_2.id, + pool_token_asset_id=self.pool_token_asset.id, + asset_1_amount=asset_1_amount.amount, + asset_2_amount=asset_2_amount.amount, + sender=pooler_address, + suggested_params=suggested_params, ) + return txn_group + + def prepare_add_liquidity_transactions_from_quote( + self, + quote: [ + FlexibleAddLiquidityQuote, + SingleAssetAddLiquidityQuote, + InitialAddLiquidityQuote, + ], + pooler_address: Optional[str] = None, + ) -> TransactionGroup: + if isinstance(quote, FlexibleAddLiquidityQuote): + return self.prepare_flexible_add_liquidity_transactions( + amounts_in=quote.amounts_in, + min_pool_token_asset_amount=quote.min_pool_token_asset_amount_with_slippage, + pooler_address=pooler_address, + ) + elif isinstance(quote, SingleAssetAddLiquidityQuote): + return self.prepare_single_asset_add_liquidity_transactions( + amount_in=quote.amount_in, + min_pool_token_asset_amount=quote.min_pool_token_asset_amount_with_slippage, + pooler_address=pooler_address, + ) + elif isinstance(quote, InitialAddLiquidityQuote): + return self.prepare_initial_add_liquidity_transactions( + amounts_in=quote.amounts_in, + pooler_address=pooler_address, + ) + + raise Exception(f"Invalid quote type({type(quote)})") + + def fetch_remove_liquidity_quote( + self, pool_token_asset_in: [AssetAmount, int], slippage: float = 0.05 + ) -> RemoveLiquidityQuote: + if not self.exists: + raise BootstrapIsRequired() - def fetch_remove_liquidity_quote(self, pool_token_asset_in, slippage=0.05): if isinstance(pool_token_asset_in, int): pool_token_asset_in = AssetAmount( self.pool_token_asset, pool_token_asset_in @@ -419,8 +607,14 @@ def fetch_remove_liquidity_quote(self, pool_token_asset_in, slippage=0.05): return quote def fetch_single_asset_remove_liquidity_quote( - self, pool_token_asset_in, output_asset, slippage=0.05 - ): + self, + pool_token_asset_in: [AssetAmount, int], + output_asset: Asset, + slippage: float = 0.05, + ) -> SingleAssetRemoveLiquidityQuote: + if not self.exists: + raise BootstrapIsRequired() + if isinstance(pool_token_asset_in, int): pool_token_asset_in = AssetAmount( self.pool_token_asset, pool_token_asset_in @@ -493,8 +687,11 @@ def fetch_single_asset_remove_liquidity_quote( return quote def prepare_remove_liquidity_transactions( - self, pool_token_asset_amount: AssetAmount, amounts_out, pooler_address=None - ): + self, + pool_token_asset_amount: [AssetAmount, int], + amounts_out: dict[Asset, AssetAmount], + pooler_address: Optional[str] = None, + ) -> TransactionGroup: if isinstance(pool_token_asset_amount, int): pool_token_asset_amount = AssetAmount( self.pool_token_asset, pool_token_asset_amount @@ -519,10 +716,10 @@ def prepare_remove_liquidity_transactions( def prepare_single_asset_remove_liquidity_transactions( self, - pool_token_asset_amount: AssetAmount, + pool_token_asset_amount: [AssetAmount, int], amount_out: AssetAmount, pooler_address: Optional[str] = None, - ): + ) -> TransactionGroup: if isinstance(pool_token_asset_amount, int): pool_token_asset_amount = AssetAmount( self.pool_token_asset, pool_token_asset_amount @@ -546,8 +743,8 @@ def prepare_single_asset_remove_liquidity_transactions( def prepare_remove_liquidity_transactions_from_quote( self, quote: [RemoveLiquidityQuote, SingleAssetRemoveLiquidityQuote], - pooler_address=None, - ): + pooler_address: Optional[str] = None, + ) -> TransactionGroup: pooler_address = pooler_address or self.client.user_address if isinstance(quote, SingleAssetRemoveLiquidityQuote): @@ -566,13 +763,17 @@ def prepare_remove_liquidity_transactions_from_quote( pooler_address=pooler_address, ) - assert False + raise NotImplementedError() def fetch_fixed_input_swap_quote( - self, amount_in: AssetAmount, slippage=0.05 + self, amount_in: AssetAmount, slippage: float = 0.05 ) -> SwapQuote: self.refresh() - assert self.issued_pool_tokens + if not self.exists: + raise BootstrapIsRequired() + + if not self.issued_pool_tokens: + raise PoolHasNoLiquidity() if amount_in.asset == self.asset_1: asset_out = self.asset_2 @@ -585,9 +786,6 @@ def fetch_fixed_input_swap_quote( else: raise False - if not input_supply or not output_supply: - raise Exception("Pool has no liquidity!") - swap_output_amount, total_fee_amount, price_impact = calculate_fixed_input_swap( input_supply=input_supply, output_supply=output_supply, @@ -607,10 +805,14 @@ 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: float = 0.05 ) -> SwapQuote: self.refresh() - assert self.issued_pool_tokens + if not self.exists: + raise BootstrapIsRequired() + + if not self.issued_pool_tokens: + raise PoolHasNoLiquidity() if amount_out.asset == self.asset_1: asset_in = self.asset_2 @@ -645,9 +847,9 @@ def prepare_swap_transactions( self, amount_in: AssetAmount, amount_out: AssetAmount, - swap_type, + swap_type: [str, bytes], swapper_address=None, - ): + ) -> TransactionGroup: swapper_address = swapper_address or self.client.user_address suggested_params = self.client.algod.suggested_params() @@ -666,7 +868,7 @@ def prepare_swap_transactions( def prepare_swap_transactions_from_quote( self, quote: SwapQuote, swapper_address=None - ): + ) -> TransactionGroup: return self.prepare_swap_transactions( amount_in=quote.amount_in_with_slippage, amount_out=quote.amount_out_with_slippage, @@ -674,7 +876,9 @@ def prepare_swap_transactions_from_quote( swapper_address=swapper_address, ) - def prepare_pool_token_asset_optin_transactions(self, user_address=None): + def prepare_pool_token_asset_optin_transactions( + self, user_address: Optional[str] = None + ) -> TransactionGroup: user_address = user_address or self.client.user_address suggested_params = self.client.algod.suggested_params() txn_group = prepare_asset_optin_transactions( @@ -684,7 +888,7 @@ def prepare_pool_token_asset_optin_transactions(self, user_address=None): ) return txn_group - def fetch_pool_position(self, pooler_address=None): + def fetch_pool_position(self, pooler_address: Optional[str] = None) -> dict: pooler_address = pooler_address or self.client.user_address account_info = self.client.algod.account_info(pooler_address) assets = {a["asset-id"]: a for a in account_info["assets"]} diff --git a/tinyman/v2/quotes.py b/tinyman/v2/quotes.py index b7a527e..7511169 100644 --- a/tinyman/v2/quotes.py +++ b/tinyman/v2/quotes.py @@ -57,11 +57,10 @@ def price(self) -> float: @dataclass -class AddLiquidityQuote: +class FlexibleAddLiquidityQuote: amounts_in: dict[Asset, AssetAmount] pool_token_asset_amount: AssetAmount slippage: float - initial: bool = False internal_swap_quote: InternalSwapQuote = None @property @@ -71,6 +70,26 @@ def min_pool_token_asset_amount_with_slippage(self) -> int: ) +@dataclass +class SingleAssetAddLiquidityQuote: + amount_in: AssetAmount + pool_token_asset_amount: AssetAmount + slippage: float + internal_swap_quote: InternalSwapQuote = None + + @property + def min_pool_token_asset_amount_with_slippage(self) -> int: + return self.pool_token_asset_amount.amount - math.ceil( + self.pool_token_asset_amount.amount * self.slippage + ) + + +@dataclass +class InitialAddLiquidityQuote: + amounts_in: dict[Asset, AssetAmount] + pool_token_asset_amount: AssetAmount + + @dataclass class RemoveLiquidityQuote: amounts_out: dict[Asset, AssetAmount] diff --git a/tinyman/v2/remove_liquidity.py b/tinyman/v2/remove_liquidity.py index a666ad6..0149fe3 100644 --- a/tinyman/v2/remove_liquidity.py +++ b/tinyman/v2/remove_liquidity.py @@ -19,7 +19,7 @@ def prepare_remove_liquidity_transactions( pool_token_asset_amount: int, sender: str, suggested_params: SuggestedParams, -): +) -> TransactionGroup: pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) pool_address = pool_logicsig.address() @@ -64,7 +64,7 @@ def prepare_single_asset_remove_liquidity_transactions( pool_token_asset_amount: int, sender: str, suggested_params: SuggestedParams, -): +) -> TransactionGroup: pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) pool_address = pool_logicsig.address() diff --git a/tinyman/v2/swap.py b/tinyman/v2/swap.py index 2eb0a97..0952d6b 100644 --- a/tinyman/v2/swap.py +++ b/tinyman/v2/swap.py @@ -24,7 +24,7 @@ def prepare_swap_transactions( swap_type: [str, bytes], sender: str, suggested_params: SuggestedParams, -): +) -> TransactionGroup: pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) pool_address = pool_logicsig.address() diff --git a/tinyman/v2/utils.py b/tinyman/v2/utils.py index 9a3e874..995048b 100644 --- a/tinyman/v2/utils.py +++ b/tinyman/v2/utils.py @@ -1,7 +1,7 @@ from base64 import b64decode -def decode_logs(logs): +def decode_logs(logs: list[[bytes, str]]) -> dict: decoded_logs = dict() for log in logs: if type(log) == str: From 6591a6d2425a2d6bc83078a25848451a04bf6810 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Mon, 3 Oct 2022 17:02:20 +0300 Subject: [PATCH 34/73] add basic examples to readme --- README.md | 140 ++++++++++++++++++ examples/v2/sneak_preview.py | 22 +++ examples/v2/tutorial/02_create_assets.py | 2 +- examples/v2/tutorial/03_bootstrap_pool.py | 7 +- .../v2/tutorial/04_add_initial_liquidity.py | 7 +- .../v2/tutorial/05_add_flexible_liquidity.py | 7 +- ...ty.py => 06_add_single_asset_liquidity.py} | 7 +- examples/v2/tutorial/07_remove_liquidity.py | 7 +- .../08_single_asset_remove_liquidity.py | 8 +- examples/v2/tutorial/09_fixed_input_swap.py | 7 +- examples/v2/tutorial/10_fixed_output_swap.py | 7 +- examples/v2/tutorial/common.py | 8 - examples/v2/utils.py | 8 + tinyman/client.py | 6 +- tinyman/utils.py | 12 +- 15 files changed, 212 insertions(+), 43 deletions(-) create mode 100644 examples/v2/sneak_preview.py rename examples/v2/tutorial/{06_add_single_liquidity.py => 06_add_single_asset_liquidity.py} (92%) create mode 100644 examples/v2/utils.py diff --git a/README.md b/README.md index aa7505e..b77db66 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,146 @@ tinyman-py-sdk is not yet released on PYPI. It can be installed directly from th `pip install git+https://github.com/tinymanorg/tinyman-py-sdk.git` +## V2 + +## Sneak Preview + +```python +# examples/v2/sneak_preview.py + +from examples.v2.utils import get_algod +from tinyman.v2.client import TinymanV2TestnetClient + +algod = get_algod() +client = TinymanV2TestnetClient(algod_client=algod) + +# Fetch our two assets of interest +USDC = client.fetch_asset(10458941) +ALGO = client.fetch_asset(0) + +# Fetch the pool we will work with +pool = client.fetch_pool(USDC, ALGO) +print(f"Pool Info: {pool.info()}") + +# Get a quote for a swap of 1 ALGO to USDC with 1% slippage tolerance +quote = pool.fetch_fixed_input_swap_quote(amount_in=ALGO(1_000_000), slippage=0.01) +print(quote) +print(f"USDC per ALGO: {quote.price}") +print(f"USDC per ALGO (worst case): {quote.price_with_slippage}") +``` + +## Tutorial + +You can find a tutorial under the `examples/v2/tutorial` folder. + +To run a step use `python ` such as `python 01_generate_account.py`. + +#### Prerequisites +1. Generating an account (`01_generate_account.py`) +2. Creating 2 assets (`02_create_assets.by`) + +#### Steps + +3. Bootstrapping a pool (`03_bootstrap_pool.py`) +4. Adding initial liquidity to the pool (`04_add_initial_liquidity.py`) +5. Adding flexible (add two asset with a flexible rate) liquidity to the pool (`05_add_flexible_liquidity.py`) +6. Adding single asset (add only one asset) liquidity to the pool(`06_add_single_asset_liquidity.py`) +7. Removing liquidity to the pool(`07_remove_liquidity.py`) +8. Removing single asset (receive single asset) liquidity to the pool(`08_single_asset_remove_liquidity.py`) +9. Swapping fixed-input (`09_fixed_input_swap.py`) +10. Swapping fixed-output (`10_fixed_output_swap.py`) + +## Example Operations + +### Bootstrap + +```python +txn_group = pool.prepare_bootstrap_transactions() +txn_group.sign_with_private_key(
, ) +txn_info = txn_group.submit(algod, wait=True) +``` + +### Add Liquidity + +#### Initial Add Liquidity + +```python +quote = pool.fetch_initial_add_liquidity_quote( + amount_a=, + amount_b=, +) +txn_group = pool.prepare_add_liquidity_transactions_from_quote(quote) +txn_group.sign_with_private_key(
, ) +txn_info = txn_group.submit(algod, wait=True) +``` + +#### Flexible Add Liquidity + +```python +quote = pool.fetch_flexible_add_liquidity_quote( + amount_a=, + amount_b=, +) +txn_group = pool.prepare_add_liquidity_transactions_from_quote(quote=quote) +txn_group.sign_with_private_key(
, ) +txn_info = txn_group.submit(algod, wait=True) +``` + +#### Single Asset Add Liquidity + +```python +quote = pool.fetch_single_asset_add_liquidity_quote(amount_a=) +txn_group = pool.prepare_add_liquidity_transactions_from_quote(quote=quote) +txn_group.sign_with_private_key(
, ) +txn_info = txn_group.submit(algod, wait=True) +``` + +### Remove Liquidity + +#### Remove Liquidity + +```python +quote = pool.fetch_remove_liquidity_quote( + pool_token_asset_in=, +) +txn_group = pool.prepare_remove_liquidity_transactions_from_quote(quote=quote) +txn_group.sign_with_private_key(
, ) +txn_info = txn_group.submit(algod, wait=True) +``` + +#### Single Asset Remove Liquidity + +```python +quote = pool.fetch_single_asset_remove_liquidity_quote( + pool_token_asset_in=, + output_asset=, +) +txn_group = pool.prepare_remove_liquidity_transactions_from_quote(quote=quote) +txn_group.sign_with_private_key(
, ) +txn_info = txn_group.submit(algod, wait=True) +``` + +### Swap + +#### Fixed Input Swap + +```python +quote = pool.fetch_fixed_input_swap_quote(amount_in=) +txn_group = pool.prepare_swap_transactions_from_quote(quote=quote) +txn_group.sign_with_private_key(
, ) +txn_info = txn_group.submit(algod, wait=True) +``` + +#### Fixed Output Swap + +```python +quote = pool.fetch_fixed_output_swap_quote(amount_in=) +txn_group = pool.prepare_swap_transactions_from_quote(quote=quote) +txn_group.sign_with_private_key(
, ) +txn_info = txn_group.submit(algod, wait=True) +``` + +## V1.1 ## Sneak Preview diff --git a/examples/v2/sneak_preview.py b/examples/v2/sneak_preview.py new file mode 100644 index 0000000..bf07e58 --- /dev/null +++ b/examples/v2/sneak_preview.py @@ -0,0 +1,22 @@ +# This sample is provided for demonstration purposes only. +# It is not intended for production use. +# This example does not constitute trading advice. +from examples.v2.utils import get_algod +from tinyman.v2.client import TinymanV2TestnetClient + +algod = get_algod() +client = TinymanV2TestnetClient(algod_client=algod) + +# Fetch our two assets of interest +USDC = client.fetch_asset(10458941) +ALGO = client.fetch_asset(0) + +# Fetch the pool we will work with +pool = client.fetch_pool(USDC, ALGO) +print(f"Pool Info: {pool.info()}") + +# Get a quote for a swap of 1 ALGO to USDC with 1% slippage tolerance +quote = pool.fetch_fixed_input_swap_quote(amount_in=ALGO(1_000_000), slippage=0.01) +print(quote) +print(f"USDC per ALGO: {quote.price}") +print(f"USDC per ALGO (worst case): {quote.price_with_slippage}") diff --git a/examples/v2/tutorial/02_create_assets.py b/examples/v2/tutorial/02_create_assets.py index cb96611..a9e9a32 100644 --- a/examples/v2/tutorial/02_create_assets.py +++ b/examples/v2/tutorial/02_create_assets.py @@ -6,10 +6,10 @@ from examples.v2.tutorial.common import ( get_account, - get_algod, get_assets_file_path, create_asset, ) +from examples.v2.utils import get_algod from tinyman.v2.client import TinymanV2TestnetClient diff --git a/examples/v2/tutorial/03_bootstrap_pool.py b/examples/v2/tutorial/03_bootstrap_pool.py index f1ab5cd..c0bb7bc 100644 --- a/examples/v2/tutorial/03_bootstrap_pool.py +++ b/examples/v2/tutorial/03_bootstrap_pool.py @@ -4,7 +4,8 @@ from pprint import pprint from urllib.parse import quote_plus -from examples.v2.tutorial.common import get_account, get_algod, get_assets +from examples.v2.tutorial.common import get_account, get_assets +from examples.v2.utils import get_algod from tinyman.v2.client import TinymanV2TestnetClient @@ -28,10 +29,10 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txinfo = txn_group.submit(algod, wait=True) +txn_info = txn_group.submit(algod, wait=True) print("Transaction Info") -pprint(txinfo) +pprint(txn_info) print( f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" diff --git a/examples/v2/tutorial/04_add_initial_liquidity.py b/examples/v2/tutorial/04_add_initial_liquidity.py index 86fb6ce..42f1d13 100644 --- a/examples/v2/tutorial/04_add_initial_liquidity.py +++ b/examples/v2/tutorial/04_add_initial_liquidity.py @@ -6,7 +6,8 @@ from tinyman.assets import AssetAmount -from examples.v2.tutorial.common import get_account, get_algod, get_assets +from examples.v2.tutorial.common import get_account, get_assets +from examples.v2.utils import get_algod from tinyman.v2.client import TinymanV2TestnetClient @@ -39,9 +40,9 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txinfo = txn_group.submit(algod, wait=True) +txn_info = txn_group.submit(algod, wait=True) print("Transaction Info") -pprint(txinfo) +pprint(txn_info) print( f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" diff --git a/examples/v2/tutorial/05_add_flexible_liquidity.py b/examples/v2/tutorial/05_add_flexible_liquidity.py index 9e60d80..5096e11 100644 --- a/examples/v2/tutorial/05_add_flexible_liquidity.py +++ b/examples/v2/tutorial/05_add_flexible_liquidity.py @@ -6,7 +6,8 @@ from tinyman.assets import AssetAmount -from examples.v2.tutorial.common import get_account, get_algod, get_assets +from examples.v2.tutorial.common import get_account, get_assets +from examples.v2.utils import get_algod from tinyman.v2.client import TinymanV2TestnetClient @@ -48,9 +49,9 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txinfo = txn_group.submit(algod, wait=True) +txn_info = txn_group.submit(algod, wait=True) print("Transaction Info") -pprint(txinfo) +pprint(txn_info) print( f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" diff --git a/examples/v2/tutorial/06_add_single_liquidity.py b/examples/v2/tutorial/06_add_single_asset_liquidity.py similarity index 92% rename from examples/v2/tutorial/06_add_single_liquidity.py rename to examples/v2/tutorial/06_add_single_asset_liquidity.py index a5e0bb1..c83753a 100644 --- a/examples/v2/tutorial/06_add_single_liquidity.py +++ b/examples/v2/tutorial/06_add_single_asset_liquidity.py @@ -6,7 +6,8 @@ from tinyman.assets import AssetAmount -from examples.v2.tutorial.common import get_account, get_algod, get_assets +from examples.v2.tutorial.common import get_account, get_assets +from examples.v2.utils import get_algod from tinyman.v2.client import TinymanV2TestnetClient @@ -47,9 +48,9 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txinfo = txn_group.submit(algod, wait=True) +txn_info = txn_group.submit(algod, wait=True) print("Transaction Info") -pprint(txinfo) +pprint(txn_info) print( f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" diff --git a/examples/v2/tutorial/07_remove_liquidity.py b/examples/v2/tutorial/07_remove_liquidity.py index 2f17c21..974838d 100644 --- a/examples/v2/tutorial/07_remove_liquidity.py +++ b/examples/v2/tutorial/07_remove_liquidity.py @@ -4,7 +4,8 @@ from pprint import pprint from urllib.parse import quote_plus -from examples.v2.tutorial.common import get_account, get_algod, get_assets +from examples.v2.tutorial.common import get_account, get_assets +from examples.v2.utils import get_algod from tinyman.v2.client import TinymanV2TestnetClient @@ -33,9 +34,9 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txinfo = txn_group.submit(algod, wait=True) +txn_info = txn_group.submit(algod, wait=True) print("Transaction Info") -pprint(txinfo) +pprint(txn_info) print( f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" diff --git a/examples/v2/tutorial/08_single_asset_remove_liquidity.py b/examples/v2/tutorial/08_single_asset_remove_liquidity.py index 291edc0..7ffe1a8 100644 --- a/examples/v2/tutorial/08_single_asset_remove_liquidity.py +++ b/examples/v2/tutorial/08_single_asset_remove_liquidity.py @@ -4,7 +4,8 @@ from pprint import pprint from urllib.parse import quote_plus -from examples.v2.tutorial.common import get_account, get_algod, get_assets +from examples.v2.tutorial.common import get_account, get_assets +from examples.v2.utils import get_algod from tinyman.v2.client import TinymanV2TestnetClient @@ -23,7 +24,6 @@ quote = pool.fetch_single_asset_remove_liquidity_quote( pool_token_asset_in=pool_token_asset_in, output_asset=pool.asset_1, - slippage=0, # TODO: 0.05 ) print("\nSingle Asset Remove Liquidity Quote:") @@ -35,9 +35,9 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txinfo = txn_group.submit(algod, wait=True) +txn_info = txn_group.submit(algod, wait=True) print("Transaction Info") -pprint(txinfo) +pprint(txn_info) print( f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" diff --git a/examples/v2/tutorial/09_fixed_input_swap.py b/examples/v2/tutorial/09_fixed_input_swap.py index 0f2a5fe..5538944 100644 --- a/examples/v2/tutorial/09_fixed_input_swap.py +++ b/examples/v2/tutorial/09_fixed_input_swap.py @@ -6,7 +6,8 @@ from tinyman.assets import AssetAmount -from examples.v2.tutorial.common import get_account, get_algod, get_assets +from examples.v2.tutorial.common import get_account, get_assets +from examples.v2.utils import get_algod from tinyman.v2.client import TinymanV2TestnetClient @@ -35,9 +36,9 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txinfo = txn_group.submit(algod, wait=True) +txn_info = txn_group.submit(algod, wait=True) print("Transaction Info") -pprint(txinfo) +pprint(txn_info) print( f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" diff --git a/examples/v2/tutorial/10_fixed_output_swap.py b/examples/v2/tutorial/10_fixed_output_swap.py index 4a1dc31..57c4519 100644 --- a/examples/v2/tutorial/10_fixed_output_swap.py +++ b/examples/v2/tutorial/10_fixed_output_swap.py @@ -6,7 +6,8 @@ from tinyman.assets import AssetAmount -from examples.v2.tutorial.common import get_account, get_algod, get_assets +from examples.v2.tutorial.common import get_account, get_assets +from examples.v2.utils import get_algod from tinyman.v2.client import TinymanV2TestnetClient @@ -35,9 +36,9 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txinfo = txn_group.submit(algod, wait=True) +txn_info = txn_group.submit(algod, wait=True) print("Transaction Info") -pprint(txinfo) +pprint(txn_info) print( f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" diff --git a/examples/v2/tutorial/common.py b/examples/v2/tutorial/common.py index 63b583a..6ff0912 100644 --- a/examples/v2/tutorial/common.py +++ b/examples/v2/tutorial/common.py @@ -5,7 +5,6 @@ from pprint import pprint from algosdk.future.transaction import AssetCreateTxn, wait_for_confirmation -from algosdk.v2client.algod import AlgodClient def get_account_file_path(filename="account.json"): @@ -42,13 +41,6 @@ def get_assets(filename="assets.json"): return assets -def get_algod(): - # return AlgodClient( - # "", "http://localhost:8080", headers={"User-Agent": "algosdk"} - # ) - return AlgodClient("", "https://testnet-api.algonode.network") - - def create_asset(algod, sender, private_key): sp = algod.suggested_params() asset_name = "".join( diff --git a/examples/v2/utils.py b/examples/v2/utils.py new file mode 100644 index 0000000..caad47a --- /dev/null +++ b/examples/v2/utils.py @@ -0,0 +1,8 @@ +from algosdk.v2client.algod import AlgodClient + + +def get_algod(): + # return AlgodClient( + # "", "http://localhost:8080", headers={"User-Agent": "algosdk"} + # ) + return AlgodClient("", "https://testnet-api.algonode.network") diff --git a/tinyman/client.py b/tinyman/client.py index 063f3b8..b9156cf 100644 --- a/tinyman/client.py +++ b/tinyman/client.py @@ -31,9 +31,9 @@ def fetch_asset(self, asset_id): def submit(self, transaction_group, wait=False): txid = self.algod.send_transactions(transaction_group.signed_transactions) if wait: - txinfo = wait_for_confirmation(self.algod, txid) - txinfo["txid"] = txid - return txinfo + txn_info = wait_for_confirmation(self.algod, txid) + txn_info["txid"] = txid + return txn_info return {"txid": txid} def prepare_asset_optin_transactions( diff --git a/tinyman/utils.py b/tinyman/utils.py index 95e13a1..4a110fb 100644 --- a/tinyman/utils.py +++ b/tinyman/utils.py @@ -35,9 +35,9 @@ def sign_and_submit_transactions( signed_transactions[i] = txn.sign(sender_sk) txid = client.send_transactions(signed_transactions) - txinfo = wait_for_confirmation(client, txid) - txinfo["txid"] = txid - return txinfo + txn_info = wait_for_confirmation(client, txid) + txn_info["txid"] = txid + return txn_info def int_to_bytes(num): @@ -152,9 +152,9 @@ def submit(self, algod, wait=False): except AlgodHTTPError as e: raise Exception(str(e)) if wait: - txinfo = wait_for_confirmation(algod, txid) - txinfo["txid"] = txid - return txinfo + txn_info = wait_for_confirmation(algod, txid) + txn_info["txid"] = txid + return txn_info return {"txid": txid} def __add__(self, other): From 1c311279ee527a58f98138f4d830e8e1f9bbe446 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Mon, 3 Oct 2022 17:09:45 +0300 Subject: [PATCH 35/73] improve markdown format --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b77db66..7070a5f 100644 --- a/README.md +++ b/README.md @@ -52,19 +52,19 @@ You can find a tutorial under the `examples/v2/tutorial` folder. To run a step use `python ` such as `python 01_generate_account.py`. #### Prerequisites -1. Generating an account (`01_generate_account.py`) -2. Creating 2 assets (`02_create_assets.by`) +1. [Generating an account](examples/v2/01_generate_account.py) +2. [Creating assets](examples/v2/02_create_assets.by) #### Steps -3. Bootstrapping a pool (`03_bootstrap_pool.py`) -4. Adding initial liquidity to the pool (`04_add_initial_liquidity.py`) -5. Adding flexible (add two asset with a flexible rate) liquidity to the pool (`05_add_flexible_liquidity.py`) -6. Adding single asset (add only one asset) liquidity to the pool(`06_add_single_asset_liquidity.py`) -7. Removing liquidity to the pool(`07_remove_liquidity.py`) -8. Removing single asset (receive single asset) liquidity to the pool(`08_single_asset_remove_liquidity.py`) -9. Swapping fixed-input (`09_fixed_input_swap.py`) -10. Swapping fixed-output (`10_fixed_output_swap.py`) +3. [Bootstrapping a pool](examples/v2/03_bootstrap_pool.py) +4. [Adding initial liquidity to the pool](examples/v2/04_add_initial_liquidity.py) +5. [Adding flexible (add two asset with a flexible rate) liquidity to the pool](examples/v2/05_add_flexible_liquidity.py) +6. [Adding single asset (add only one asset) liquidity to the pool](examples/v2/06_add_single_asset_liquidity.py) +7. [Removing liquidity to the pool](examples/v2/07_remove_liquidity.py) +8. [Removing single asset(receive single asset) liquidity to the pool](examples/v2/08_single_asset_remove_liquidity.py) +9. [Swapping fixed-input](examples/v2/09_fixed_input_swap.py) +10. [Swapping fixed-output](examples/v2/10_fixed_output_swap.py) ## Example Operations From b8ce1c48fa57da0b356d11bff3e66a83ec5ef8f8 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 5 Oct 2022 17:54:13 +0300 Subject: [PATCH 36/73] add flash loan & swap functions --- .../tutorial/11_flash_loan_1_single_asset.py | 65 ++++ .../12_flash_loan_2_multiple_assets.py | 74 +++++ examples/v2/tutorial/13_flash_swap_1.py | 80 +++++ examples/v2/tutorial/14_flash_swap_2.py | 80 +++++ tinyman/utils.py | 4 +- tinyman/v2/flash_loan.py | 108 +++++++ tinyman/v2/flash_swap.py | 113 +++++++ tinyman/v2/formulas.py | 93 +++++- tinyman/v2/pools.py | 304 ++++++++++++++---- tinyman/v2/quotes.py | 12 +- 10 files changed, 856 insertions(+), 77 deletions(-) create mode 100644 examples/v2/tutorial/11_flash_loan_1_single_asset.py create mode 100644 examples/v2/tutorial/12_flash_loan_2_multiple_assets.py create mode 100644 examples/v2/tutorial/13_flash_swap_1.py create mode 100644 examples/v2/tutorial/14_flash_swap_2.py create mode 100644 tinyman/v2/flash_loan.py create mode 100644 tinyman/v2/flash_swap.py diff --git a/examples/v2/tutorial/11_flash_loan_1_single_asset.py b/examples/v2/tutorial/11_flash_loan_1_single_asset.py new file mode 100644 index 0000000..fd57254 --- /dev/null +++ b/examples/v2/tutorial/11_flash_loan_1_single_asset.py @@ -0,0 +1,65 @@ +# This sample is provided for demonstration purposes only. +# It is not intended for production use. +# This example does not constitute trading advice. +from pprint import pprint +from urllib.parse import quote_plus + +from tinyman.assets import AssetAmount + +from examples.v2.tutorial.common import get_account, get_assets +from examples.v2.utils import get_algod +from tinyman.v2.client import TinymanV2TestnetClient +from algosdk.future.transaction import AssetTransferTxn + +account = get_account() +algod = get_algod() +client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) + +ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] +ASSET_A = client.fetch_asset(ASSET_A_ID) +ASSET_B = client.fetch_asset(ASSET_B_ID) +pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) + +position = pool.fetch_pool_position() + +quote = pool.fetch_flash_loan_quote( + loan_amount_a=AssetAmount(pool.asset_1, 1_000_000), + loan_amount_b=AssetAmount(pool.asset_2, 0), +) + +print("\nQuote:") +print(quote) + +account_info = algod.account_info(account["address"]) + +for asset in account_info["assets"]: + if asset["asset-id"] == pool.asset_1.id: + asset_1_balance = asset["amount"] + +# Transfer amount is equal to sum of initial account balance and loan amount +# this transaction demonstrates that you can use the total amount +transactions = [ + AssetTransferTxn( + sender=account["address"], + sp=algod.suggested_params(), + receiver=account["address"], + amt=asset_1_balance + quote.amounts_out[pool.asset_1].amount, + index=pool.asset_1.id, + ) +] + +txn_group = pool.prepare_flash_loan_transactions_from_quote( + quote=quote, transactions=transactions +) + +# Sign +txn_group.sign_with_private_key(account["address"], account["private_key"]) + +# Submit transactions to the network and wait for confirmation +txn_info = txn_group.submit(algod, wait=True) +print("Transaction Info") +pprint(txn_info) + +print( + f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" +) diff --git a/examples/v2/tutorial/12_flash_loan_2_multiple_assets.py b/examples/v2/tutorial/12_flash_loan_2_multiple_assets.py new file mode 100644 index 0000000..4de2ebe --- /dev/null +++ b/examples/v2/tutorial/12_flash_loan_2_multiple_assets.py @@ -0,0 +1,74 @@ +# This sample is provided for demonstration purposes only. +# It is not intended for production use. +# This example does not constitute trading advice. +from pprint import pprint +from urllib.parse import quote_plus + +from tinyman.assets import AssetAmount + +from examples.v2.tutorial.common import get_account, get_assets +from examples.v2.utils import get_algod +from tinyman.v2.client import TinymanV2TestnetClient +from algosdk.future.transaction import AssetTransferTxn + +account = get_account() +algod = get_algod() +client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) + +ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] +ASSET_A = client.fetch_asset(ASSET_A_ID) +ASSET_B = client.fetch_asset(ASSET_B_ID) +pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) + +position = pool.fetch_pool_position() + +quote = pool.fetch_flash_loan_quote( + loan_amount_a=AssetAmount(pool.asset_1, 3_000_000), + loan_amount_b=AssetAmount(pool.asset_2, 2_000_000), +) + +print("\nQuote:") +print(quote) + +account_info = algod.account_info(account["address"]) + +for asset in account_info["assets"]: + if asset["asset-id"] == pool.asset_1.id: + asset_1_balance = asset["amount"] + if asset["asset-id"] == pool.asset_1.id: + asset_2_balance = asset["amount"] + +# Transfer amounts are equal to sum of initial account balance and loan amount +# These transactions demonstrate that you can use the total amount +transactions = [ + AssetTransferTxn( + sender=account["address"], + sp=algod.suggested_params(), + receiver=account["address"], + amt=asset_1_balance + quote.amounts_out[pool.asset_1].amount, + index=pool.asset_1.id, + ), + AssetTransferTxn( + sender=account["address"], + sp=algod.suggested_params(), + receiver=account["address"], + amt=asset_2_balance + quote.amounts_out[pool.asset_2].amount, + index=pool.asset_2.id, + ), +] + +txn_group = pool.prepare_flash_loan_transactions_from_quote( + quote=quote, transactions=transactions +) + +# Sign +txn_group.sign_with_private_key(account["address"], account["private_key"]) + +# Submit transactions to the network and wait for confirmation +txn_info = txn_group.submit(algod, wait=True) +print("Transaction Info") +pprint(txn_info) + +print( + f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" +) diff --git a/examples/v2/tutorial/13_flash_swap_1.py b/examples/v2/tutorial/13_flash_swap_1.py new file mode 100644 index 0000000..58bd2d5 --- /dev/null +++ b/examples/v2/tutorial/13_flash_swap_1.py @@ -0,0 +1,80 @@ +# This sample is provided for demonstration purposes only. +# It is not intended for production use. +# This example does not constitute trading advice. +from pprint import pprint +from urllib.parse import quote_plus + +from algosdk.future.transaction import AssetTransferTxn + +from examples.v2.tutorial.common import get_account, get_assets +from examples.v2.utils import get_algod +from tinyman.v2.client import TinymanV2TestnetClient +from tinyman.v2.flash_swap import prepare_flash_swap_transactions +from tinyman.v2.formulas import calculate_flash_swap_asset_2_payment_amount + +account = get_account() +algod = get_algod() +client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) + +ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] +ASSET_A = client.fetch_asset(ASSET_A_ID) +ASSET_B = client.fetch_asset(ASSET_B_ID) +pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) + +suggested_params = algod.suggested_params() +account_info = algod.account_info(account["address"]) + +for asset in account_info["assets"]: + if asset["asset-id"] == pool.asset_1.id: + balance = asset["amount"] + +asset_1_loan_amount = 1_000_000 +asset_2_loan_amount = 0 +asset_1_payment_amount = 0 +asset_2_payment_amount = calculate_flash_swap_asset_2_payment_amount( + asset_1_reserves=pool.asset_1_reserves, + asset_2_reserves=pool.asset_2_reserves, + total_fee_share=pool.total_fee_share, + protocol_fee_ratio=pool.protocol_fee_ratio, + asset_1_loan_amount=asset_1_loan_amount, + asset_2_loan_amount=asset_2_loan_amount, + asset_1_payment_amount=asset_1_payment_amount, +) + +# Transfer amount is equal to sum of initial account balance and loan amount +# This transaction demonstrate that you can use the total amount +transfer_amount = balance + asset_1_loan_amount +transactions = [ + AssetTransferTxn( + sender=account["address"], + sp=suggested_params, + receiver=account["address"], + amt=transfer_amount, + index=pool.asset_1.id, + ) +] + +txn_group = prepare_flash_swap_transactions( + validator_app_id=pool.validator_app_id, + asset_1_id=pool.asset_1.id, + asset_2_id=pool.asset_2.id, + asset_1_loan_amount=asset_1_loan_amount, + asset_2_loan_amount=asset_2_loan_amount, + asset_1_payment_amount=asset_1_payment_amount, + asset_2_payment_amount=asset_2_payment_amount, + transactions=transactions, + suggested_params=suggested_params, + sender=account["address"], +) + +# Sign +txn_group.sign_with_private_key(account["address"], account["private_key"]) + +# Submit transactions to the network and wait for confirmation +txn_info = txn_group.submit(algod, wait=True) +print("Transaction Info") +pprint(txn_info) + +print( + f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" +) diff --git a/examples/v2/tutorial/14_flash_swap_2.py b/examples/v2/tutorial/14_flash_swap_2.py new file mode 100644 index 0000000..532756b --- /dev/null +++ b/examples/v2/tutorial/14_flash_swap_2.py @@ -0,0 +1,80 @@ +# This sample is provided for demonstration purposes only. +# It is not intended for production use. +# This example does not constitute trading advice. +from pprint import pprint +from urllib.parse import quote_plus + +from algosdk.future.transaction import AssetTransferTxn + +from examples.v2.tutorial.common import get_account, get_assets +from examples.v2.utils import get_algod +from tinyman.v2.client import TinymanV2TestnetClient +from tinyman.v2.flash_swap import prepare_flash_swap_transactions +from tinyman.v2.formulas import calculate_flash_swap_asset_1_payment_amount + +account = get_account() +algod = get_algod() +client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) + +ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] +ASSET_A = client.fetch_asset(ASSET_A_ID) +ASSET_B = client.fetch_asset(ASSET_B_ID) +pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) + +suggested_params = algod.suggested_params() +account_info = algod.account_info(account["address"]) + +for asset in account_info["assets"]: + if asset["asset-id"] == pool.asset_1.id: + balance = asset["amount"] + +asset_1_loan_amount = 1_000_000 +asset_2_loan_amount = 0 +asset_2_payment_amount = 0 +asset_1_payment_amount = calculate_flash_swap_asset_1_payment_amount( + asset_1_reserves=pool.asset_1_reserves, + asset_2_reserves=pool.asset_2_reserves, + total_fee_share=pool.total_fee_share, + protocol_fee_ratio=pool.protocol_fee_ratio, + asset_1_loan_amount=asset_1_loan_amount, + asset_2_loan_amount=asset_2_loan_amount, + asset_2_payment_amount=asset_2_payment_amount, +) + +# Transfer amount is equal to sum of initial account balance and loan amount +# This transaction demonstrate that you can use the total amount +transfer_amount = balance + asset_1_loan_amount +transactions = [ + AssetTransferTxn( + sender=account["address"], + sp=suggested_params, + receiver=account["address"], + amt=transfer_amount, + index=pool.asset_1.id, + ) +] + +txn_group = prepare_flash_swap_transactions( + validator_app_id=pool.validator_app_id, + asset_1_id=pool.asset_1.id, + asset_2_id=pool.asset_2.id, + asset_1_loan_amount=asset_1_loan_amount, + asset_2_loan_amount=asset_2_loan_amount, + asset_1_payment_amount=asset_1_payment_amount, + asset_2_payment_amount=asset_2_payment_amount, + transactions=transactions, + suggested_params=suggested_params, + sender=account["address"], +) + +# Sign +txn_group.sign_with_private_key(account["address"], account["private_key"]) + +# Submit transactions to the network and wait for confirmation +txn_info = txn_group.submit(algod, wait=True) +print("Transaction Info") +pprint(txn_info) + +print( + f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" +) diff --git a/tinyman/utils.py b/tinyman/utils.py index 4a110fb..c002ed0 100644 --- a/tinyman/utils.py +++ b/tinyman/utils.py @@ -121,6 +121,8 @@ def calculate_price_impact( class TransactionGroup: def __init__(self, transactions): + for txn in transactions: + txn.group = None transactions = assign_group_id(transactions) self.transactions = transactions self.signed_transactions = [None for _ in self.transactions] @@ -159,6 +161,4 @@ def submit(self, algod, wait=False): def __add__(self, other): transactions = self.transactions + other.transactions - for txn in transactions: - txn.group = None return TransactionGroup(transactions) diff --git a/tinyman/v2/flash_loan.py b/tinyman/v2/flash_loan.py new file mode 100644 index 0000000..11e4192 --- /dev/null +++ b/tinyman/v2/flash_loan.py @@ -0,0 +1,108 @@ +from algosdk.future.transaction import ( + Transaction, + ApplicationNoOpTxn, + PaymentTxn, + AssetTransferTxn, + SuggestedParams, +) + +from tinyman.utils import TransactionGroup +from .constants import ( + FLASH_LOAN_APP_ARGUMENT, + VERIFY_FLASH_LOAN_APP_ARGUMENT, +) +from .contracts import get_pool_logicsig + + +def prepare_flash_loan_transactions( + validator_app_id: int, + asset_1_id: int, + asset_2_id: int, + asset_1_loan_amount: int, + asset_2_loan_amount: int, + asset_1_payment_amount: int, + asset_2_payment_amount: int, + transactions: list[Transaction], + sender: str, + suggested_params: SuggestedParams, +) -> TransactionGroup: + assert asset_1_loan_amount or asset_2_loan_amount + + pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) + pool_address = pool_logicsig.address() + min_fee = suggested_params.min_fee + + if asset_1_loan_amount and asset_2_loan_amount: + payment_count = inner_transaction_count = 2 + else: + payment_count = inner_transaction_count = 1 + + index_diff = len(transactions) + payment_count + 1 + txns = [ + # Flash Loan + ApplicationNoOpTxn( + sender=sender, + sp=suggested_params, + index=validator_app_id, + app_args=[ + FLASH_LOAN_APP_ARGUMENT, + index_diff, + asset_1_loan_amount, + asset_2_loan_amount, + ], + foreign_assets=[asset_1_id, asset_2_id], + accounts=[pool_address], + ) + ] + # This app call contains inner transactions + txns[0].fee = min_fee * (inner_transaction_count + 1) + + if transactions: + txns.extend(transactions) + + if asset_1_loan_amount: + txns.append( + AssetTransferTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + index=asset_1_id, + amt=asset_1_payment_amount, + ) + ) + + if asset_2_loan_amount: + if asset_2_id: + txns.append( + AssetTransferTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + index=asset_2_id, + amt=asset_2_payment_amount, + ) + ) + else: + txns.append( + PaymentTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + amt=asset_2_payment_amount, + ) + ) + + # Verify Flash Loan + txns.append( + ApplicationNoOpTxn( + sender=sender, + sp=suggested_params, + index=validator_app_id, + app_args=[VERIFY_FLASH_LOAN_APP_ARGUMENT, index_diff], + foreign_assets=[], + accounts=[pool_address], + ) + ) + + txn_group = TransactionGroup(txns) + return txn_group diff --git a/tinyman/v2/flash_swap.py b/tinyman/v2/flash_swap.py new file mode 100644 index 0000000..7fb6178 --- /dev/null +++ b/tinyman/v2/flash_swap.py @@ -0,0 +1,113 @@ +from algosdk.future.transaction import ( + Transaction, + ApplicationNoOpTxn, + PaymentTxn, + AssetTransferTxn, + SuggestedParams, +) + +from tinyman.utils import TransactionGroup +from .constants import ( + FLASH_SWAP_APP_ARGUMENT, + VERIFY_FLASH_SWAP_APP_ARGUMENT, +) +from .contracts import get_pool_logicsig + + +def prepare_flash_swap_transactions( + validator_app_id: int, + asset_1_id: int, + asset_2_id: int, + asset_1_loan_amount: int, + asset_2_loan_amount: int, + asset_1_payment_amount: int, + asset_2_payment_amount: int, + transactions: list[Transaction], + sender: str, + suggested_params: SuggestedParams, +) -> TransactionGroup: + assert asset_1_loan_amount or asset_2_loan_amount + + pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) + pool_address = pool_logicsig.address() + min_fee = suggested_params.min_fee + + if asset_1_loan_amount and asset_2_loan_amount: + inner_transaction_count = 2 + else: + inner_transaction_count = 1 + + if asset_1_payment_amount and asset_2_payment_amount: + payment_count = 2 + else: + payment_count = 1 + + index_diff = len(transactions) + payment_count + 1 + txns = [ + # Flash Swap + ApplicationNoOpTxn( + sender=sender, + sp=suggested_params, + index=validator_app_id, + app_args=[ + FLASH_SWAP_APP_ARGUMENT, + index_diff, + asset_1_loan_amount, + asset_2_loan_amount, + ], + foreign_assets=[asset_1_id, asset_2_id], + accounts=[pool_address], + ) + ] + # This app call contains inner transactions + txns[0].fee = min_fee * (inner_transaction_count + 1) + + if transactions: + txns.extend(transactions) + + if asset_1_payment_amount: + txns.append( + AssetTransferTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + index=asset_1_id, + amt=asset_1_payment_amount, + ) + ) + + if asset_2_payment_amount: + if asset_2_id: + txns.append( + AssetTransferTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + index=asset_2_id, + amt=asset_2_payment_amount, + ) + ) + else: + txns.append( + PaymentTxn( + sender=sender, + sp=suggested_params, + receiver=pool_address, + amt=asset_2_payment_amount, + ) + ) + + # Verify Flash Swap + txns.append( + ApplicationNoOpTxn( + sender=sender, + sp=suggested_params, + index=validator_app_id, + app_args=[VERIFY_FLASH_SWAP_APP_ARGUMENT, index_diff], + foreign_assets=[asset_1_id, asset_2_id], + accounts=[pool_address], + ) + ) + + txn_group = TransactionGroup(txns) + return txn_group diff --git a/tinyman/v2/formulas.py b/tinyman/v2/formulas.py index 394feed..9ded684 100644 --- a/tinyman/v2/formulas.py +++ b/tinyman/v2/formulas.py @@ -20,12 +20,12 @@ def calculate_poolers_fee_amount(total_fee_amount: int, protocol_fee_ratio: int) def calculate_fixed_input_fee_amount(input_amount: int, total_fee_share: int) -> int: - total_fee_amount = (input_amount * total_fee_share) // 10000 + total_fee_amount = (input_amount * total_fee_share) // 10_000 return total_fee_amount def calculate_fixed_output_fee_amounts(swap_amount: int, total_fee_share: int) -> int: - input_amount = (swap_amount * 10000) // (10000 - total_fee_share) + input_amount = (swap_amount * 10_000) // (10_000 - total_fee_share) total_fee_amount = input_amount - swap_amount return total_fee_amount @@ -35,6 +35,95 @@ def calculate_internal_swap_fee_amount(swap_amount: int, total_fee_share: int) - return total_fee_amount +def calculate_flash_loan_payment_amount(loan_amount: int, total_fee_share: int) -> int: + total_fee_amount = calculate_fixed_input_fee_amount(loan_amount, total_fee_share) + payment_amount = loan_amount + total_fee_amount + return payment_amount + + +def calculate_flash_swap_asset_2_payment_amount( + asset_1_reserves: int, + asset_2_reserves: int, + total_fee_share: int, + protocol_fee_ratio: int, + asset_1_loan_amount: int, + asset_2_loan_amount: int, + asset_1_payment_amount: int, +) -> int: + k = asset_1_reserves * asset_2_reserves + asset_1_total_fee_amount = calculate_fixed_input_fee_amount( + asset_1_payment_amount, total_fee_share + ) + asset_1_protocol_fee_amount = calculate_protocol_fee_amount( + asset_1_total_fee_amount, protocol_fee_ratio + ) + asset_1_poolers_fee_amount = calculate_poolers_fee_amount( + asset_1_total_fee_amount, protocol_fee_ratio + ) + + final_asset_1_reserves = (asset_1_reserves - asset_1_loan_amount) + ( + asset_1_payment_amount - asset_1_protocol_fee_amount + ) + final_asset_1_reserves_without_poolers_fee = ( + final_asset_1_reserves - asset_1_poolers_fee_amount + ) + minimum_final_asset_2_reserves_without_poolers_fee = ( + k / final_asset_1_reserves_without_poolers_fee + ) + minimum_asset_2_payment_amount_without_poolers_fee = ( + minimum_final_asset_2_reserves_without_poolers_fee + - (asset_2_reserves - asset_2_loan_amount) + ) + minimum_asset_2_payment_amount = math.ceil( + minimum_asset_2_payment_amount_without_poolers_fee + * 10_000 + / (10_000 - total_fee_share) + ) + return minimum_asset_2_payment_amount + + +def calculate_flash_swap_asset_1_payment_amount( + asset_1_reserves: int, + asset_2_reserves: int, + total_fee_share: int, + protocol_fee_ratio: int, + asset_1_loan_amount: int, + asset_2_loan_amount: int, + asset_2_payment_amount: int, +) -> int: + k = asset_1_reserves * asset_2_reserves + asset_2_total_fee_amount = calculate_fixed_input_fee_amount( + asset_2_payment_amount, total_fee_share + ) + asset_2_protocol_fee_amount = calculate_protocol_fee_amount( + asset_2_total_fee_amount, protocol_fee_ratio + ) + asset_2_poolers_fee_amount = calculate_poolers_fee_amount( + asset_2_total_fee_amount, protocol_fee_ratio + ) + + final_asset_2_reserves = (asset_2_reserves - asset_2_loan_amount) + ( + asset_2_payment_amount - asset_2_protocol_fee_amount + ) + final_asset_2_reserves_without_poolers_fee = ( + final_asset_2_reserves - asset_2_poolers_fee_amount + ) + minimum_final_asset_1_reserves_without_poolers_fee = ( + k / final_asset_2_reserves_without_poolers_fee + ) + minimum_asset_1_payment_amount_without_poolers_fee = ( + minimum_final_asset_1_reserves_without_poolers_fee + - (asset_1_reserves - asset_1_loan_amount) + ) + + minimum_asset_1_payment_amount = math.ceil( + minimum_asset_1_payment_amount_without_poolers_fee + * 10_000 + / (10_000 - total_fee_share) + ) + return minimum_asset_1_payment_amount + + def calculate_initial_add_liquidity(asset_1_amount: int, asset_2_amount: int) -> int: assert bool(asset_1_amount) and bool( asset_2_amount diff --git a/tinyman/v2/pools.py b/tinyman/v2/pools.py index cc03b31..5b626ad 100644 --- a/tinyman/v2/pools.py +++ b/tinyman/v2/pools.py @@ -1,6 +1,6 @@ from typing import Optional -from algosdk.future.transaction import LogicSigAccount +from algosdk.future.transaction import LogicSigAccount, Transaction, SuggestedParams from algosdk.v2client.algod import AlgodClient from tinyman.assets import Asset, AssetAmount @@ -21,12 +21,14 @@ PoolHasNoLiquidity, PoolAlreadyHasLiquidity, ) +from .flash_loan import prepare_flash_loan_transactions from .formulas import ( calculate_subsequent_add_liquidity, calculate_initial_add_liquidity, calculate_fixed_input_swap, calculate_remove_liquidity_output_amounts, calculate_fixed_output_swap, + calculate_flash_loan_payment_amount, ) from .quotes import ( FlexibleAddLiquidityQuote, @@ -36,6 +38,7 @@ InternalSwapQuote, SingleAssetRemoveLiquidityQuote, SwapQuote, + FlashLoanQuote, ) from .remove_liquidity import ( prepare_remove_liquidity_transactions, @@ -268,16 +271,55 @@ def convert(self, amount: AssetAmount) -> AssetAmount: raise NotImplementedError() + def prepare_pool_token_asset_optin_transactions( + self, + user_address: Optional[str] = None, + suggested_params: SuggestedParams = None, + ) -> TransactionGroup: + user_address = user_address or self.client.user_address + + if suggested_params is None: + suggested_params = self.client.algod.suggested_params() + + txn_group = prepare_asset_optin_transactions( + asset_id=self.pool_token_asset.id, + sender=user_address, + suggested_params=suggested_params, + ) + return txn_group + + def fetch_pool_position(self, user_address: Optional[str] = None) -> dict: + user_address = user_address or self.client.user_address + account_info = self.client.algod.account_info(user_address) + assets = {a["asset-id"]: a for a in account_info["assets"]} + pool_token_asset_amount = assets.get(self.pool_token_asset.id, {}).get( + "amount", 0 + ) + quote = self.fetch_remove_liquidity_quote(pool_token_asset_amount) + return { + self.asset_1: quote.amounts_out[self.asset_1], + self.asset_2: quote.amounts_out[self.asset_2], + self.pool_token_asset: quote.pool_token_asset_amount, + "share": (pool_token_asset_amount / self.issued_pool_tokens), + } + def prepare_bootstrap_transactions( - self, pooler_address: Optional[str] = None + self, + user_address: Optional[str] = None, + refresh: bool = True, + suggested_params: SuggestedParams = None, ) -> TransactionGroup: - self.refresh() + + if refresh: + self.refresh() if self.exists: raise AlreadyBootstrapped() - pooler_address = pooler_address or self.client.user_address - suggested_params = self.client.algod.suggested_params() + user_address = user_address or self.client.user_address + + if suggested_params is None: + suggested_params = self.client.algod.suggested_params() if self.asset_2.id == 0: pool_minimum_balance = MIN_POOL_BALANCE_ASA_ALGO_PAIR @@ -300,7 +342,7 @@ def prepare_bootstrap_transactions( validator_app_id=self.validator_app_id, asset_1_id=self.asset_1.id, asset_2_id=self.asset_2.id, - sender=pooler_address, + sender=user_address, suggested_params=suggested_params, app_call_fee=app_call_fee, required_algo=required_algo, @@ -308,7 +350,11 @@ def prepare_bootstrap_transactions( return txn_group def fetch_flexible_add_liquidity_quote( - self, amount_a: AssetAmount, amount_b: AssetAmount, slippage: float = 0.05 + self, + amount_a: AssetAmount, + amount_b: AssetAmount, + slippage: float = 0.05, + refresh: bool = True, ) -> FlexibleAddLiquidityQuote: assert {self.asset_1, self.asset_2} == { amount_a.asset, @@ -317,7 +363,9 @@ def fetch_flexible_add_liquidity_quote( amount_1 = amount_a if amount_a.asset == self.asset_1 else amount_b amount_2 = amount_a if amount_a.asset == self.asset_2 else amount_b - self.refresh() + + if refresh: + self.refresh() if not self.exists: raise BootstrapIsRequired() @@ -371,9 +419,11 @@ def fetch_flexible_add_liquidity_quote( return quote def fetch_single_asset_add_liquidity_quote( - self, amount_a: AssetAmount, slippage: float = 0.05 + self, amount_a: AssetAmount, slippage: float = 0.05, refresh: bool = True ) -> SingleAssetAddLiquidityQuote: - self.refresh() + if refresh: + self.refresh() + if not self.exists: raise BootstrapIsRequired() @@ -444,6 +494,7 @@ def fetch_initial_add_liquidity_quote( self, amount_a: AssetAmount, amount_b: AssetAmount, + refresh: bool = True, ) -> InitialAddLiquidityQuote: assert {self.asset_1, self.asset_2} == { amount_a.asset, @@ -452,7 +503,9 @@ def fetch_initial_add_liquidity_quote( amount_1 = amount_a if amount_a.asset == self.asset_1 else amount_b amount_2 = amount_a if amount_a.asset == self.asset_2 else amount_b - self.refresh() + + if refresh: + self.refresh() if not self.exists: raise BootstrapIsRequired() @@ -478,12 +531,15 @@ def prepare_flexible_add_liquidity_transactions( self, amounts_in: dict[Asset, AssetAmount], min_pool_token_asset_amount: int, - pooler_address: Optional[str] = None, + user_address: Optional[str] = None, + suggested_params: SuggestedParams = None, ) -> TransactionGroup: - pooler_address = pooler_address or self.client.user_address + user_address = user_address or self.client.user_address asset_1_amount = amounts_in[self.asset_1] asset_2_amount = amounts_in[self.asset_2] - suggested_params = self.client.algod.suggested_params() + + if suggested_params is None: + suggested_params = self.client.algod.suggested_params() txn_group = prepare_flexible_add_liquidity_transactions( validator_app_id=self.validator_app_id, @@ -493,7 +549,7 @@ def prepare_flexible_add_liquidity_transactions( asset_1_amount=asset_1_amount.amount, asset_2_amount=asset_2_amount.amount, min_pool_token_asset_amount=min_pool_token_asset_amount, - sender=pooler_address, + sender=user_address, suggested_params=suggested_params, ) return txn_group @@ -502,10 +558,13 @@ def prepare_single_asset_add_liquidity_transactions( self, amount_in: AssetAmount, min_pool_token_asset_amount: Optional[int], - pooler_address: Optional[str] = None, + user_address: Optional[str] = None, + suggested_params: SuggestedParams = None, ) -> TransactionGroup: - pooler_address = pooler_address or self.client.user_address - suggested_params = self.client.algod.suggested_params() + user_address = user_address or self.client.user_address + + if suggested_params is None: + suggested_params = self.client.algod.suggested_params() txn_group = prepare_single_asset_add_liquidity_transactions( validator_app_id=self.validator_app_id, @@ -519,7 +578,7 @@ def prepare_single_asset_add_liquidity_transactions( if amount_in.asset == self.asset_2 else None, min_pool_token_asset_amount=min_pool_token_asset_amount, - sender=pooler_address, + sender=user_address, suggested_params=suggested_params, ) return txn_group @@ -527,12 +586,15 @@ def prepare_single_asset_add_liquidity_transactions( def prepare_initial_add_liquidity_transactions( self, amounts_in: dict[Asset, AssetAmount], - pooler_address: Optional[str] = None, + user_address: Optional[str] = None, + suggested_params: SuggestedParams = None, ) -> TransactionGroup: - pooler_address = pooler_address or self.client.user_address + user_address = user_address or self.client.user_address asset_1_amount = amounts_in[self.asset_1] asset_2_amount = amounts_in[self.asset_2] - suggested_params = self.client.algod.suggested_params() + + if suggested_params is None: + suggested_params = self.client.algod.suggested_params() txn_group = prepare_initial_add_liquidity_transactions( validator_app_id=self.validator_app_id, @@ -541,7 +603,7 @@ def prepare_initial_add_liquidity_transactions( pool_token_asset_id=self.pool_token_asset.id, asset_1_amount=asset_1_amount.amount, asset_2_amount=asset_2_amount.amount, - sender=pooler_address, + sender=user_address, suggested_params=suggested_params, ) return txn_group @@ -553,30 +615,33 @@ def prepare_add_liquidity_transactions_from_quote( SingleAssetAddLiquidityQuote, InitialAddLiquidityQuote, ], - pooler_address: Optional[str] = None, + user_address: Optional[str] = None, ) -> TransactionGroup: if isinstance(quote, FlexibleAddLiquidityQuote): return self.prepare_flexible_add_liquidity_transactions( amounts_in=quote.amounts_in, min_pool_token_asset_amount=quote.min_pool_token_asset_amount_with_slippage, - pooler_address=pooler_address, + user_address=user_address, ) elif isinstance(quote, SingleAssetAddLiquidityQuote): return self.prepare_single_asset_add_liquidity_transactions( amount_in=quote.amount_in, min_pool_token_asset_amount=quote.min_pool_token_asset_amount_with_slippage, - pooler_address=pooler_address, + user_address=user_address, ) elif isinstance(quote, InitialAddLiquidityQuote): return self.prepare_initial_add_liquidity_transactions( amounts_in=quote.amounts_in, - pooler_address=pooler_address, + user_address=user_address, ) raise Exception(f"Invalid quote type({type(quote)})") def fetch_remove_liquidity_quote( - self, pool_token_asset_in: [AssetAmount, int], slippage: float = 0.05 + self, + pool_token_asset_in: [AssetAmount, int], + slippage: float = 0.05, + refresh: bool = True, ) -> RemoveLiquidityQuote: if not self.exists: raise BootstrapIsRequired() @@ -586,7 +651,9 @@ def fetch_remove_liquidity_quote( self.pool_token_asset, pool_token_asset_in ) - self.refresh() + if refresh: + self.refresh() + ( asset_1_output_amount, asset_2_output_amount, @@ -611,6 +678,7 @@ def fetch_single_asset_remove_liquidity_quote( pool_token_asset_in: [AssetAmount, int], output_asset: Asset, slippage: float = 0.05, + refresh: bool = True, ) -> SingleAssetRemoveLiquidityQuote: if not self.exists: raise BootstrapIsRequired() @@ -620,7 +688,9 @@ def fetch_single_asset_remove_liquidity_quote( self.pool_token_asset, pool_token_asset_in ) - self.refresh() + if refresh: + self.refresh() + ( asset_1_output_amount, asset_2_output_amount, @@ -690,17 +760,21 @@ def prepare_remove_liquidity_transactions( self, pool_token_asset_amount: [AssetAmount, int], amounts_out: dict[Asset, AssetAmount], - pooler_address: Optional[str] = None, + user_address: Optional[str] = None, + suggested_params: SuggestedParams = None, ) -> TransactionGroup: if isinstance(pool_token_asset_amount, int): pool_token_asset_amount = AssetAmount( self.pool_token_asset, pool_token_asset_amount ) - pooler_address = pooler_address or self.client.user_address + user_address = user_address or self.client.user_address asset_1_amount = amounts_out[self.asset_1] asset_2_amount = amounts_out[self.asset_2] - suggested_params = self.client.algod.suggested_params() + + if suggested_params is None: + suggested_params = self.client.algod.suggested_params() + txn_group = prepare_remove_liquidity_transactions( validator_app_id=self.validator_app_id, asset_1_id=self.asset_1.id, @@ -709,7 +783,7 @@ def prepare_remove_liquidity_transactions( min_asset_1_amount=asset_1_amount.amount, min_asset_2_amount=asset_2_amount.amount, pool_token_asset_amount=pool_token_asset_amount.amount, - sender=pooler_address, + sender=user_address, suggested_params=suggested_params, ) return txn_group @@ -718,15 +792,19 @@ def prepare_single_asset_remove_liquidity_transactions( self, pool_token_asset_amount: [AssetAmount, int], amount_out: AssetAmount, - pooler_address: Optional[str] = None, + user_address: Optional[str] = None, + suggested_params: SuggestedParams = None, ) -> TransactionGroup: if isinstance(pool_token_asset_amount, int): pool_token_asset_amount = AssetAmount( self.pool_token_asset, pool_token_asset_amount ) - pooler_address = pooler_address or self.client.user_address - suggested_params = self.client.algod.suggested_params() + user_address = user_address or self.client.user_address + + if suggested_params is None: + suggested_params = self.client.algod.suggested_params() + txn_group = prepare_single_asset_remove_liquidity_transactions( validator_app_id=self.validator_app_id, asset_1_id=self.asset_1.id, @@ -735,7 +813,7 @@ def prepare_single_asset_remove_liquidity_transactions( output_asset_id=amount_out.asset.id, min_output_asset_amount=amount_out.amount, pool_token_asset_amount=pool_token_asset_amount.amount, - sender=pooler_address, + sender=user_address, suggested_params=suggested_params, ) return txn_group @@ -743,15 +821,15 @@ def prepare_single_asset_remove_liquidity_transactions( def prepare_remove_liquidity_transactions_from_quote( self, quote: [RemoveLiquidityQuote, SingleAssetRemoveLiquidityQuote], - pooler_address: Optional[str] = None, + user_address: Optional[str] = None, ) -> TransactionGroup: - pooler_address = pooler_address or self.client.user_address + user_address = user_address or self.client.user_address if isinstance(quote, SingleAssetRemoveLiquidityQuote): return self.prepare_single_asset_remove_liquidity_transactions( pool_token_asset_amount=quote.pool_token_asset_amount, amount_out=quote.amount_out_with_slippage, - pooler_address=pooler_address, + user_address=user_address, ) elif isinstance(quote, RemoveLiquidityQuote): return self.prepare_remove_liquidity_transactions( @@ -760,15 +838,17 @@ def prepare_remove_liquidity_transactions_from_quote( self.asset_1: quote.amounts_out_with_slippage[self.asset_1], self.asset_2: quote.amounts_out_with_slippage[self.asset_2], }, - pooler_address=pooler_address, + user_address=user_address, ) raise NotImplementedError() def fetch_fixed_input_swap_quote( - self, amount_in: AssetAmount, slippage: float = 0.05 + self, amount_in: AssetAmount, slippage: float = 0.05, refresh: bool = True ) -> SwapQuote: - self.refresh() + if refresh: + self.refresh() + if not self.exists: raise BootstrapIsRequired() @@ -805,9 +885,11 @@ def fetch_fixed_input_swap_quote( return quote def fetch_fixed_output_swap_quote( - self, amount_out: AssetAmount, slippage: float = 0.05 + self, amount_out: AssetAmount, slippage: float = 0.05, refresh: bool = True ) -> SwapQuote: - self.refresh() + if refresh: + self.refresh() + if not self.exists: raise BootstrapIsRequired() @@ -848,10 +930,13 @@ def prepare_swap_transactions( amount_in: AssetAmount, amount_out: AssetAmount, swap_type: [str, bytes], - swapper_address=None, + user_address: str = None, + suggested_params: SuggestedParams = None, ) -> TransactionGroup: - swapper_address = swapper_address or self.client.user_address - suggested_params = self.client.algod.suggested_params() + user_address = user_address or self.client.user_address + + if suggested_params is None: + suggested_params = self.client.algod.suggested_params() txn_group = prepare_swap_transactions( validator_app_id=self.validator_app_id, @@ -861,44 +946,123 @@ def prepare_swap_transactions( asset_in_amount=amount_in.amount, asset_out_amount=amount_out.amount, swap_type=swap_type, - sender=swapper_address, + sender=user_address, suggested_params=suggested_params, ) return txn_group def prepare_swap_transactions_from_quote( - self, quote: SwapQuote, swapper_address=None + self, quote: SwapQuote, user_address: str = None ) -> TransactionGroup: return self.prepare_swap_transactions( amount_in=quote.amount_in_with_slippage, amount_out=quote.amount_out_with_slippage, swap_type=quote.swap_type, - swapper_address=swapper_address, + user_address=user_address, ) - def prepare_pool_token_asset_optin_transactions( - self, user_address: Optional[str] = None + def fetch_flash_loan_quote( + self, + loan_amount_a: AssetAmount, + loan_amount_b: AssetAmount, + refresh: bool = True, + ) -> FlashLoanQuote: + assert {self.asset_1, self.asset_2} == { + loan_amount_a.asset, + loan_amount_b.asset, + }, "Pool assets and given assets don't match." + + if loan_amount_a.asset == self.asset_1: + loan_amount_1 = loan_amount_a + else: + loan_amount_1 = loan_amount_b + + if loan_amount_a.asset == self.asset_2: + loan_amount_2 = loan_amount_a + else: + loan_amount_2 = loan_amount_b + + if refresh: + self.refresh() + + if not self.exists: + raise BootstrapIsRequired() + + if not self.issued_pool_tokens: + raise PoolHasNoLiquidity() + + if loan_amount_1.amount > self.asset_1_reserves: + raise Exception( + f"The loan amount({loan_amount_1.amount}) cannot exceed the reserves." + ) + + if loan_amount_2.amount > self.asset_2_reserves: + raise Exception( + f"The loan amount({loan_amount_2.amount}) cannot exceed the reserves." + ) + + quote = FlashLoanQuote( + amounts_out={ + self.asset_1: loan_amount_1, + self.asset_2: loan_amount_2, + }, + amounts_in={ + self.asset_1: AssetAmount( + self.asset_1, + amount=calculate_flash_loan_payment_amount( + loan_amount=loan_amount_1.amount, + total_fee_share=self.total_fee_share, + ), + ), + self.asset_2: AssetAmount( + self.asset_2, + amount=calculate_flash_loan_payment_amount( + loan_amount=loan_amount_2.amount, + total_fee_share=self.total_fee_share, + ), + ), + }, + ) + return quote + + def prepare_flash_loan_transactions( + self, + amounts_out: dict[Asset, AssetAmount], + amounts_in: dict[Asset, AssetAmount], + transactions: list[Transaction], + user_address: str = None, + suggested_params: SuggestedParams = None, ) -> TransactionGroup: user_address = user_address or self.client.user_address - suggested_params = self.client.algod.suggested_params() - txn_group = prepare_asset_optin_transactions( - asset_id=self.pool_token_asset.id, + + if suggested_params is None: + suggested_params = self.client.algod.suggested_params() + + txn_group = prepare_flash_loan_transactions( + validator_app_id=self.validator_app_id, + asset_1_id=self.asset_1.id, + asset_2_id=self.asset_2.id, + asset_1_loan_amount=amounts_out[self.asset_1].amount, + asset_2_loan_amount=amounts_out[self.asset_2].amount, + asset_1_payment_amount=amounts_in[self.asset_1].amount, + asset_2_payment_amount=amounts_in[self.asset_2].amount, + transactions=transactions, sender=user_address, suggested_params=suggested_params, ) return txn_group - def fetch_pool_position(self, pooler_address: Optional[str] = None) -> dict: - pooler_address = pooler_address or self.client.user_address - account_info = self.client.algod.account_info(pooler_address) - assets = {a["asset-id"]: a for a in account_info["assets"]} - pool_token_asset_amount = assets.get(self.pool_token_asset.id, {}).get( - "amount", 0 + def prepare_flash_loan_transactions_from_quote( + self, + quote: FlashLoanQuote, + transactions: list[Transaction], + user_address: str = None, + ) -> TransactionGroup: + user_address = user_address or self.client.user_address + + return self.prepare_flash_loan_transactions( + amounts_out=quote.amounts_out, + amounts_in=quote.amounts_in, + transactions=transactions, + user_address=user_address, ) - quote = self.fetch_remove_liquidity_quote(pool_token_asset_amount) - return { - self.asset_1: quote.amounts_out[self.asset_1], - self.asset_2: quote.amounts_out[self.asset_2], - self.pool_token_asset: quote.pool_token_asset_amount, - "share": (pool_token_asset_amount / self.issued_pool_tokens), - } diff --git a/tinyman/v2/quotes.py b/tinyman/v2/quotes.py index 7511169..fcfee1f 100644 --- a/tinyman/v2/quotes.py +++ b/tinyman/v2/quotes.py @@ -98,13 +98,13 @@ class RemoveLiquidityQuote: @property def amounts_out_with_slippage(self) -> dict[Asset, AssetAmount]: - out = {} + amounts_out = {} for asset, asset_amount in self.amounts_out.items(): amount_with_slippage = asset_amount.amount - int( (asset_amount.amount * self.slippage) ) - out[asset] = AssetAmount(asset, amount_with_slippage) - return out + amounts_out[asset] = AssetAmount(asset, amount_with_slippage) + return amounts_out @dataclass @@ -120,3 +120,9 @@ def amount_out_with_slippage(self) -> AssetAmount: self.amount_out.amount * self.slippage ) return AssetAmount(self.amount_out.asset, amount_with_slippage) + + +@dataclass +class FlashLoanQuote: + amounts_out: dict[Asset, AssetAmount] + amounts_in: dict[Asset, AssetAmount] From 1de874d5141493cdb0ec4832378a13f8d69ac62a Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 5 Oct 2022 18:55:55 +0300 Subject: [PATCH 37/73] allow to initialize a pool with state dict --- tinyman/utils.py | 19 ------ tinyman/v1/utils.py | 20 ++++++ tinyman/v2/pools.py | 154 ++++++++++++++++++++++---------------------- tinyman/v2/utils.py | 21 ++++++ 4 files changed, 119 insertions(+), 95 deletions(-) create mode 100644 tinyman/v1/utils.py diff --git a/tinyman/utils.py b/tinyman/utils.py index c002ed0..308dd5d 100644 --- a/tinyman/utils.py +++ b/tinyman/utils.py @@ -71,25 +71,6 @@ def get_state_bytes(state, key): return state.get(key.decode(), {"bytes": ""})["bytes"] -def get_state_from_account_info(account_info, app_id): - try: - app = [a for a in account_info["apps-local-state"] if a["id"] == app_id][0] - except IndexError: - return {} - try: - app_state = {} - for x in app["key-value"]: - key = b64decode(x["key"]) - if x["value"]["type"] == 1: - value = b64decode(x["value"].get("bytes", "")) - else: - value = x["value"].get("uint", 0) - app_state[key] = value - except KeyError: - return {} - return app_state - - def apply_delta(state, delta): state = dict(state) for d in delta: diff --git a/tinyman/v1/utils.py b/tinyman/v1/utils.py new file mode 100644 index 0000000..dbad291 --- /dev/null +++ b/tinyman/v1/utils.py @@ -0,0 +1,20 @@ +from base64 import b64decode + + +def get_state_from_account_info(account_info, app_id): + try: + app = [a for a in account_info["apps-local-state"] if a["id"] == app_id][0] + except IndexError: + return {} + try: + app_state = {} + for x in app["key-value"]: + key = b64decode(x["key"]) + if x["value"]["type"] == 1: + value = b64decode(x["value"].get("bytes", "")) + else: + value = x["value"].get("uint", 0) + app_state[key] = value + except KeyError: + return {} + return app_state diff --git a/tinyman/v2/pools.py b/tinyman/v2/pools.py index 5b626ad..4081ff0 100644 --- a/tinyman/v2/pools.py +++ b/tinyman/v2/pools.py @@ -5,7 +5,7 @@ from tinyman.assets import Asset, AssetAmount from tinyman.optin import prepare_asset_optin_transactions -from tinyman.utils import get_state_int, get_state_bytes, bytes_to_int, TransactionGroup +from tinyman.utils import TransactionGroup from .add_liquidity import ( prepare_initial_add_liquidity_transactions, prepare_single_asset_add_liquidity_transactions, @@ -45,6 +45,16 @@ prepare_single_asset_remove_liquidity_transactions, ) from .swap import prepare_swap_transactions +from .utils import get_state_from_account_info + + +def generate_pool_info(address, validator_app_id, round_number, state): + return { + "address": address, + "validator_app_id": validator_app_id, + "round": round_number, + **state, + } def get_pool_info( @@ -53,72 +63,25 @@ def get_pool_info( pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) pool_address = pool_logicsig.address() account_info = client.account_info(pool_address) - return get_pool_info_from_account_info(account_info) + pool_state = get_pool_state_from_account_info(account_info) + if pool_state: + generate_pool_info( + pool_address, validator_app_id, account_info["round"], pool_state + ) + return {} -def get_pool_info_from_account_info(account_info: dict) -> dict: +def get_validator_app_id_from_account_info(account_info: dict) -> Optional[int]: try: - validator_app_id = account_info["apps-local-state"][0]["id"] + return account_info["apps-local-state"][0]["id"] except IndexError: - return {} - validator_app_state = { - x["key"]: x["value"] for x in account_info["apps-local-state"][0]["key-value"] - } + return None - asset_1_id = get_state_int(validator_app_state, "asset_1_id") - asset_2_id = get_state_int(validator_app_state, "asset_2_id") - pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) - pool_address = pool_logicsig.address() - - assert account_info["address"] == pool_address - - pool_token_asset_id = get_state_int(validator_app_state, "pool_token_asset_id") - issued_pool_tokens = get_state_int(validator_app_state, "issued_pool_tokens") - - # reserves - asset_1_reserves = get_state_int(validator_app_state, "asset_1_reserves") - asset_2_reserves = get_state_int(validator_app_state, "asset_2_reserves") - - # fees - asset_1_protocol_fees = get_state_int(validator_app_state, "asset_1_protocol_fees") - asset_2_protocol_fees = get_state_int(validator_app_state, "asset_2_protocol_fees") - - # fee rates - total_fee_share = get_state_int(validator_app_state, "total_fee_share") - protocol_fee_ratio = get_state_int(validator_app_state, "protocol_fee_ratio") - - # oracle - asset_1_cumulative_price = bytes_to_int( - get_state_bytes(validator_app_state, "asset_1_cumulative_price") - ) - asset_2_cumulative_price = bytes_to_int( - get_state_bytes(validator_app_state, "asset_2_cumulative_price") - ) - cumulative_price_update_timestamp = get_state_int( - validator_app_state, "cumulative_price_update_timestamp" - ) - - pool = { - "address": pool_address, - "asset_1_id": asset_1_id, - "asset_2_id": asset_2_id, - "pool_token_asset_id": pool_token_asset_id, - "asset_1_reserves": asset_1_reserves, - "asset_2_reserves": asset_2_reserves, - "issued_pool_tokens": issued_pool_tokens, - "asset_1_protocol_fees": asset_1_protocol_fees, - "asset_2_protocol_fees": asset_2_protocol_fees, - "asset_1_cumulative_price": asset_1_cumulative_price, - "asset_2_cumulative_price": asset_2_cumulative_price, - "cumulative_price_update_timestamp": cumulative_price_update_timestamp, - "total_fee_share": total_fee_share, - "protocol_fee_ratio": protocol_fee_ratio, - "validator_app_id": validator_app_id, - "algo_balance": account_info["amount"], - "round": account_info["round"], - } - return pool +def get_pool_state_from_account_info(account_info: dict) -> dict: + if validator_app_id := get_validator_app_id_from_account_info(account_info): + return get_state_from_account_info(account_info, validator_app_id) + return {} class Pool: @@ -160,7 +123,6 @@ def __init__( self.total_fee_share = None self.protocol_fee_ratio = None self.last_refreshed_round = None - self.algo_balance = None if fetch: self.refresh() @@ -172,14 +134,54 @@ def __repr__(self): @classmethod def from_account_info( - cls, account_info: dict, client: Optional[TinymanV2Client] = None + cls, + account_info: dict, + client: TinymanV2Client, + fetch: bool = False, ): - info = get_pool_info_from_account_info(account_info) + state = get_pool_state_from_account_info(account_info) + validator_app_id = get_validator_app_id_from_account_info(account_info) + assert validator_app_id == client.validator_app_id + + info = generate_pool_info( + address=account_info["address"], + validator_app_id=client.validator_app_id, + round_number=account_info["round"], + state=state, + ) + pool = Pool( - client, - info["asset_1_id"], - info["asset_2_id"], - info, + client=client, + asset_a=info["asset_1_id"], + asset_b=info["asset_2_id"], + info=info, + fetch=fetch, + validator_app_id=info["validator_app_id"], + ) + return pool + + @classmethod + def from_state( + cls, + address: str, + state: dict, + round_number: int, + client: TinymanV2Client, + fetch: bool = False, + ): + info = generate_pool_info( + address=address, + validator_app_id=client.validator_app_id, + round_number=round_number, + state=state, + ) + + pool = Pool( + client=client, + asset_a=info["asset_1_id"], + asset_b=info["asset_2_id"], + info=info, + fetch=fetch, validator_app_id=info["validator_app_id"], ) return pool @@ -209,7 +211,6 @@ def update_from_info(self, info: dict) -> None: self.total_fee_share = info["total_fee_share"] self.protocol_fee_ratio = info["protocol_fee_ratio"] self.last_refreshed_round = info["round"] - self.algo_balance = info["algo_balance"] def get_logicsig(self) -> LogicSigAccount: pool_logicsig = get_pool_logicsig( @@ -306,17 +307,22 @@ def fetch_pool_position(self, user_address: Optional[str] = None) -> dict: def prepare_bootstrap_transactions( self, user_address: Optional[str] = None, + pool_algo_balance=None, refresh: bool = True, suggested_params: SuggestedParams = None, ) -> TransactionGroup: + user_address = user_address or self.client.user_address + if refresh: self.refresh() if self.exists: raise AlreadyBootstrapped() - user_address = user_address or self.client.user_address + if pool_algo_balance is None: + pool_account_info = self.client.algod.account_info(self.address) + pool_algo_balance = pool_account_info["amount"] if suggested_params is None: suggested_params = self.client.algod.suggested_params() @@ -330,12 +336,8 @@ def prepare_bootstrap_transactions( app_call_fee = (inner_transaction_count + 1) * suggested_params.min_fee required_algo = pool_minimum_balance + app_call_fee - required_algo += ( - 100_000 # to fund minimum balance increase because of asset creation - ) - - pool_account_info = self.client.algod.account_info(self.address) - pool_algo_balance = pool_account_info["amount"] + # to fund minimum balance increase because of asset creation + required_algo += 100_000 required_algo = max(required_algo - pool_algo_balance, 0) txn_group = prepare_bootstrap_transactions( diff --git a/tinyman/v2/utils.py b/tinyman/v2/utils.py index 995048b..a297477 100644 --- a/tinyman/v2/utils.py +++ b/tinyman/v2/utils.py @@ -1,5 +1,7 @@ from base64 import b64decode +from tinyman.utils import bytes_to_int + def decode_logs(logs: list[[bytes, str]]) -> dict: decoded_logs = dict() @@ -14,3 +16,22 @@ def decode_logs(logs: list[[bytes, str]]) -> dict: else: raise NotImplementedError() return decoded_logs + + +def get_state_from_account_info(account_info, app_id): + try: + app = [a for a in account_info["apps-local-state"] if a["id"] == app_id][0] + except IndexError: + return {} + try: + app_state = {} + for x in app["key-value"]: + key = b64decode(x["key"]).decode() + if x["value"]["type"] == 1: + value = bytes_to_int(b64decode(x["value"].get("bytes", ""))) + else: + value = x["value"].get("uint", 0) + app_state[key] = value + except KeyError: + return {} + return app_state From 42751732c36f5b758a47186d7b54734d44188ca0 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 6 Oct 2022 16:55:04 +0300 Subject: [PATCH 38/73] add fee and management functions --- tinyman/v2/fees.py | 88 ++++++++++++++++++++++++++++++++++++++++ tinyman/v2/management.py | 68 +++++++++++++++++++++++++++++++ tinyman/v2/pools.py | 68 +++++++++++++++++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 tinyman/v2/fees.py create mode 100644 tinyman/v2/management.py diff --git a/tinyman/v2/fees.py b/tinyman/v2/fees.py new file mode 100644 index 0000000..47516a1 --- /dev/null +++ b/tinyman/v2/fees.py @@ -0,0 +1,88 @@ +from algosdk.future.transaction import ( + ApplicationNoOpTxn, + SuggestedParams, +) + +from tinyman.utils import TransactionGroup +from tinyman.v2.constants import ( + CLAIM_FEES_APP_ARGUMENT, + CLAIM_EXTRA_APP_ARGUMENT, + SET_FEE_APP_ARGUMENT, +) + + +def prepare_claim_fees_transactions( + validator_app_id: int, + asset_1_id: int, + asset_2_id: int, + pool_address: str, + fee_collector: str, + sender: str, + suggested_params: SuggestedParams, +) -> TransactionGroup: + txns = [ + ApplicationNoOpTxn( + sender=sender, + sp=suggested_params, + index=validator_app_id, + app_args=[CLAIM_FEES_APP_ARGUMENT], + foreign_assets=[asset_1_id, asset_2_id], + accounts=[pool_address, fee_collector], + ), + ] + + min_fee = suggested_params.min_fee + app_call_fee = min_fee * 3 + txns[-1].fee = app_call_fee + + txn_group = TransactionGroup(txns) + return txn_group + + +def prepare_claim_extra_transactions( + validator_app_id: int, + asset_1_id: int, + asset_2_id: int, + pool_address: str, + fee_collector: str, + sender: str, + suggested_params: SuggestedParams, +) -> TransactionGroup: + txns = [ + ApplicationNoOpTxn( + sender=sender, + sp=suggested_params, + index=validator_app_id, + app_args=[CLAIM_EXTRA_APP_ARGUMENT], + foreign_assets=[asset_1_id, asset_2_id], + accounts=[pool_address, fee_collector], + ), + ] + + min_fee = suggested_params.min_fee + app_call_fee = min_fee * 3 + txns[-1].fee = app_call_fee + + txn_group = TransactionGroup(txns) + return txn_group + + +def prepare_set_fee_transactions( + validator_app_id: int, + pool_address: str, + total_fee_share: int, + protocol_fee_ratio: int, + fee_manager: str, + suggested_params: SuggestedParams, +) -> TransactionGroup: + txns = [ + ApplicationNoOpTxn( + sender=fee_manager, + sp=suggested_params, + index=validator_app_id, + app_args=[SET_FEE_APP_ARGUMENT, total_fee_share, protocol_fee_ratio], + accounts=[pool_address], + ), + ] + txn_group = TransactionGroup(txns) + return txn_group diff --git a/tinyman/v2/management.py b/tinyman/v2/management.py new file mode 100644 index 0000000..5892af7 --- /dev/null +++ b/tinyman/v2/management.py @@ -0,0 +1,68 @@ +from algosdk.future.transaction import ( + ApplicationNoOpTxn, + SuggestedParams, +) + +from tinyman.utils import TransactionGroup +from tinyman.v2.constants import ( + SET_FEE_COLLECTOR_APP_ARGUMENT, + SET_FEE_SETTER_APP_ARGUMENT, + SET_FEE_MANAGER_APP_ARGUMENT, +) + + +def prepare_set_fee_collector_transactions( + validator_app_id: int, + fee_manager: str, + new_fee_collector: str, + suggested_params: SuggestedParams, +) -> TransactionGroup: + txns = [ + ApplicationNoOpTxn( + sender=fee_manager, + sp=suggested_params, + index=validator_app_id, + app_args=[SET_FEE_COLLECTOR_APP_ARGUMENT], + accounts=[new_fee_collector], + ), + ] + txn_group = TransactionGroup(txns) + return txn_group + + +def prepare_set_fee_setter_transactions( + validator_app_id: int, + fee_manager: str, + new_fee_setter: str, + suggested_params: SuggestedParams, +) -> TransactionGroup: + txns = [ + ApplicationNoOpTxn( + sender=fee_manager, + sp=suggested_params, + index=validator_app_id, + app_args=[SET_FEE_SETTER_APP_ARGUMENT], + accounts=[new_fee_setter], + ), + ] + txn_group = TransactionGroup(txns) + return txn_group + + +def prepare_set_fee_manager_transactions( + validator_app_id: int, + fee_manager: str, + new_fee_manager: str, + suggested_params: SuggestedParams, +) -> TransactionGroup: + txns = [ + ApplicationNoOpTxn( + sender=fee_manager, + sp=suggested_params, + index=validator_app_id, + app_args=[SET_FEE_MANAGER_APP_ARGUMENT], + accounts=[new_fee_manager], + ), + ] + txn_group = TransactionGroup(txns) + return txn_group diff --git a/tinyman/v2/pools.py b/tinyman/v2/pools.py index 4081ff0..b955070 100644 --- a/tinyman/v2/pools.py +++ b/tinyman/v2/pools.py @@ -21,6 +21,11 @@ PoolHasNoLiquidity, PoolAlreadyHasLiquidity, ) +from .fees import ( + prepare_claim_fees_transactions, + prepare_claim_extra_transactions, + prepare_set_fee_transactions, +) from .flash_loan import prepare_flash_loan_transactions from .formulas import ( calculate_subsequent_add_liquidity, @@ -1068,3 +1073,66 @@ def prepare_flash_loan_transactions_from_quote( transactions=transactions, user_address=user_address, ) + + def prepare_claim_fees_transactions( + self, + fee_collector: str, + user_address: str = None, + suggested_params: SuggestedParams = None, + ) -> TransactionGroup: + user_address = user_address or self.client.user_address + + if suggested_params is None: + suggested_params = self.client.algod.suggested_params() + + return prepare_claim_fees_transactions( + validator_app_id=self.validator_app_id, + asset_1_id=self.asset_1.id, + asset_2_id=self.asset_2.id, + pool_address=self.address, + fee_collector=fee_collector, + sender=user_address, + suggested_params=suggested_params, + ) + + def prepare_claim_extra_transactions( + self, + fee_collector: str, + user_address: str = None, + suggested_params: SuggestedParams = None, + ) -> TransactionGroup: + user_address = user_address or self.client.user_address + + if suggested_params is None: + suggested_params = self.client.algod.suggested_params() + + return prepare_claim_extra_transactions( + validator_app_id=self.validator_app_id, + asset_1_id=self.asset_1.id, + asset_2_id=self.asset_2.id, + pool_address=self.address, + fee_collector=fee_collector, + sender=user_address, + suggested_params=suggested_params, + ) + + def prepare_set_fee_transactions( + self, + total_fee_share: int, + protocol_fee_ratio: int, + user_address: str = None, + suggested_params: SuggestedParams = None, + ) -> TransactionGroup: + user_address = user_address or self.client.user_address + + if suggested_params is None: + suggested_params = self.client.algod.suggested_params() + + return prepare_set_fee_transactions( + validator_app_id=self.validator_app_id, + pool_address=self.address, + total_fee_share=total_fee_share, + protocol_fee_ratio=protocol_fee_ratio, + fee_manager=user_address, + suggested_params=suggested_params, + ) From 90ce5a0ec0cdf713dfe775f115b0fb2d3a905daa Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Fri, 7 Oct 2022 12:02:51 +0300 Subject: [PATCH 39/73] add constants and asc.json --- .gitignore | 3 --- tinyman/v2/asc.json | 12 ++++++++++ tinyman/v2/constants.py | 51 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 tinyman/v2/asc.json create mode 100644 tinyman/v2/constants.py diff --git a/.gitignore b/.gitignore index ad18a6c..fa5e850 100644 --- a/.gitignore +++ b/.gitignore @@ -131,8 +131,5 @@ dmypy.json # Pyre type checker .pyre/ -# TODO: Remove -tinyman/v2/asc.json -tinyman/v2/constants.py account*.json assets*.json diff --git a/tinyman/v2/asc.json b/tinyman/v2/asc.json new file mode 100644 index 0000000..41bf73f --- /dev/null +++ b/tinyman/v2/asc.json @@ -0,0 +1,12 @@ +{ + "repo": "https://github.com/tinymanorg/tinyman-contracts-v2", + "contracts": { + "pool_logicsig": { + "type": "logicsig", + "logic": { + "bytecode": "BoAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgQBbNQA0ADEYEkQxGYEBEkSBAUM=" + }, + "name": "pool_logicsig" + } + } +} \ No newline at end of file diff --git a/tinyman/v2/constants.py b/tinyman/v2/constants.py new file mode 100644 index 0000000..8b7703e --- /dev/null +++ b/tinyman/v2/constants.py @@ -0,0 +1,51 @@ +from algosdk.logic import get_application_address + +BOOTSTRAP_APP_ARGUMENT = b"bootstrap" +ADD_LIQUIDITY_APP_ARGUMENT = b"add_liquidity" +ADD_INITIAL_LIQUIDITY_APP_ARGUMENT = b"add_initial_liquidity" +REMOVE_LIQUIDITY_APP_ARGUMENT = b"remove_liquidity" +SWAP_APP_ARGUMENT = b"swap" +FLASH_LOAN_APP_ARGUMENT = b"flash_loan" +VERIFY_FLASH_LOAN_APP_ARGUMENT = b"verify_flash_loan" +FLASH_SWAP_APP_ARGUMENT = b"flash_swap" +VERIFY_FLASH_SWAP_APP_ARGUMENT = b"verify_flash_swap" +CLAIM_FEES_APP_ARGUMENT = b"claim_fees" +CLAIM_EXTRA_APP_ARGUMENT = b"claim_extra" +SET_FEE_APP_ARGUMENT = b"set_fee" +SET_FEE_COLLECTOR_APP_ARGUMENT = b"set_fee_collector" +SET_FEE_SETTER_APP_ARGUMENT = b"set_fee_setter" +SET_FEE_MANAGER_APP_ARGUMENT = b"set_fee_manager" + +FIXED_INPUT_APP_ARGUMENT = b"fixed-input" +FIXED_OUTPUT_APP_ARGUMENT = b"fixed-output" + +ADD_LIQUIDITY_FLEXIBLE_MODE_APP_ARGUMENT = b"flexible" +ADD_LIQUIDITY_SINGLE_MODE_APP_ARGUMENT = b"single" + + +TESTNET_VALIDATOR_APP_ID_V2 = 113134165 +MAINNET_VALIDATOR_APP_ID_V2 = None + +TESTNET_VALIDATOR_APP_ID = TESTNET_VALIDATOR_APP_ID_V2 +MAINNET_VALIDATOR_APP_ID = None + +TESTNET_VALIDATOR_APP_ADDRESS = get_application_address(TESTNET_VALIDATOR_APP_ID) +# MAINNET_VALIDATOR_APP__ADDRESS = get_application_address(MAINNET_VALIDATOR_APP_ID) + + +LOCKED_POOL_TOKENS = 1000 +ASSET_MIN_TOTAL = 1000000 + +# State +APP_LOCAL_INTS = 12 +APP_LOCAL_BYTES = 2 +APP_GLOBAL_INTS = 0 +APP_GLOBAL_BYTES = 3 + +# 100,000 Algo +# + 100,000 ASA 1 +# + 100,000 ASA 2 +# + 100,000 Pool Token +# + 542,500 App Optin (100000 + (25000+3500)*12 + (25000+25000)*2) +MIN_POOL_BALANCE_ASA_ALGO_PAIR = 300_000 + (100_000 + (25_000 + 3_500) * APP_LOCAL_INTS + (25_000 + 25_000) * APP_LOCAL_BYTES) +MIN_POOL_BALANCE_ASA_ASA_PAIR = MIN_POOL_BALANCE_ASA_ALGO_PAIR + 100_000 From c90c3171d59b7ce86c3d3ce49616b4af1663b5e9 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Fri, 7 Oct 2022 17:53:53 +0300 Subject: [PATCH 40/73] add basic tests --- examples/v2/tutorial/13_flash_swap_1.py | 36 ++- examples/v2/tutorial/14_flash_swap_2.py | 37 ++- tests/v2/__init__.py | 46 ++++ tests/v2/test_add_liquidity.py | 315 ++++++++++++++++++++++++ tests/v2/test_bootstrap.py | 226 ++++++++++++----- tests/v2/test_flash_loan.py | 138 +++++++++++ tests/v2/test_flash_swap.py | 99 ++++++++ tests/v2/test_remove_liquidity.py | 188 ++++++++++++++ tests/v2/test_swap.py | 170 +++++++++++++ tinyman/assets.py | 9 +- tinyman/client.py | 4 +- tinyman/v2/bootstrap.py | 2 +- tinyman/v2/constants.py | 4 +- tinyman/v2/flash_swap.py | 43 +--- tinyman/v2/formulas.py | 19 +- tinyman/v2/pools.py | 91 +++++-- tinyman/v2/quotes.py | 1 + 17 files changed, 1289 insertions(+), 139 deletions(-) create mode 100644 tests/v2/test_add_liquidity.py create mode 100644 tests/v2/test_flash_loan.py create mode 100644 tests/v2/test_flash_swap.py create mode 100644 tests/v2/test_remove_liquidity.py create mode 100644 tests/v2/test_swap.py diff --git a/examples/v2/tutorial/13_flash_swap_1.py b/examples/v2/tutorial/13_flash_swap_1.py index 58bd2d5..9d97aaf 100644 --- a/examples/v2/tutorial/13_flash_swap_1.py +++ b/examples/v2/tutorial/13_flash_swap_1.py @@ -4,7 +4,7 @@ from pprint import pprint from urllib.parse import quote_plus -from algosdk.future.transaction import AssetTransferTxn +from algosdk.future.transaction import AssetTransferTxn, PaymentTxn from examples.v2.tutorial.common import get_account, get_assets from examples.v2.utils import get_algod @@ -54,14 +54,44 @@ ) ] +if asset_1_payment_amount: + transactions.append( + AssetTransferTxn( + sender=account["address"], + sp=suggested_params, + receiver=pool.address, + index=pool.asset_1.id, + amt=asset_1_payment_amount, + ) + ) + +if asset_2_payment_amount: + if pool.asset_2.id: + transactions.append( + AssetTransferTxn( + sender=account["address"], + sp=suggested_params, + receiver=pool.address, + index=pool.asset_2.id, + amt=asset_2_payment_amount, + ) + ) + else: + transactions.append( + PaymentTxn( + sender=account["address"], + sp=suggested_params, + receiver=pool.address, + amt=asset_2_payment_amount, + ) + ) + txn_group = prepare_flash_swap_transactions( validator_app_id=pool.validator_app_id, asset_1_id=pool.asset_1.id, asset_2_id=pool.asset_2.id, asset_1_loan_amount=asset_1_loan_amount, asset_2_loan_amount=asset_2_loan_amount, - asset_1_payment_amount=asset_1_payment_amount, - asset_2_payment_amount=asset_2_payment_amount, transactions=transactions, suggested_params=suggested_params, sender=account["address"], diff --git a/examples/v2/tutorial/14_flash_swap_2.py b/examples/v2/tutorial/14_flash_swap_2.py index 532756b..60a47b7 100644 --- a/examples/v2/tutorial/14_flash_swap_2.py +++ b/examples/v2/tutorial/14_flash_swap_2.py @@ -4,7 +4,7 @@ from pprint import pprint from urllib.parse import quote_plus -from algosdk.future.transaction import AssetTransferTxn +from algosdk.future.transaction import AssetTransferTxn, PaymentTxn from examples.v2.tutorial.common import get_account, get_assets from examples.v2.utils import get_algod @@ -54,14 +54,45 @@ ) ] + +if asset_1_payment_amount: + transactions.append( + AssetTransferTxn( + sender=account["address"], + sp=suggested_params, + receiver=pool.address, + index=pool.asset_1.id, + amt=asset_1_payment_amount, + ) + ) + +if asset_2_payment_amount: + if pool.asset_2.id: + transactions.append( + AssetTransferTxn( + sender=account["address"], + sp=suggested_params, + receiver=pool.address, + index=pool.asset_2.id, + amt=asset_2_payment_amount, + ) + ) + else: + transactions.append( + PaymentTxn( + sender=account["address"], + sp=suggested_params, + receiver=pool.address, + amt=asset_2_payment_amount, + ) + ) + txn_group = prepare_flash_swap_transactions( validator_app_id=pool.validator_app_id, asset_1_id=pool.asset_1.id, asset_2_id=pool.asset_2.id, asset_1_loan_amount=asset_1_loan_amount, asset_2_loan_amount=asset_2_loan_amount, - asset_1_payment_amount=asset_1_payment_amount, - asset_2_payment_amount=asset_2_payment_amount, transactions=transactions, suggested_params=suggested_params, sender=account["address"], diff --git a/tests/v2/__init__.py b/tests/v2/__init__.py index e69de29..538a59f 100644 --- a/tests/v2/__init__.py +++ b/tests/v2/__init__.py @@ -0,0 +1,46 @@ +from unittest import TestCase + +from algosdk.v2client.algod import AlgodClient + +from tinyman.v2.client import TinymanV2Client +from tests import get_suggested_params + + +class BaseTestCase(TestCase): + maxDiff = None + + @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, + user_address=user_address or cls.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 or cls.pool_token_asset_id, + "asset_1_protocol_fees": 0, + "asset_1_id": asset_1_id or cls.asset_1_id, + "asset_2_id": asset_2_id or cls.asset_2_id, + "issued_pool_tokens": 0, + "asset_2_reserves": 0, + "protocol_fee_ratio": 6, + "total_fee_share": 30, + } + state.update(**kwargs) + return state diff --git a/tests/v2/test_add_liquidity.py b/tests/v2/test_add_liquidity.py new file mode 100644 index 0000000..7b87df5 --- /dev/null +++ b/tests/v2/test_add_liquidity.py @@ -0,0 +1,315 @@ +from unittest.mock import ANY + +from algosdk.account import generate_account +from algosdk.constants import ASSETTRANSFER_TXN, APPCALL_TXN +from algosdk.encoding import decode_address +from algosdk.future.transaction import OnComplete +from algosdk.logic import get_application_address + +from tests.v2 import BaseTestCase +from tinyman.assets import AssetAmount +from tinyman.utils import int_to_bytes +from tinyman.v2.constants import ( + LOCKED_POOL_TOKENS, + ADD_INITIAL_LIQUIDITY_APP_ARGUMENT, + ADD_LIQUIDITY_APP_ARGUMENT, + ADD_LIQUIDITY_FLEXIBLE_MODE_APP_ARGUMENT, + ADD_LIQUIDITY_SINGLE_MODE_APP_ARGUMENT, +) +from tinyman.v2.contracts import get_pool_logicsig +from tinyman.v2.pools import Pool +from tinyman.v2.quotes import ( + InitialAddLiquidityQuote, + FlexibleAddLiquidityQuote, + SingleAssetAddLiquidityQuote, +) + + +class InitialAddLiquidityTestCase(BaseTestCase): + @classmethod + def setUpClass(cls): + cls.VALIDATOR_APP_ID = 12345 + cls.sender_private_key, cls.user_address = generate_account() + cls.asset_1_id = 10 + cls.asset_2_id = 8 + cls.pool_token_asset_id = 15 + cls.pool_address = get_pool_logicsig( + cls.VALIDATOR_APP_ID, cls.asset_1_id, cls.asset_2_id + ).address() + cls.application_address = get_application_address(cls.VALIDATOR_APP_ID) + cls.pool_state = cls.get_pool_state() + + cls.pool = Pool.from_state( + address=cls.pool_address, + state=cls.pool_state, + round_number=100, + client=cls.get_tinyman_client(), + ) + + def test_add_liquidity(self): + asset_a_amount = AssetAmount(self.pool.asset_1, 1_000_000) + asset_b_amount = AssetAmount(self.pool.asset_2, 100_000_000) + quote = self.pool.fetch_initial_add_liquidity_quote( + amount_a=asset_a_amount, amount_b=asset_b_amount, refresh=False + ) + + self.assertEqual(type(quote), InitialAddLiquidityQuote) + self.assertEqual( + quote.amounts_in[self.pool.asset_1], + AssetAmount(self.pool.asset_1, 1_000_000), + ) + self.assertEqual( + quote.amounts_in[self.pool.asset_2], + AssetAmount(self.pool.asset_2, 100_000_000), + ) + self.assertEqual( + quote.pool_token_asset_amount, + AssetAmount(self.pool.pool_token_asset, 10_000_000 - LOCKED_POOL_TOKENS), + ) + + suggested_params = self.get_suggested_params() + txn_group = self.pool.prepare_add_liquidity_transactions_from_quote( + quote=quote, suggested_params=suggested_params + ) + transactions = txn_group.transactions + + self.assertEqual(len(transactions), 3) + self.assertDictEqual( + dict(transactions[0].dictify()), + { + "aamt": quote.amounts_in[self.pool.asset_1].amount, + "arcv": decode_address(self.pool_address), + "fee": 1000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": ASSETTRANSFER_TXN, + "xaid": self.pool.asset_1.id, + }, + ) + + self.assertDictEqual( + dict(transactions[1].dictify()), + { + "aamt": quote.amounts_in[self.pool.asset_2].amount, + "arcv": decode_address(self.pool_address), + "fee": 1000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": ASSETTRANSFER_TXN, + "xaid": self.pool.asset_2.id, + }, + ) + + self.assertDictEqual( + dict(transactions[2].dictify()), + { + "apaa": [ADD_INITIAL_LIQUIDITY_APP_ARGUMENT], + "apan": OnComplete.NoOpOC, + "apas": [self.pool.pool_token_asset.id], + "apat": [decode_address(self.pool_address)], + "apid": self.VALIDATOR_APP_ID, + "fee": 2000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": APPCALL_TXN, + }, + ) + + +class AddLiquidityTestCase(BaseTestCase): + @classmethod + def setUpClass(cls): + cls.VALIDATOR_APP_ID = 12345 + cls.sender_private_key, cls.user_address = generate_account() + cls.asset_1_id = 10 + cls.asset_2_id = 8 + cls.pool_token_asset_id = 15 + cls.pool_address = get_pool_logicsig( + cls.VALIDATOR_APP_ID, cls.asset_1_id, cls.asset_2_id + ).address() + cls.application_address = get_application_address(cls.VALIDATOR_APP_ID) + cls.pool_state = cls.get_pool_state( + asset_1_reserves=1_000_000, + asset_2_reserves=100_000_000, + issued_pool_tokens=10_000_000, + ) + cls.pool = Pool.from_state( + address=cls.pool_address, + state=cls.pool_state, + round_number=100, + client=cls.get_tinyman_client(), + ) + + def test_flexible_add_liquidity(self): + asset_a_amount = AssetAmount(self.pool.asset_1, 10_000_000) + asset_b_amount = AssetAmount(self.pool.asset_2, 10_000_000) + quote = self.pool.fetch_flexible_add_liquidity_quote( + amount_a=asset_a_amount, amount_b=asset_b_amount, refresh=False + ) + + self.assertEqual(type(quote), FlexibleAddLiquidityQuote) + self.assertEqual(quote.slippage, 0.05) + self.assertEqual( + quote.amounts_in[self.pool.asset_1], + AssetAmount(self.pool.asset_1, 10_000_000), + ) + self.assertEqual( + quote.amounts_in[self.pool.asset_2], + AssetAmount(self.pool.asset_2, 10_000_000), + ) + self.assertEqual( + quote.pool_token_asset_amount, + AssetAmount(self.pool.pool_token_asset, 24_774_768), + ) + self.assertEqual(quote.min_pool_token_asset_amount_with_slippage, 23_536_029) + + internal_swap_quote = quote.internal_swap_quote + self.assertEqual( + internal_swap_quote.amount_in, AssetAmount(self.pool.asset_1, 2_168_784) + ) + self.assertEqual( + internal_swap_quote.amount_out, AssetAmount(self.pool.asset_2, 68_377_223) + ) + self.assertEqual( + internal_swap_quote.swap_fees, AssetAmount(self.pool.asset_1, 6_506) + ) + self.assertEqual(internal_swap_quote.price_impact, 0.68472) + + suggested_params = self.get_suggested_params() + txn_group = self.pool.prepare_add_liquidity_transactions_from_quote( + quote=quote, suggested_params=suggested_params + ) + transactions = txn_group.transactions + + self.assertEqual(len(transactions), 3) + self.assertDictEqual( + dict(transactions[0].dictify()), + { + "aamt": quote.amounts_in[self.pool.asset_1].amount, + "arcv": decode_address(self.pool_address), + "fee": 1000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": ASSETTRANSFER_TXN, + "xaid": self.pool.asset_1.id, + }, + ) + self.assertDictEqual( + dict(transactions[1].dictify()), + { + "aamt": quote.amounts_in[self.pool.asset_2].amount, + "arcv": decode_address(self.pool_address), + "fee": 1000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": ASSETTRANSFER_TXN, + "xaid": self.pool.asset_2.id, + }, + ) + self.assertDictEqual( + dict(transactions[2].dictify()), + { + "apaa": [ + ADD_LIQUIDITY_APP_ARGUMENT, + ADD_LIQUIDITY_FLEXIBLE_MODE_APP_ARGUMENT, + int_to_bytes(23_536_029), + ], + "apan": OnComplete.NoOpOC, + "apas": [self.pool.pool_token_asset.id], + "apat": [decode_address(self.pool.address)], + "apid": self.VALIDATOR_APP_ID, + "fee": 3000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": APPCALL_TXN, + }, + ) + + def test_single_asset_add_liquidity(self): + asset_a_amount = AssetAmount(self.pool.asset_1, 10_000_000) + quote = self.pool.fetch_single_asset_add_liquidity_quote( + amount_a=asset_a_amount, refresh=False + ) + + self.assertEqual(type(quote), SingleAssetAddLiquidityQuote) + self.assertEqual(quote.slippage, 0.05) + self.assertEqual(quote.amount_in, AssetAmount(self.pool.asset_1, 10_000_000)) + self.assertEqual( + quote.pool_token_asset_amount, + AssetAmount(self.pool.pool_token_asset, 23_155_740), + ) + self.assertEqual(quote.min_pool_token_asset_amount_with_slippage, 21_997_953) + + internal_swap_quote = quote.internal_swap_quote + self.assertEqual( + internal_swap_quote.amount_in, AssetAmount(self.pool.asset_1, 2_323_595) + ) + self.assertEqual( + internal_swap_quote.amount_out, AssetAmount(self.pool.asset_2, 69_848_864) + ) + self.assertEqual( + internal_swap_quote.swap_fees, AssetAmount(self.pool.asset_1, 6_970) + ) + self.assertEqual(internal_swap_quote.price_impact, 0.69939) + + suggested_params = self.get_suggested_params() + txn_group = self.pool.prepare_add_liquidity_transactions_from_quote( + quote=quote, suggested_params=suggested_params + ) + transactions = txn_group.transactions + + self.assertEqual(len(transactions), 2) + self.assertDictEqual( + dict(transactions[0].dictify()), + { + "aamt": quote.amount_in.amount, + "arcv": decode_address(self.pool_address), + "fee": 1000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": ASSETTRANSFER_TXN, + "xaid": self.pool.asset_1.id, + }, + ) + self.assertDictEqual( + dict(transactions[1].dictify()), + { + "apaa": [ + ADD_LIQUIDITY_APP_ARGUMENT, + ADD_LIQUIDITY_SINGLE_MODE_APP_ARGUMENT, + int_to_bytes(21_997_953), + ], + "apan": OnComplete.NoOpOC, + "apas": [self.pool.pool_token_asset.id], + "apat": [decode_address(self.pool.address)], + "apid": self.VALIDATOR_APP_ID, + "fee": 3000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": APPCALL_TXN, + }, + ) diff --git a/tests/v2/test_bootstrap.py b/tests/v2/test_bootstrap.py index 051ef83..8536fbc 100644 --- a/tests/v2/test_bootstrap.py +++ b/tests/v2/test_bootstrap.py @@ -1,84 +1,198 @@ -import unittest +from unittest.mock import ANY from algosdk.account import generate_account -from algosdk.future.transaction import PaymentTxn, ApplicationOptInTxn +from algosdk.constants import APPCALL_TXN, PAYMENT_TXN +from algosdk.encoding import decode_address +from algosdk.future.transaction import OnComplete from algosdk.logic import get_application_address -from tests import get_suggested_params -from tinyman.v2.bootstrap import prepare_bootstrap_transactions +from tests.v2 import BaseTestCase from tinyman.v2.constants import BOOTSTRAP_APP_ARGUMENT from tinyman.v2.contracts import get_pool_logicsig +from tinyman.v2.pools import Pool -class BootstrapTestCase(unittest.TestCase): +class BootstrapTestCase(BaseTestCase): @classmethod def setUpClass(cls): cls.VALIDATOR_APP_ID = 12345 + cls.application_address = get_application_address(cls.VALIDATOR_APP_ID) - def test_asa_asa_pair(self): - _, sender = generate_account() - - suggested_params = get_suggested_params() - txn_group = prepare_bootstrap_transactions( - validator_app_id=self.VALIDATOR_APP_ID, - asset_1_id=10, - asset_2_id=8, - sender=sender, - app_call_fee=suggested_params.min_fee * 7, - required_algo=500_000, - suggested_params=suggested_params, + cls.sender_private_key, cls.user_address = generate_account() + cls.asset_1_id = 10 + cls.asset_2_id = 8 + cls.pool_address = get_pool_logicsig( + cls.VALIDATOR_APP_ID, cls.asset_1_id, cls.asset_2_id + ).address() + cls.application_address = get_application_address(cls.VALIDATOR_APP_ID) + cls.pool_state = {} + + cls.pool = Pool( + client=cls.get_tinyman_client(), + asset_a=cls.asset_1_id, + asset_b=cls.asset_2_id, + info=None, + fetch=False, + validator_app_id=cls.VALIDATOR_APP_ID, + ) + + def test_bootstrap(self): + txn_group = self.pool.prepare_bootstrap_transactions( + pool_algo_balance=0, + refresh=False, + suggested_params=self.get_suggested_params(), ) transactions = txn_group.transactions self.assertEqual(len(transactions), 2) + self.assertDictEqual( + dict(transactions[0].dictify()), + { + "amt": 1_049_000, + "fee": 1000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "rcv": decode_address(self.pool_address), + "snd": decode_address(self.user_address), + "type": PAYMENT_TXN, + }, + ) + self.assertDictEqual( + dict(transactions[1].dictify()), + { + "apaa": [BOOTSTRAP_APP_ARGUMENT], + "apan": OnComplete.OptInOC, + "apas": [self.pool.asset_1.id, self.pool.asset_2.id], + "apid": self.VALIDATOR_APP_ID, + "fee": 7000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "rekey": decode_address(self.application_address), + "snd": decode_address(self.pool_address), + "type": APPCALL_TXN, + }, + ) - self.assertTrue(isinstance(transactions[0], PaymentTxn)) - self.assertEqual(transactions[0].amt, 500_000) - self.assertEqual(transactions[0].sender, sender) - self.assertEqual( - transactions[0].receiver, - get_pool_logicsig(self.VALIDATOR_APP_ID, 10, 8).address(), + def test_pool_is_already_funded(self): + txn_group = self.pool.prepare_bootstrap_transactions( + pool_algo_balance=2_000_000, + refresh=False, + suggested_params=self.get_suggested_params(), ) - self.assertEqual(transactions[0].rekey_to, None) - self.assertTrue(isinstance(transactions[1], ApplicationOptInTxn)) - self.assertEqual(transactions[1].index, self.VALIDATOR_APP_ID) - self.assertEqual( - transactions[1].sender, - get_pool_logicsig(self.VALIDATOR_APP_ID, 10, 8).address(), + transactions = txn_group.transactions + self.assertEqual(len(transactions), 1) + self.assertDictEqual( + dict(transactions[0].dictify()), + { + "apaa": [BOOTSTRAP_APP_ARGUMENT], + "apan": OnComplete.OptInOC, + "apas": [self.pool.asset_1.id, self.pool.asset_2.id], + "apid": self.VALIDATOR_APP_ID, + "fee": 7000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "rekey": decode_address(self.application_address), + "snd": decode_address(self.pool_address), + "type": APPCALL_TXN, + }, ) - self.assertEqual(transactions[1].fee, suggested_params.min_fee * 7) - self.assertEqual(transactions[1].app_args, [BOOTSTRAP_APP_ARGUMENT]) - self.assertEqual(transactions[1].foreign_assets, [10, 8]) - self.assertEqual( - transactions[1].rekey_to, get_application_address(self.VALIDATOR_APP_ID) + + +class BootstrapAlgoPoolTestCase(BaseTestCase): + @classmethod + def setUpClass(cls): + cls.VALIDATOR_APP_ID = 12345 + cls.application_address = get_application_address(cls.VALIDATOR_APP_ID) + + cls.sender_private_key, cls.user_address = generate_account() + cls.asset_1_id = 10 + cls.asset_2_id = 0 + cls.pool_address = get_pool_logicsig( + cls.VALIDATOR_APP_ID, cls.asset_1_id, cls.asset_2_id + ).address() + cls.application_address = get_application_address(cls.VALIDATOR_APP_ID) + cls.pool_state = {} + + cls.pool = Pool( + client=cls.get_tinyman_client(), + asset_a=cls.asset_1_id, + asset_b=cls.asset_2_id, + info=None, + fetch=False, + validator_app_id=cls.VALIDATOR_APP_ID, + ) + + def test_bootstrap(self): + txn_group = self.pool.prepare_bootstrap_transactions( + pool_algo_balance=0, + refresh=False, + suggested_params=self.get_suggested_params(), + ) + + transactions = txn_group.transactions + self.assertEqual(len(transactions), 2) + self.assertDictEqual( + dict(transactions[0].dictify()), + { + "amt": 948_000, + "fee": 1000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "rcv": decode_address(self.pool_address), + "snd": decode_address(self.user_address), + "type": PAYMENT_TXN, + }, + ) + self.assertDictEqual( + dict(transactions[1].dictify()), + { + "apaa": [BOOTSTRAP_APP_ARGUMENT], + "apan": OnComplete.OptInOC, + "apas": [self.pool.asset_1.id, self.pool.asset_2.id], + "apid": self.VALIDATOR_APP_ID, + "fee": 6000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "rekey": decode_address(self.application_address), + "snd": decode_address(self.pool_address), + "type": APPCALL_TXN, + }, ) def test_pool_is_already_funded(self): - _, sender = generate_account() - - suggested_params = get_suggested_params() - txn_group = prepare_bootstrap_transactions( - validator_app_id=self.VALIDATOR_APP_ID, - asset_1_id=10, - asset_2_id=8, - sender=sender, - app_call_fee=suggested_params.min_fee * 6, - required_algo=0, - suggested_params=suggested_params, + txn_group = self.pool.prepare_bootstrap_transactions( + pool_algo_balance=2_000_000, + refresh=False, + suggested_params=self.get_suggested_params(), ) transactions = txn_group.transactions self.assertEqual(len(transactions), 1) - self.assertTrue(isinstance(transactions[0], ApplicationOptInTxn)) - self.assertEqual(transactions[0].index, self.VALIDATOR_APP_ID) - self.assertEqual( - transactions[0].sender, - get_pool_logicsig(self.VALIDATOR_APP_ID, 10, 8).address(), - ) - self.assertEqual(transactions[0].fee, suggested_params.min_fee * 6) - self.assertEqual(transactions[0].app_args, [BOOTSTRAP_APP_ARGUMENT]) - self.assertEqual(transactions[0].foreign_assets, [10, 8]) - self.assertEqual( - transactions[0].rekey_to, get_application_address(self.VALIDATOR_APP_ID) + self.assertDictEqual( + dict(transactions[0].dictify()), + { + "apaa": [BOOTSTRAP_APP_ARGUMENT], + "apan": OnComplete.OptInOC, + "apas": [self.pool.asset_1.id, self.pool.asset_2.id], + "apid": self.VALIDATOR_APP_ID, + "fee": 6000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "rekey": decode_address(self.application_address), + "snd": decode_address(self.pool_address), + "type": APPCALL_TXN, + }, ) diff --git a/tests/v2/test_flash_loan.py b/tests/v2/test_flash_loan.py new file mode 100644 index 0000000..4416032 --- /dev/null +++ b/tests/v2/test_flash_loan.py @@ -0,0 +1,138 @@ +from unittest.mock import ANY + +from algosdk.account import generate_account +from algosdk.constants import APPCALL_TXN +from algosdk.encoding import decode_address +from algosdk.future.transaction import AssetTransferTxn, OnComplete +from algosdk.logic import get_application_address + +from tests.v2 import BaseTestCase +from tinyman.assets import AssetAmount +from tinyman.utils import int_to_bytes +from tinyman.v2.constants import FLASH_LOAN_APP_ARGUMENT, VERIFY_FLASH_LOAN_APP_ARGUMENT +from tinyman.v2.contracts import get_pool_logicsig +from tinyman.v2.pools import Pool +from tinyman.v2.quotes import FlashLoanQuote + + +class FlashLoanTestCase(BaseTestCase): + @classmethod + def setUpClass(cls): + cls.VALIDATOR_APP_ID = 12345 + cls.sender_private_key, cls.user_address = generate_account() + cls.asset_1_id = 10 + cls.asset_2_id = 8 + cls.pool_token_asset_id = 15 + cls.pool_address = get_pool_logicsig( + cls.VALIDATOR_APP_ID, cls.asset_1_id, cls.asset_2_id + ).address() + cls.application_address = get_application_address(cls.VALIDATOR_APP_ID) + cls.pool_state = cls.get_pool_state( + asset_1_reserves=10_000_000, + asset_2_reserves=1_000_000_000, + issued_pool_tokens=100_000_000, + ) + cls.pool = Pool.from_state( + address=cls.pool_address, + state=cls.pool_state, + round_number=100, + client=cls.get_tinyman_client(), + ) + + def test_flash_loan(self): + quote = self.pool.fetch_flash_loan_quote( + loan_amount_a=AssetAmount(self.pool.asset_1, 1_000_000), + loan_amount_b=AssetAmount(self.pool.asset_2, 10_000_000), + refresh=False, + ) + + self.assertEqual(type(quote), FlashLoanQuote) + self.assertEqual( + quote.amounts_out[self.pool.asset_1], + AssetAmount(self.pool.asset_1, 1_000_000), + ) + self.assertEqual( + quote.amounts_out[self.pool.asset_2], + AssetAmount(self.pool.asset_2, 10_000_000), + ) + self.assertEqual( + quote.amounts_in[self.pool.asset_1], + AssetAmount(self.pool.asset_1, 1_003_000), + ) + self.assertEqual( + quote.amounts_in[self.pool.asset_2], + AssetAmount(self.pool.asset_2, 10_030_000), + ) + self.assertEqual( + quote.fees[self.pool.asset_1], AssetAmount(self.pool.asset_1, 3_000) + ) + self.assertEqual( + quote.fees[self.pool.asset_2], AssetAmount(self.pool.asset_2, 30_000) + ) + + suggested_params = self.get_suggested_params() + txn_group = self.pool.prepare_flash_loan_transactions_from_quote( + quote=quote, suggested_params=suggested_params, transactions=[] + ) + index_diff = 3 + transactions = txn_group.transactions + + self.assertEqual(len(transactions), 4) + self.assertDictEqual( + dict(transactions[0].dictify()), + { + "apaa": [ + FLASH_LOAN_APP_ARGUMENT, + int_to_bytes(index_diff), + int_to_bytes(quote.amounts_out[self.pool.asset_1].amount), + int_to_bytes(quote.amounts_out[self.pool.asset_2].amount), + ], + "apan": OnComplete.NoOpOC, + "apas": [self.pool.asset_1.id, self.pool.asset_2.id], + "apat": [decode_address(self.pool.address)], + "apid": self.VALIDATOR_APP_ID, + "fee": 3000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": APPCALL_TXN, + }, + ) + + self.assertEqual(type(transactions[1]), AssetTransferTxn) + self.assertEqual(transactions[1].index, self.pool.asset_1.id) + self.assertEqual(transactions[1].sender, self.user_address) + self.assertEqual(transactions[1].receiver, self.pool_address) + self.assertEqual( + transactions[1].amount, quote.amounts_in[self.pool.asset_1].amount + ) + + self.assertEqual(type(transactions[2]), AssetTransferTxn) + self.assertEqual(transactions[2].index, self.pool.asset_2.id) + self.assertEqual(transactions[2].sender, self.user_address) + self.assertEqual(transactions[2].receiver, self.pool_address) + self.assertEqual( + transactions[2].amount, quote.amounts_in[self.pool.asset_2].amount + ) + + self.assertDictEqual( + dict(transactions[3].dictify()), + { + "apaa": [ + VERIFY_FLASH_LOAN_APP_ARGUMENT, + int_to_bytes(index_diff), + ], + "apan": OnComplete.NoOpOC, + "apat": [decode_address(self.pool.address)], + "apid": self.VALIDATOR_APP_ID, + "fee": 1000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": APPCALL_TXN, + }, + ) diff --git a/tests/v2/test_flash_swap.py b/tests/v2/test_flash_swap.py new file mode 100644 index 0000000..ed7b6d1 --- /dev/null +++ b/tests/v2/test_flash_swap.py @@ -0,0 +1,99 @@ +from unittest.mock import ANY + +from algosdk.account import generate_account +from algosdk.constants import APPCALL_TXN +from algosdk.encoding import decode_address +from algosdk.future.transaction import OnComplete +from algosdk.logic import get_application_address + +from tests.v2 import BaseTestCase +from tinyman.utils import int_to_bytes +from tinyman.v2.constants import FLASH_SWAP_APP_ARGUMENT, VERIFY_FLASH_SWAP_APP_ARGUMENT +from tinyman.v2.contracts import get_pool_logicsig +from tinyman.v2.flash_swap import prepare_flash_swap_transactions +from tinyman.v2.pools import Pool + + +class FlashSwapTestCase(BaseTestCase): + @classmethod + def setUpClass(cls): + cls.VALIDATOR_APP_ID = 12345 + cls.sender_private_key, cls.user_address = generate_account() + cls.asset_1_id = 10 + cls.asset_2_id = 8 + cls.pool_token_asset_id = 15 + cls.pool_address = get_pool_logicsig( + cls.VALIDATOR_APP_ID, cls.asset_1_id, cls.asset_2_id + ).address() + cls.application_address = get_application_address(cls.VALIDATOR_APP_ID) + cls.pool_state = cls.get_pool_state( + asset_1_reserves=10_000_000, + asset_2_reserves=1_000_000_000, + issued_pool_tokens=100_000_000, + ) + cls.pool = Pool.from_state( + address=cls.pool_address, + state=cls.pool_state, + round_number=100, + client=cls.get_tinyman_client(), + ) + + def test_flash_swap(self): + index_diff = 1 + txn_group = prepare_flash_swap_transactions( + validator_app_id=self.VALIDATOR_APP_ID, + asset_1_id=self.pool.asset_1.id, + asset_2_id=self.pool.asset_2.id, + asset_1_loan_amount=1_000_000, + asset_2_loan_amount=100_000_000, + transactions=[], + sender=self.user_address, + suggested_params=self.get_suggested_params(), + ) + + transactions = txn_group.transactions + self.assertEqual(len(transactions), 2) + self.assertDictEqual( + dict(transactions[0].dictify()), + { + "apaa": [ + FLASH_SWAP_APP_ARGUMENT, + int_to_bytes(index_diff), + int_to_bytes(1_000_000), + int_to_bytes(100_000_000), + ], + "apan": OnComplete.NoOpOC, + "apas": [self.pool.asset_1.id, self.pool.asset_2.id], + "apat": [decode_address(self.pool.address)], + "apid": self.VALIDATOR_APP_ID, + "fee": 3000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": APPCALL_TXN, + }, + ) + + # Verify + self.assertDictEqual( + dict(transactions[1].dictify()), + { + "apaa": [ + VERIFY_FLASH_SWAP_APP_ARGUMENT, + int_to_bytes(index_diff), + ], + "apan": OnComplete.NoOpOC, + "apas": [self.pool.asset_1.id, self.pool.asset_2.id], + "apat": [decode_address(self.pool.address)], + "apid": self.VALIDATOR_APP_ID, + "fee": 1000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": APPCALL_TXN, + }, + ) diff --git a/tests/v2/test_remove_liquidity.py b/tests/v2/test_remove_liquidity.py new file mode 100644 index 0000000..05881bc --- /dev/null +++ b/tests/v2/test_remove_liquidity.py @@ -0,0 +1,188 @@ +from unittest.mock import ANY + +from algosdk.account import generate_account +from algosdk.constants import ASSETTRANSFER_TXN, APPCALL_TXN +from algosdk.encoding import decode_address +from algosdk.future.transaction import OnComplete +from algosdk.logic import get_application_address + +from tests.v2 import BaseTestCase +from tinyman.assets import AssetAmount +from tinyman.utils import int_to_bytes +from tinyman.v2.constants import REMOVE_LIQUIDITY_APP_ARGUMENT +from tinyman.v2.contracts import get_pool_logicsig +from tinyman.v2.pools import Pool +from tinyman.v2.quotes import RemoveLiquidityQuote, SingleAssetRemoveLiquidityQuote + + +class RemoveLiquidityTestCase(BaseTestCase): + @classmethod + def setUpClass(cls): + cls.VALIDATOR_APP_ID = 12345 + cls.sender_private_key, cls.user_address = generate_account() + cls.asset_1_id = 10 + cls.asset_2_id = 8 + cls.pool_token_asset_id = 15 + cls.pool_address = get_pool_logicsig( + cls.VALIDATOR_APP_ID, cls.asset_1_id, cls.asset_2_id + ).address() + cls.application_address = get_application_address(cls.VALIDATOR_APP_ID) + cls.pool_state = cls.get_pool_state( + asset_1_reserves=1_000_000, + asset_2_reserves=100_000_000, + issued_pool_tokens=10_000_000, + ) + cls.pool = Pool.from_state( + address=cls.pool_address, + state=cls.pool_state, + round_number=100, + client=cls.get_tinyman_client(), + ) + + def test_remove_liquidity(self): + quote = self.pool.fetch_remove_liquidity_quote( + pool_token_asset_in=5_000_000, refresh=False + ) + + self.assertEqual(type(quote), RemoveLiquidityQuote) + self.assertEqual(quote.slippage, 0.05) + self.assertEqual( + quote.pool_token_asset_amount, + AssetAmount(self.pool.pool_token_asset, 5_000_000), + ) + self.assertEqual( + quote.amounts_out[self.pool.asset_1], + AssetAmount(self.pool.asset_1, 500_000), + ) + self.assertEqual( + quote.amounts_out[self.pool.asset_2], + AssetAmount(self.pool.asset_2, 50_000_000), + ) + self.assertEqual( + quote.amounts_out_with_slippage[self.pool.asset_1], + AssetAmount(self.pool.asset_1, 475_000), + ) + self.assertEqual( + quote.amounts_out_with_slippage[self.pool.asset_2], + AssetAmount(self.pool.asset_2, 47_500_000), + ) + + suggested_params = self.get_suggested_params() + txn_group = self.pool.prepare_remove_liquidity_transactions_from_quote( + quote=quote, suggested_params=suggested_params + ) + transactions = txn_group.transactions + + self.assertEqual(len(transactions), 2) + self.assertDictEqual( + dict(transactions[0].dictify()), + { + "aamt": quote.pool_token_asset_amount.amount, + "arcv": decode_address(self.pool_address), + "fee": 1000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": ASSETTRANSFER_TXN, + "xaid": self.pool.pool_token_asset.id, + }, + ) + self.assertDictEqual( + dict(transactions[1].dictify()), + { + "apaa": [ + REMOVE_LIQUIDITY_APP_ARGUMENT, + int_to_bytes( + quote.amounts_out_with_slippage[self.pool.asset_1].amount + ), + int_to_bytes( + quote.amounts_out_with_slippage[self.pool.asset_2].amount + ), + ], + "apan": OnComplete.NoOpOC, + "apas": [self.pool.asset_1.id, self.pool.asset_2.id], + "apat": [decode_address(self.pool_address)], + "apid": self.VALIDATOR_APP_ID, + "fee": 3000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": APPCALL_TXN, + }, + ) + + def test_single_asset_remove_liquidity(self): + quote = self.pool.fetch_single_asset_remove_liquidity_quote( + pool_token_asset_in=5_000_000, output_asset=self.pool.asset_1, refresh=False + ) + + self.assertEqual(type(quote), SingleAssetRemoveLiquidityQuote) + self.assertEqual(quote.slippage, 0.05) + self.assertEqual( + quote.pool_token_asset_amount, + AssetAmount(self.pool.pool_token_asset, 5_000_000), + ) + self.assertEqual(quote.amount_out, AssetAmount(self.pool.asset_1, 749_624)) + self.assertEqual( + quote.amount_out_with_slippage, AssetAmount(self.pool.asset_1, 712_143) + ) + + internal_swap_quote = quote.internal_swap_quote + self.assertEqual( + internal_swap_quote.amount_in, AssetAmount(self.pool.asset_2, 50_000_000) + ) + self.assertEqual( + internal_swap_quote.amount_out, AssetAmount(self.pool.asset_1, 249_624) + ) + self.assertEqual( + internal_swap_quote.swap_fees, AssetAmount(self.pool.asset_2, 150_000) + ) + self.assertEqual(internal_swap_quote.price_impact, 0.50075) + + suggested_params = self.get_suggested_params() + txn_group = self.pool.prepare_remove_liquidity_transactions_from_quote( + quote=quote, suggested_params=suggested_params + ) + transactions = txn_group.transactions + + self.assertEqual(len(transactions), 2) + self.assertDictEqual( + dict(transactions[0].dictify()), + { + "aamt": quote.pool_token_asset_amount.amount, + "arcv": decode_address(self.pool_address), + "fee": 1000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": ASSETTRANSFER_TXN, + "xaid": self.pool.pool_token_asset.id, + }, + ) + self.assertDictEqual( + dict(transactions[1].dictify()), + { + "apaa": [ + REMOVE_LIQUIDITY_APP_ARGUMENT, + int_to_bytes(quote.amount_out_with_slippage.amount), + int_to_bytes(0), + ], + "apan": OnComplete.NoOpOC, + "apas": [self.pool.asset_1.id], + "apat": [decode_address(self.pool_address)], + "apid": self.VALIDATOR_APP_ID, + "fee": 3000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": APPCALL_TXN, + }, + ) diff --git a/tests/v2/test_swap.py b/tests/v2/test_swap.py new file mode 100644 index 0000000..96decfd --- /dev/null +++ b/tests/v2/test_swap.py @@ -0,0 +1,170 @@ +from unittest.mock import ANY + +from algosdk.account import generate_account +from algosdk.constants import ASSETTRANSFER_TXN, APPCALL_TXN +from algosdk.encoding import decode_address +from algosdk.future.transaction import OnComplete +from algosdk.logic import get_application_address + +from tests.v2 import BaseTestCase +from tinyman.assets import AssetAmount +from tinyman.utils import int_to_bytes +from tinyman.v2.constants import ( + SWAP_APP_ARGUMENT, + FIXED_INPUT_APP_ARGUMENT, + FIXED_OUTPUT_APP_ARGUMENT, +) +from tinyman.v2.contracts import get_pool_logicsig +from tinyman.v2.pools import Pool +from tinyman.v2.quotes import SwapQuote + + +class SwapTestCase(BaseTestCase): + @classmethod + def setUpClass(cls): + cls.VALIDATOR_APP_ID = 12345 + cls.sender_private_key, cls.user_address = generate_account() + cls.asset_1_id = 10 + cls.asset_2_id = 8 + cls.pool_token_asset_id = 15 + cls.pool_address = get_pool_logicsig( + cls.VALIDATOR_APP_ID, cls.asset_1_id, cls.asset_2_id + ).address() + cls.application_address = get_application_address(cls.VALIDATOR_APP_ID) + cls.pool_state = cls.get_pool_state( + asset_1_reserves=10_000_000, + asset_2_reserves=1_000_000_000, + issued_pool_tokens=100_000_000, + ) + cls.pool = Pool.from_state( + address=cls.pool_address, + state=cls.pool_state, + round_number=100, + client=cls.get_tinyman_client(), + ) + + def test_fixed_input_swap(self): + quote = self.pool.fetch_fixed_input_swap_quote( + amount_in=AssetAmount(self.pool.asset_1, 10_000_000), refresh=False + ) + + self.assertEqual(type(quote), SwapQuote) + self.assertEqual(quote.slippage, 0.05) + self.assertEqual(quote.swap_type, "fixed-input") + self.assertEqual(quote.amount_in, AssetAmount(self.pool.asset_1, 10_000_000)) + self.assertEqual(quote.amount_out, AssetAmount(self.pool.asset_2, 499_248_873)) + self.assertEqual( + quote.amount_in_with_slippage, AssetAmount(self.pool.asset_1, 10_000_000) + ) + self.assertEqual( + quote.amount_out_with_slippage, AssetAmount(self.pool.asset_2, 474_286_430) + ) + self.assertEqual(quote.swap_fees, AssetAmount(self.pool.asset_1, 300_00)) + self.assertEqual(quote.price_impact, 0.50075) + + suggested_params = self.get_suggested_params() + txn_group = self.pool.prepare_swap_transactions_from_quote( + quote=quote, suggested_params=suggested_params + ) + transactions = txn_group.transactions + + self.assertEqual(len(transactions), 2) + self.assertDictEqual( + dict(transactions[0].dictify()), + { + "aamt": quote.amount_in.amount, + "arcv": decode_address(self.pool_address), + "fee": 1000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": ASSETTRANSFER_TXN, + "xaid": quote.amount_in.asset.id, + }, + ) + self.assertDictEqual( + dict(transactions[1].dictify()), + { + "apaa": [ + SWAP_APP_ARGUMENT, + FIXED_INPUT_APP_ARGUMENT, + int_to_bytes(quote.amount_out_with_slippage.amount), + ], + "apan": OnComplete.NoOpOC, + "apas": [self.pool.asset_1.id, self.pool.asset_2.id], + "apat": [decode_address(self.pool_address)], + "apid": self.VALIDATOR_APP_ID, + "fee": 2000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": APPCALL_TXN, + }, + ) + + def test_fixed_output_swap(self): + quote = self.pool.fetch_fixed_output_swap_quote( + amount_out=AssetAmount(self.pool.asset_2, 499_248_873), refresh=False + ) + + self.assertEqual(type(quote), SwapQuote) + self.assertEqual(quote.slippage, 0.05) + self.assertEqual(quote.swap_type, "fixed-output") + self.assertEqual(quote.amount_in, AssetAmount(self.pool.asset_1, 10_000_000)) + self.assertEqual(quote.amount_out, AssetAmount(self.pool.asset_2, 499_248_873)) + self.assertEqual( + quote.amount_in_with_slippage, AssetAmount(self.pool.asset_1, 10_500_000) + ) + self.assertEqual( + quote.amount_out_with_slippage, AssetAmount(self.pool.asset_2, 499_248_873) + ) + self.assertEqual(quote.swap_fees, AssetAmount(self.pool.asset_1, 300_00)) + self.assertEqual(quote.price_impact, 0.50075) + + suggested_params = self.get_suggested_params() + txn_group = self.pool.prepare_swap_transactions_from_quote( + quote=quote, suggested_params=suggested_params + ) + transactions = txn_group.transactions + + self.assertEqual(len(transactions), 2) + self.assertDictEqual( + dict(transactions[0].dictify()), + { + "aamt": quote.amount_in_with_slippage.amount, + "arcv": decode_address(self.pool_address), + "fee": 1000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": ASSETTRANSFER_TXN, + "xaid": quote.amount_in.asset.id, + }, + ) + self.assertDictEqual( + dict(transactions[1].dictify()), + { + "apaa": [ + SWAP_APP_ARGUMENT, + FIXED_OUTPUT_APP_ARGUMENT, + int_to_bytes(quote.amount_out.amount), + ], + "apan": OnComplete.NoOpOC, + "apas": [self.pool.asset_1.id, self.pool.asset_2.id], + "apat": [decode_address(self.pool_address)], + "apid": self.VALIDATOR_APP_ID, + "fee": 3000, + "fv": ANY, + "gh": ANY, + "grp": ANY, + "lv": ANY, + "snd": decode_address(self.user_address), + "type": APPCALL_TXN, + }, + ) diff --git a/tinyman/assets.py b/tinyman/assets.py index f367058..06a3a5f 100644 --- a/tinyman/assets.py +++ b/tinyman/assets.py @@ -75,5 +75,10 @@ def __eq__(self, other: "AssetAmount"): raise TypeError("Unsupported types for ==") def __repr__(self) -> str: - amount = Decimal(self.amount) / Decimal(10**self.asset.decimals) - return f"{self.asset.unit_name}('{amount}')" + if self.asset.decimals is not None: + amount = ( + Decimal(self.amount) / Decimal(10**self.asset.decimals) + ).quantize(1 / Decimal(10**self.asset.decimals)) + return f"{self.asset.unit_name}('{amount}' Base Unit)" + else: + return f"{self.asset.unit_name}('{self.amount}' Micro Unit)" diff --git a/tinyman/client.py b/tinyman/client.py index b9156cf..0af997c 100644 --- a/tinyman/client.py +++ b/tinyman/client.py @@ -1,3 +1,5 @@ +from typing import Optional + from algosdk.v2client.algod import AlgodClient from algosdk.future.transaction import wait_for_confirmation from tinyman.assets import Asset @@ -10,7 +12,7 @@ def __init__( algod_client: AlgodClient, validator_app_id: int, user_address=None, - staking_app_id: int = None, + staking_app_id: Optional[int] = None, ): self.algod = algod_client self.validator_app_id = validator_app_id diff --git a/tinyman/v2/bootstrap.py b/tinyman/v2/bootstrap.py index 2bba469..7936983 100644 --- a/tinyman/v2/bootstrap.py +++ b/tinyman/v2/bootstrap.py @@ -32,7 +32,7 @@ def prepare_bootstrap_transactions( sender=sender, sp=suggested_params, receiver=pool_address, - amt=int(required_algo), + amt=required_algo, ) ) diff --git a/tinyman/v2/constants.py b/tinyman/v2/constants.py index 8b7703e..87e2ba0 100644 --- a/tinyman/v2/constants.py +++ b/tinyman/v2/constants.py @@ -47,5 +47,7 @@ # + 100,000 ASA 2 # + 100,000 Pool Token # + 542,500 App Optin (100000 + (25000+3500)*12 + (25000+25000)*2) -MIN_POOL_BALANCE_ASA_ALGO_PAIR = 300_000 + (100_000 + (25_000 + 3_500) * APP_LOCAL_INTS + (25_000 + 25_000) * APP_LOCAL_BYTES) +MIN_POOL_BALANCE_ASA_ALGO_PAIR = 300_000 + ( + 100_000 + (25_000 + 3_500) * APP_LOCAL_INTS + (25_000 + 25_000) * APP_LOCAL_BYTES +) MIN_POOL_BALANCE_ASA_ASA_PAIR = MIN_POOL_BALANCE_ASA_ALGO_PAIR + 100_000 diff --git a/tinyman/v2/flash_swap.py b/tinyman/v2/flash_swap.py index 7fb6178..76e3f17 100644 --- a/tinyman/v2/flash_swap.py +++ b/tinyman/v2/flash_swap.py @@ -1,8 +1,6 @@ from algosdk.future.transaction import ( Transaction, ApplicationNoOpTxn, - PaymentTxn, - AssetTransferTxn, SuggestedParams, ) @@ -20,8 +18,6 @@ def prepare_flash_swap_transactions( asset_2_id: int, asset_1_loan_amount: int, asset_2_loan_amount: int, - asset_1_payment_amount: int, - asset_2_payment_amount: int, transactions: list[Transaction], sender: str, suggested_params: SuggestedParams, @@ -37,12 +33,7 @@ def prepare_flash_swap_transactions( else: inner_transaction_count = 1 - if asset_1_payment_amount and asset_2_payment_amount: - payment_count = 2 - else: - payment_count = 1 - - index_diff = len(transactions) + payment_count + 1 + index_diff = len(transactions) + 1 txns = [ # Flash Swap ApplicationNoOpTxn( @@ -65,38 +56,6 @@ def prepare_flash_swap_transactions( if transactions: txns.extend(transactions) - if asset_1_payment_amount: - txns.append( - AssetTransferTxn( - sender=sender, - sp=suggested_params, - receiver=pool_address, - index=asset_1_id, - amt=asset_1_payment_amount, - ) - ) - - if asset_2_payment_amount: - if asset_2_id: - txns.append( - AssetTransferTxn( - sender=sender, - sp=suggested_params, - receiver=pool_address, - index=asset_2_id, - amt=asset_2_payment_amount, - ) - ) - else: - txns.append( - PaymentTxn( - sender=sender, - sp=suggested_params, - receiver=pool_address, - amt=asset_2_payment_amount, - ) - ) - # Verify Flash Swap txns.append( ApplicationNoOpTxn( diff --git a/tinyman/v2/formulas.py b/tinyman/v2/formulas.py index 9ded684..d05a30b 100644 --- a/tinyman/v2/formulas.py +++ b/tinyman/v2/formulas.py @@ -141,13 +141,18 @@ def calculate_remove_liquidity_output_amounts( asset_2_reserves: int, issued_pool_tokens: int, ) -> (int, int): - asset_1_output_amount = int( - (pool_token_asset_amount * asset_1_reserves) / issued_pool_tokens - ) - asset_2_output_amount = int( - (pool_token_asset_amount * asset_2_reserves) / issued_pool_tokens - ) - return asset_1_output_amount, asset_2_output_amount + if issued_pool_tokens > (pool_token_asset_amount + LOCKED_POOL_TOKENS): + asset_1_output_amount = ( + pool_token_asset_amount * asset_1_reserves / issued_pool_tokens + ) + asset_2_output_amount = ( + pool_token_asset_amount * asset_2_reserves / issued_pool_tokens + ) + else: + asset_1_output_amount = asset_1_reserves + asset_2_output_amount = asset_2_reserves + + return int(asset_1_output_amount), int(asset_2_output_amount) def calculate_subsequent_add_liquidity( diff --git a/tinyman/v2/pools.py b/tinyman/v2/pools.py index b955070..6deaccc 100644 --- a/tinyman/v2/pools.py +++ b/tinyman/v2/pools.py @@ -34,6 +34,7 @@ calculate_remove_liquidity_output_amounts, calculate_fixed_output_swap, calculate_flash_loan_payment_amount, + calculate_fixed_input_fee_amount, ) from .quotes import ( FlexibleAddLiquidityQuote, @@ -69,11 +70,13 @@ def get_pool_info( pool_address = pool_logicsig.address() account_info = client.account_info(pool_address) pool_state = get_pool_state_from_account_info(account_info) - if pool_state: - generate_pool_info( - pool_address, validator_app_id, account_info["round"], pool_state - ) - return {} + + return generate_pool_info( + address=pool_address, + validator_app_id=validator_app_id, + round_number=account_info.get("round"), + state=pool_state, + ) def get_validator_app_id_from_account_info(account_info: dict) -> Optional[int]: @@ -107,9 +110,15 @@ def __init__( ) if isinstance(asset_a, int): - asset_a = client.fetch_asset(asset_a) + if fetch: + asset_a = client.fetch_asset(asset_a) + else: + asset_a = Asset(id=asset_a) if isinstance(asset_b, int): - asset_b = client.fetch_asset(asset_b) + if fetch: + asset_b = client.fetch_asset(asset_b) + else: + asset_b = Asset(id=asset_b) if asset_a.id > asset_b.id: self.asset_1 = asset_a @@ -132,7 +141,7 @@ def __init__( if fetch: self.refresh() elif info is not None: - self.update_from_info(info) + 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}" @@ -174,6 +183,8 @@ def from_state( client: TinymanV2Client, fetch: bool = False, ): + assert state + info = generate_pool_info( address=address, validator_app_id=client.validator_app_id, @@ -199,23 +210,28 @@ def refresh(self, info: Optional[dict] = None) -> None: self.asset_1.id, self.asset_2.id, ) - if not info: - return self.update_from_info(info) - def update_from_info(self, info: dict) -> None: - if info["pool_token_asset_id"] is not None: + def update_from_info(self, info: dict, fetch: bool = True) -> None: + if info.get("pool_token_asset_id"): self.exists = True - - self.pool_token_asset = self.client.fetch_asset(info["pool_token_asset_id"]) - self.asset_1_reserves = info["asset_1_reserves"] - self.asset_2_reserves = info["asset_2_reserves"] - self.issued_pool_tokens = info["issued_pool_tokens"] - self.asset_1_protocol_fees = info["asset_1_protocol_fees"] - self.asset_2_protocol_fees = info["asset_2_protocol_fees"] - self.total_fee_share = info["total_fee_share"] - self.protocol_fee_ratio = info["protocol_fee_ratio"] - self.last_refreshed_round = info["round"] + if fetch: + self.pool_token_asset = self.client.fetch_asset( + info["pool_token_asset_id"] + ) + else: + self.pool_token_asset = Asset( + id=info["pool_token_asset_id"], unit_name="TMPOOL2", decimals=6 + ) + + self.asset_1_reserves = info["asset_1_reserves"] + self.asset_2_reserves = info["asset_2_reserves"] + self.issued_pool_tokens = info["issued_pool_tokens"] + self.asset_1_protocol_fees = info["asset_1_protocol_fees"] + self.asset_2_protocol_fees = info["asset_2_protocol_fees"] + self.total_fee_share = info["total_fee_share"] + self.protocol_fee_ratio = info["protocol_fee_ratio"] + self.last_refreshed_round = info["round"] def get_logicsig(self) -> LogicSigAccount: pool_logicsig = get_pool_logicsig( @@ -623,23 +639,27 @@ def prepare_add_liquidity_transactions_from_quote( InitialAddLiquidityQuote, ], user_address: Optional[str] = None, + suggested_params: SuggestedParams = None, ) -> TransactionGroup: if isinstance(quote, FlexibleAddLiquidityQuote): return self.prepare_flexible_add_liquidity_transactions( amounts_in=quote.amounts_in, min_pool_token_asset_amount=quote.min_pool_token_asset_amount_with_slippage, user_address=user_address, + suggested_params=suggested_params, ) elif isinstance(quote, SingleAssetAddLiquidityQuote): return self.prepare_single_asset_add_liquidity_transactions( amount_in=quote.amount_in, min_pool_token_asset_amount=quote.min_pool_token_asset_amount_with_slippage, user_address=user_address, + suggested_params=suggested_params, ) elif isinstance(quote, InitialAddLiquidityQuote): return self.prepare_initial_add_liquidity_transactions( amounts_in=quote.amounts_in, user_address=user_address, + suggested_params=suggested_params, ) raise Exception(f"Invalid quote type({type(quote)})") @@ -829,6 +849,7 @@ def prepare_remove_liquidity_transactions_from_quote( self, quote: [RemoveLiquidityQuote, SingleAssetRemoveLiquidityQuote], user_address: Optional[str] = None, + suggested_params: SuggestedParams = None, ) -> TransactionGroup: user_address = user_address or self.client.user_address @@ -837,6 +858,7 @@ def prepare_remove_liquidity_transactions_from_quote( pool_token_asset_amount=quote.pool_token_asset_amount, amount_out=quote.amount_out_with_slippage, user_address=user_address, + suggested_params=suggested_params, ) elif isinstance(quote, RemoveLiquidityQuote): return self.prepare_remove_liquidity_transactions( @@ -846,6 +868,7 @@ def prepare_remove_liquidity_transactions_from_quote( self.asset_2: quote.amounts_out_with_slippage[self.asset_2], }, user_address=user_address, + suggested_params=suggested_params, ) raise NotImplementedError() @@ -959,13 +982,17 @@ def prepare_swap_transactions( return txn_group def prepare_swap_transactions_from_quote( - self, quote: SwapQuote, user_address: str = None + self, + quote: SwapQuote, + user_address: str = None, + suggested_params: SuggestedParams = None, ) -> TransactionGroup: return self.prepare_swap_transactions( amount_in=quote.amount_in_with_slippage, amount_out=quote.amount_out_with_slippage, swap_type=quote.swap_type, user_address=user_address, + suggested_params=suggested_params, ) def fetch_flash_loan_quote( @@ -1029,6 +1056,22 @@ def fetch_flash_loan_quote( ), ), }, + fees={ + self.asset_1: AssetAmount( + self.asset_1, + amount=calculate_fixed_input_fee_amount( + input_amount=loan_amount_1.amount, + total_fee_share=self.total_fee_share, + ), + ), + self.asset_2: AssetAmount( + self.asset_2, + amount=calculate_fixed_input_fee_amount( + input_amount=loan_amount_2.amount, + total_fee_share=self.total_fee_share, + ), + ), + }, ) return quote @@ -1064,6 +1107,7 @@ def prepare_flash_loan_transactions_from_quote( quote: FlashLoanQuote, transactions: list[Transaction], user_address: str = None, + suggested_params: SuggestedParams = None, ) -> TransactionGroup: user_address = user_address or self.client.user_address @@ -1072,6 +1116,7 @@ def prepare_flash_loan_transactions_from_quote( amounts_in=quote.amounts_in, transactions=transactions, user_address=user_address, + suggested_params=suggested_params, ) def prepare_claim_fees_transactions( diff --git a/tinyman/v2/quotes.py b/tinyman/v2/quotes.py index fcfee1f..9ccb662 100644 --- a/tinyman/v2/quotes.py +++ b/tinyman/v2/quotes.py @@ -126,3 +126,4 @@ def amount_out_with_slippage(self) -> AssetAmount: class FlashLoanQuote: amounts_out: dict[Asset, AssetAmount] amounts_in: dict[Asset, AssetAmount] + fees: dict[Asset, AssetAmount] From b146461e6a115e47a80bf10dc12187cc86442b87 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 12 Oct 2022 11:27:23 +0300 Subject: [PATCH 41/73] use calculate_price_impact function for V1 calculations --- tinyman/v1/pools.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tinyman/v1/pools.py b/tinyman/v1/pools.py index 134fe87..76dc0a8 100644 --- a/tinyman/v1/pools.py +++ b/tinyman/v1/pools.py @@ -4,7 +4,7 @@ from algosdk.v2client.algod import AlgodClient from algosdk.encoding import decode_address from .contracts import get_pool_logicsig -from tinyman.utils import get_state_int +from tinyman.utils import get_state_int, calculate_price_impact from tinyman.assets import Asset, AssetAmount from .swap import prepare_swap_transactions from .bootstrap import prepare_bootstrap_transactions @@ -372,9 +372,12 @@ def fetch_fixed_input_swap_quote( amount_out = AssetAmount(asset_out, int(asset_out_amount)) - swap_price = amount_out.amount / amount_in.amount - pool_price = output_supply / input_supply - price_impact = abs(round((swap_price / pool_price) - 1, 5)) + price_impact = calculate_price_impact( + input_supply=input_supply, + output_supply=output_supply, + swap_input_amount=amount_in.amount, + swap_output_amount=amount_out.amount, + ) quote = SwapQuote( swap_type="fixed-input", @@ -413,9 +416,12 @@ def fetch_fixed_output_swap_quote( amount_in = AssetAmount(asset_in, int(asset_in_amount)) - swap_price = amount_out.amount / amount_in.amount - pool_price = output_supply / input_supply - price_impact = abs(round((swap_price / pool_price) - 1, 5)) + price_impact = calculate_price_impact( + input_supply=input_supply, + output_supply=output_supply, + swap_input_amount=amount_in.amount, + swap_output_amount=amount_out.amount, + ) quote = SwapQuote( swap_type="fixed-output", From 064a810bc7c686a9280362b143df42f98658cf9b Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 12 Oct 2022 11:28:00 +0300 Subject: [PATCH 42/73] remove redundant and late int conversions --- .gitignore | 1 + tinyman/v2/add_liquidity.py | 20 ++++++++++---------- tinyman/v2/constants.py | 1 - tinyman/v2/pools.py | 8 ++++---- tinyman/v2/swap.py | 8 ++++---- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index fa5e850..eff2877 100644 --- a/.gitignore +++ b/.gitignore @@ -131,5 +131,6 @@ dmypy.json # Pyre type checker .pyre/ +# Tutorials account*.json assets*.json diff --git a/tinyman/v2/add_liquidity.py b/tinyman/v2/add_liquidity.py index 39a555e..823395f 100644 --- a/tinyman/v2/add_liquidity.py +++ b/tinyman/v2/add_liquidity.py @@ -36,14 +36,14 @@ def prepare_flexible_add_liquidity_transactions( sender=sender, sp=suggested_params, receiver=pool_address, - amt=int(asset_1_amount), + amt=asset_1_amount, index=asset_1_id, ), AssetTransferTxn( sender=sender, sp=suggested_params, receiver=pool_address, - amt=int(asset_2_amount), + amt=asset_2_amount, index=asset_2_id, ) if asset_2_id != 0 @@ -51,7 +51,7 @@ def prepare_flexible_add_liquidity_transactions( sender=sender, sp=suggested_params, receiver=pool_address, - amt=int(asset_2_amount), + amt=asset_2_amount, ), ApplicationNoOpTxn( sender=sender, @@ -60,7 +60,7 @@ def prepare_flexible_add_liquidity_transactions( app_args=[ ADD_LIQUIDITY_APP_ARGUMENT, ADD_LIQUIDITY_FLEXIBLE_MODE_APP_ARGUMENT, - int(min_pool_token_asset_amount), + min_pool_token_asset_amount, ], foreign_assets=[pool_token_asset_id], accounts=[pool_address], @@ -109,7 +109,7 @@ def prepare_single_asset_add_liquidity_transactions( sender=sender, sp=suggested_params, receiver=pool_address, - amt=int(asset_in_amount), + amt=asset_in_amount, index=asset_in_id, ) if asset_in_id != 0 @@ -117,7 +117,7 @@ def prepare_single_asset_add_liquidity_transactions( sender=sender, sp=suggested_params, receiver=pool_address, - amt=int(asset_in_amount), + amt=asset_in_amount, ), ApplicationNoOpTxn( sender=sender, @@ -126,7 +126,7 @@ def prepare_single_asset_add_liquidity_transactions( app_args=[ ADD_LIQUIDITY_APP_ARGUMENT, ADD_LIQUIDITY_SINGLE_MODE_APP_ARGUMENT, - int(min_pool_token_asset_amount), + min_pool_token_asset_amount, ], foreign_assets=[pool_token_asset_id], accounts=[pool_address], @@ -159,14 +159,14 @@ def prepare_initial_add_liquidity_transactions( sender=sender, sp=suggested_params, receiver=pool_address, - amt=int(asset_1_amount), + amt=asset_1_amount, index=asset_1_id, ), AssetTransferTxn( sender=sender, sp=suggested_params, receiver=pool_address, - amt=int(asset_2_amount), + amt=asset_2_amount, index=asset_2_id, ) if asset_2_id != 0 @@ -174,7 +174,7 @@ def prepare_initial_add_liquidity_transactions( sender=sender, sp=suggested_params, receiver=pool_address, - amt=int(asset_2_amount), + amt=asset_2_amount, ), ApplicationNoOpTxn( sender=sender, diff --git a/tinyman/v2/constants.py b/tinyman/v2/constants.py index 87e2ba0..cd2f456 100644 --- a/tinyman/v2/constants.py +++ b/tinyman/v2/constants.py @@ -32,7 +32,6 @@ TESTNET_VALIDATOR_APP_ADDRESS = get_application_address(TESTNET_VALIDATOR_APP_ID) # MAINNET_VALIDATOR_APP__ADDRESS = get_application_address(MAINNET_VALIDATOR_APP_ID) - LOCKED_POOL_TOKENS = 1000 ASSET_MIN_TOTAL = 1000000 diff --git a/tinyman/v2/pools.py b/tinyman/v2/pools.py index 6deaccc..902d81e 100644 --- a/tinyman/v2/pools.py +++ b/tinyman/v2/pools.py @@ -423,7 +423,7 @@ def fetch_flexible_add_liquidity_quote( ), swap_fees=AssetAmount( self.asset_1 if swap_from_asset_1_to_asset_2 else self.asset_2, - int(swap_total_fee_amount), + swap_total_fee_amount, ), price_impact=swap_price_impact, ) @@ -499,7 +499,7 @@ def fetch_single_asset_add_liquidity_quote( ), swap_fees=AssetAmount( self.asset_1 if swap_from_asset_1_to_asset_2 else self.asset_2, - int(swap_total_fee_amount), + swap_total_fee_amount, ), price_impact=swap_price_impact, ) @@ -742,7 +742,7 @@ def fetch_single_asset_remove_liquidity_quote( internal_swap_quote = InternalSwapQuote( amount_in=AssetAmount(self.asset_2, asset_2_output_amount), amount_out=AssetAmount(self.asset_1, swap_output_amount), - swap_fees=AssetAmount(self.asset_2, int(total_fee_amount)), + swap_fees=AssetAmount(self.asset_2, total_fee_amount), price_impact=price_impact, ) quote = SingleAssetRemoveLiquidityQuote( @@ -767,7 +767,7 @@ def fetch_single_asset_remove_liquidity_quote( internal_swap_quote = InternalSwapQuote( amount_in=AssetAmount(self.asset_1, asset_1_output_amount), amount_out=AssetAmount(self.asset_2, swap_output_amount), - swap_fees=AssetAmount(self.asset_1, int(total_fee_amount)), + swap_fees=AssetAmount(self.asset_1, total_fee_amount), price_impact=price_impact, ) quote = SingleAssetRemoveLiquidityQuote( diff --git a/tinyman/v2/swap.py b/tinyman/v2/swap.py index 0952d6b..dabc6c8 100644 --- a/tinyman/v2/swap.py +++ b/tinyman/v2/swap.py @@ -34,20 +34,20 @@ def prepare_swap_transactions( sp=suggested_params, receiver=pool_address, index=asset_in_id, - amt=int(asset_in_amount), + amt=asset_in_amount, ) if asset_in_id != 0 else PaymentTxn( sender=sender, sp=suggested_params, receiver=pool_address, - amt=int(asset_in_amount), + amt=asset_in_amount, ), ApplicationNoOpTxn( sender=sender, sp=suggested_params, index=validator_app_id, - app_args=[SWAP_APP_ARGUMENT, swap_type, int(asset_out_amount)], + app_args=[SWAP_APP_ARGUMENT, swap_type, asset_out_amount], foreign_assets=[asset_1_id, asset_2_id], accounts=[pool_address], ), @@ -65,7 +65,7 @@ def prepare_swap_transactions( # App call contains 1 inner transaction app_call_fee = min_fee * 2 elif swap_type == FIXED_OUTPUT_APP_ARGUMENT: - # App call contains 2 inner transaction2 + # App call contains 2 inner transactions app_call_fee = min_fee * 3 else: raise NotImplementedError() From 3c2f4b0fbe636a8279e70ea0ce9f4358265f1dc8 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 12 Oct 2022 13:09:12 +0300 Subject: [PATCH 43/73] drop python 3.7 support --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b4ce07e..28727c9 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ }, install_requires=["py-algorand-sdk >= 1.6.0"], packages=setuptools.find_packages(), - python_requires=">=3.7", + python_requires=">=3.8", package_data={"tinyman.v1": ["asc.json"]}, include_package_data=True, ) From 566a210b20ba807068330e8619db5c80be0f4a99 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 12 Oct 2022 13:09:46 +0300 Subject: [PATCH 44/73] setup github actions --- .github/workflows/tests.yml | 32 ++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 4 ++-- 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..c79c48b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,32 @@ +name: Lint & Tests + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.9", "3.8"] + py-algorand-sdk-version: ["1.18", "1.17", "1.16", "1.15", "1.14", "1.13"] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 black py-algorand-sdk==${{ matrix.py-algorand-sdk-version }} + + - name: Run pre-commit hooks + uses: pre-commit/action@v3.0.0 + + - name: Run Unit tests + run: | + python -m unittest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 24c7e2f..0b32a2e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,12 +3,12 @@ repos: rev: '3.9.2' # pick a git hash / tag to point to hooks: - id: flake8 - args: ['--ignore=E501,F403,F405,E126,E121,W503,E203'] + args: ['--ignore=E501,F403,F405,E126,E121,W503,E203', '.'] exclude: ^(env|venv) - repo: https://github.com/psf/black rev: 22.8.0 hooks: - id: black - args: ['--check'] + args: ['.', '--check'] exclude: ^(env|venv) From 5bd93acc7b3c25092a40d4778db2a63319528485 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 12 Oct 2022 13:09:50 +0300 Subject: [PATCH 45/73] fix type annotations --- tinyman/staking/__init__.py | 6 +++--- tinyman/v2/flash_loan.py | 2 +- tinyman/v2/flash_swap.py | 2 +- tinyman/v2/pools.py | 14 +++++++------- tinyman/v2/quotes.py | 14 +++++++------- tinyman/v2/utils.py | 2 +- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tinyman/staking/__init__.py b/tinyman/staking/__init__.py index fc1d92e..9d09cd9 100644 --- a/tinyman/staking/__init__.py +++ b/tinyman/staking/__init__.py @@ -3,7 +3,7 @@ from base64 import b64decode, b64encode from datetime import datetime from hashlib import sha256 -from typing import List, Optional +from typing import Optional from algosdk.constants import PAYMENT_TXN, ASSETTRANSFER_TXN from algosdk.encoding import is_valid_address @@ -221,8 +221,8 @@ def prepare_setup_transaction( reward_period: int, start_time: int, end_time: int, - asset_ids: List[int], - min_amounts: List[int], + asset_ids: "list[int]", + min_amounts: "list[int]", sender, suggested_params, ): diff --git a/tinyman/v2/flash_loan.py b/tinyman/v2/flash_loan.py index 11e4192..b4df949 100644 --- a/tinyman/v2/flash_loan.py +++ b/tinyman/v2/flash_loan.py @@ -22,7 +22,7 @@ def prepare_flash_loan_transactions( asset_2_loan_amount: int, asset_1_payment_amount: int, asset_2_payment_amount: int, - transactions: list[Transaction], + transactions: "list[Transaction]", sender: str, suggested_params: SuggestedParams, ) -> TransactionGroup: diff --git a/tinyman/v2/flash_swap.py b/tinyman/v2/flash_swap.py index 76e3f17..b89dbbc 100644 --- a/tinyman/v2/flash_swap.py +++ b/tinyman/v2/flash_swap.py @@ -18,7 +18,7 @@ def prepare_flash_swap_transactions( asset_2_id: int, asset_1_loan_amount: int, asset_2_loan_amount: int, - transactions: list[Transaction], + transactions: "list[Transaction]", sender: str, suggested_params: SuggestedParams, ) -> TransactionGroup: diff --git a/tinyman/v2/pools.py b/tinyman/v2/pools.py index 902d81e..ff63739 100644 --- a/tinyman/v2/pools.py +++ b/tinyman/v2/pools.py @@ -552,7 +552,7 @@ def fetch_initial_add_liquidity_quote( def prepare_flexible_add_liquidity_transactions( self, - amounts_in: dict[Asset, AssetAmount], + amounts_in: "dict[Asset, AssetAmount]", min_pool_token_asset_amount: int, user_address: Optional[str] = None, suggested_params: SuggestedParams = None, @@ -608,7 +608,7 @@ def prepare_single_asset_add_liquidity_transactions( def prepare_initial_add_liquidity_transactions( self, - amounts_in: dict[Asset, AssetAmount], + amounts_in: "dict[Asset, AssetAmount]", user_address: Optional[str] = None, suggested_params: SuggestedParams = None, ) -> TransactionGroup: @@ -786,7 +786,7 @@ def fetch_single_asset_remove_liquidity_quote( def prepare_remove_liquidity_transactions( self, pool_token_asset_amount: [AssetAmount, int], - amounts_out: dict[Asset, AssetAmount], + amounts_out: "dict[Asset, AssetAmount]", user_address: Optional[str] = None, suggested_params: SuggestedParams = None, ) -> TransactionGroup: @@ -1077,9 +1077,9 @@ def fetch_flash_loan_quote( def prepare_flash_loan_transactions( self, - amounts_out: dict[Asset, AssetAmount], - amounts_in: dict[Asset, AssetAmount], - transactions: list[Transaction], + amounts_out: "dict[Asset, AssetAmount]", + amounts_in: "dict[Asset, AssetAmount]", + transactions: "list[Transaction]", user_address: str = None, suggested_params: SuggestedParams = None, ) -> TransactionGroup: @@ -1105,7 +1105,7 @@ def prepare_flash_loan_transactions( def prepare_flash_loan_transactions_from_quote( self, quote: FlashLoanQuote, - transactions: list[Transaction], + transactions: "list[Transaction]", user_address: str = None, suggested_params: SuggestedParams = None, ) -> TransactionGroup: diff --git a/tinyman/v2/quotes.py b/tinyman/v2/quotes.py index 9ccb662..d0edd41 100644 --- a/tinyman/v2/quotes.py +++ b/tinyman/v2/quotes.py @@ -58,7 +58,7 @@ def price(self) -> float: @dataclass class FlexibleAddLiquidityQuote: - amounts_in: dict[Asset, AssetAmount] + amounts_in: "dict[Asset, AssetAmount]" pool_token_asset_amount: AssetAmount slippage: float internal_swap_quote: InternalSwapQuote = None @@ -86,18 +86,18 @@ def min_pool_token_asset_amount_with_slippage(self) -> int: @dataclass class InitialAddLiquidityQuote: - amounts_in: dict[Asset, AssetAmount] + amounts_in: "dict[Asset, AssetAmount]" pool_token_asset_amount: AssetAmount @dataclass class RemoveLiquidityQuote: - amounts_out: dict[Asset, AssetAmount] + amounts_out: "dict[Asset, AssetAmount]" pool_token_asset_amount: AssetAmount slippage: float @property - def amounts_out_with_slippage(self) -> dict[Asset, AssetAmount]: + def amounts_out_with_slippage(self) -> "dict[Asset, AssetAmount]": amounts_out = {} for asset, asset_amount in self.amounts_out.items(): amount_with_slippage = asset_amount.amount - int( @@ -124,6 +124,6 @@ def amount_out_with_slippage(self) -> AssetAmount: @dataclass class FlashLoanQuote: - amounts_out: dict[Asset, AssetAmount] - amounts_in: dict[Asset, AssetAmount] - fees: dict[Asset, AssetAmount] + amounts_out: "dict[Asset, AssetAmount]" + amounts_in: "dict[Asset, AssetAmount]" + fees: "dict[Asset, AssetAmount]" diff --git a/tinyman/v2/utils.py b/tinyman/v2/utils.py index a297477..8a652c2 100644 --- a/tinyman/v2/utils.py +++ b/tinyman/v2/utils.py @@ -3,7 +3,7 @@ from tinyman.utils import bytes_to_int -def decode_logs(logs: list[[bytes, str]]) -> dict: +def decode_logs(logs: "list[[bytes, str]]") -> dict: decoded_logs = dict() for log in logs: if type(log) == str: From a6834bf0b96e6f1db1ef8e25aa49f7c2da6b2d32 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 12 Oct 2022 14:41:16 +0300 Subject: [PATCH 46/73] remove pre-commit actions --- .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 c79c48b..84562cf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,8 +24,11 @@ jobs: python -m pip install --upgrade pip pip install flake8 black py-algorand-sdk==${{ matrix.py-algorand-sdk-version }} - - name: Run pre-commit hooks - uses: pre-commit/action@v3.0.0 + - name: Run flake8 + run: flake8 ${{ github.workspace }} --ignore=E501,F403,F405,E126,E121,W503,E203 + + - name: Run Black + run: black ${{ github.workspace }} --check - name: Run Unit tests run: | From 4396677d2573595bad0913b493ec1879813096b0 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 12 Oct 2022 16:49:51 +0300 Subject: [PATCH 47/73] update claim extra app call --- tinyman/v2/fees.py | 11 +++++------ tinyman/v2/pools.py | 22 ---------------------- 2 files changed, 5 insertions(+), 28 deletions(-) diff --git a/tinyman/v2/fees.py b/tinyman/v2/fees.py index 47516a1..99e4d30 100644 --- a/tinyman/v2/fees.py +++ b/tinyman/v2/fees.py @@ -41,9 +41,8 @@ def prepare_claim_fees_transactions( def prepare_claim_extra_transactions( validator_app_id: int, - asset_1_id: int, - asset_2_id: int, - pool_address: str, + asset_id: int, + address: str, fee_collector: str, sender: str, suggested_params: SuggestedParams, @@ -54,13 +53,13 @@ def prepare_claim_extra_transactions( sp=suggested_params, index=validator_app_id, app_args=[CLAIM_EXTRA_APP_ARGUMENT], - foreign_assets=[asset_1_id, asset_2_id], - accounts=[pool_address, fee_collector], + foreign_assets=[asset_id], + accounts=[address, fee_collector], ), ] min_fee = suggested_params.min_fee - app_call_fee = min_fee * 3 + app_call_fee = min_fee * 2 txns[-1].fee = app_call_fee txn_group = TransactionGroup(txns) diff --git a/tinyman/v2/pools.py b/tinyman/v2/pools.py index ff63739..6c50f41 100644 --- a/tinyman/v2/pools.py +++ b/tinyman/v2/pools.py @@ -23,7 +23,6 @@ ) from .fees import ( prepare_claim_fees_transactions, - prepare_claim_extra_transactions, prepare_set_fee_transactions, ) from .flash_loan import prepare_flash_loan_transactions @@ -1140,27 +1139,6 @@ def prepare_claim_fees_transactions( suggested_params=suggested_params, ) - def prepare_claim_extra_transactions( - self, - fee_collector: str, - user_address: str = None, - suggested_params: SuggestedParams = None, - ) -> TransactionGroup: - user_address = user_address or self.client.user_address - - if suggested_params is None: - suggested_params = self.client.algod.suggested_params() - - return prepare_claim_extra_transactions( - validator_app_id=self.validator_app_id, - asset_1_id=self.asset_1.id, - asset_2_id=self.asset_2.id, - pool_address=self.address, - fee_collector=fee_collector, - sender=user_address, - suggested_params=suggested_params, - ) - def prepare_set_fee_transactions( self, total_fee_share: int, From 572f120d04333dcf01a76980b8d10c4ffcde316f Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Fri, 21 Oct 2022 15:13:30 +0300 Subject: [PATCH 48/73] fix typo --- tinyman/v2/formulas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tinyman/v2/formulas.py b/tinyman/v2/formulas.py index d05a30b..87664c7 100644 --- a/tinyman/v2/formulas.py +++ b/tinyman/v2/formulas.py @@ -24,7 +24,7 @@ def calculate_fixed_input_fee_amount(input_amount: int, total_fee_share: int) -> return total_fee_amount -def calculate_fixed_output_fee_amounts(swap_amount: int, total_fee_share: int) -> int: +def calculate_fixed_output_fee_amount(swap_amount: int, total_fee_share: int) -> int: input_amount = (swap_amount * 10_000) // (10_000 - total_fee_share) total_fee_amount = input_amount - swap_amount return total_fee_amount @@ -277,7 +277,7 @@ def calculate_fixed_output_swap( swap_amount = calculate_swap_amount_of_fixed_output_swap( input_supply, output_supply, swap_output_amount ) - total_fee_amount = calculate_fixed_output_fee_amounts( + total_fee_amount = calculate_fixed_output_fee_amount( swap_amount=swap_amount, total_fee_share=total_fee_share ) swap_input_amount = swap_amount + total_fee_amount From a47c5d44f11d7be7dbe3ee01f274c795ef1273a2 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Tue, 25 Oct 2022 14:59:31 +0300 Subject: [PATCH 49/73] fix typo --- README.md | 4 ++-- tinyman/utils.py | 3 +++ tinyman/v1/bootstrap.py | 2 +- tinyman/v1/burn.py | 2 +- tinyman/v1/fees.py | 2 +- tinyman/v1/mint.py | 2 +- tinyman/v1/redeem.py | 2 +- tinyman/v1/swap.py | 2 +- tinyman/v2/bootstrap.py | 2 +- 9 files changed, 12 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7070a5f..f15b6d8 100644 --- a/README.md +++ b/README.md @@ -259,9 +259,9 @@ for i, txn in enumerate(transaction_group.transactions): transaction_group.signed_transactions[i] = kmd.sign_transaction(handle, KMD_WALLET_PASSWORD, txn) ``` -A User account LogicSig can also be used in a similar way or using the `sign_with_logicisg` convenience method: +A User account LogicSig can also be used in a similar way or using the `sign_with_logicsig` convenience method: ```python -transaction_group.sign_with_logicisg(logicsig) +transaction_group.sign_with_logicsig(logicsig) ``` ### Submission diff --git a/tinyman/utils.py b/tinyman/utils.py index 308dd5d..3a6042d 100644 --- a/tinyman/utils.py +++ b/tinyman/utils.py @@ -119,6 +119,9 @@ def id(self): return group_id def sign_with_logicisg(self, logicsig): + return self.sign_with_logicsig(logicsig) + + def sign_with_logicsig(self, logicsig): address = logicsig.address() for i, txn in enumerate(self.transactions): if txn.sender == address: diff --git a/tinyman/v1/bootstrap.py b/tinyman/v1/bootstrap.py index 3080cef..8de163f 100644 --- a/tinyman/v1/bootstrap.py +++ b/tinyman/v1/bootstrap.py @@ -66,5 +66,5 @@ def prepare_bootstrap_transactions( ) ] txn_group = TransactionGroup(txns) - txn_group.sign_with_logicisg(pool_logicsig) + txn_group.sign_with_logicsig(pool_logicsig) return txn_group diff --git a/tinyman/v1/burn.py b/tinyman/v1/burn.py index 53f82e3..4247abe 100644 --- a/tinyman/v1/burn.py +++ b/tinyman/v1/burn.py @@ -66,5 +66,5 @@ def prepare_burn_transactions( ), ] txn_group = TransactionGroup(txns) - txn_group.sign_with_logicisg(pool_logicsig) + txn_group.sign_with_logicsig(pool_logicsig) return txn_group diff --git a/tinyman/v1/fees.py b/tinyman/v1/fees.py index 857379e..334f967 100644 --- a/tinyman/v1/fees.py +++ b/tinyman/v1/fees.py @@ -43,5 +43,5 @@ def prepare_redeem_fees_transactions( ), ] txn_group = TransactionGroup(txns) - txn_group.sign_with_logicisg(pool_logicsig) + txn_group.sign_with_logicsig(pool_logicsig) return txn_group diff --git a/tinyman/v1/mint.py b/tinyman/v1/mint.py index 77a7c52..550efe0 100644 --- a/tinyman/v1/mint.py +++ b/tinyman/v1/mint.py @@ -66,5 +66,5 @@ def prepare_mint_transactions( ), ] txn_group = TransactionGroup(txns) - txn_group.sign_with_logicisg(pool_logicsig) + txn_group.sign_with_logicsig(pool_logicsig) return txn_group diff --git a/tinyman/v1/redeem.py b/tinyman/v1/redeem.py index b069570..af19c38 100644 --- a/tinyman/v1/redeem.py +++ b/tinyman/v1/redeem.py @@ -51,5 +51,5 @@ def prepare_redeem_transactions( ), ] txn_group = TransactionGroup(txns) - txn_group.sign_with_logicisg(pool_logicsig) + txn_group.sign_with_logicsig(pool_logicsig) return txn_group diff --git a/tinyman/v1/swap.py b/tinyman/v1/swap.py index 3e85cc4..33bcd21 100644 --- a/tinyman/v1/swap.py +++ b/tinyman/v1/swap.py @@ -75,5 +75,5 @@ def prepare_swap_transactions( ] txn_group = TransactionGroup(txns) - txn_group.sign_with_logicisg(pool_logicsig) + txn_group.sign_with_logicsig(pool_logicsig) return txn_group diff --git a/tinyman/v2/bootstrap.py b/tinyman/v2/bootstrap.py index 7936983..e8aa153 100644 --- a/tinyman/v2/bootstrap.py +++ b/tinyman/v2/bootstrap.py @@ -49,5 +49,5 @@ def prepare_bootstrap_transactions( txns.append(bootstrap_app_call) txn_group = TransactionGroup(txns) - txn_group.sign_with_logicisg(pool_logicsig) + txn_group.sign_with_logicsig(pool_logicsig) return txn_group From 4176cc98b84e80527125f14f87cb157e589674a3 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 3 Nov 2022 22:50:18 +0300 Subject: [PATCH 50/73] fix setup tools --- setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 28727c9..362b21e 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,10 @@ install_requires=["py-algorand-sdk >= 1.6.0"], packages=setuptools.find_packages(), python_requires=">=3.8", - package_data={"tinyman.v1": ["asc.json"]}, + package_data={ + "tinyman.v1": ["asc.json"], + "tinyman.v2": ["asc.json"], + "tinyman.staking": ["asc.json"], + }, include_package_data=True, ) From c17550a888594a4f87f95fc16db59a706438d54b Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 3 Nov 2022 23:57:30 +0300 Subject: [PATCH 51/73] add __eq__ comparison method for Asset --- tinyman/assets.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tinyman/assets.py b/tinyman/assets.py index 06a3a5f..87fa1c8 100644 --- a/tinyman/assets.py +++ b/tinyman/assets.py @@ -15,6 +15,12 @@ def __call__(self, amount: int) -> "AssetAmount": def __hash__(self) -> int: return self.id + def __repr__(self) -> str: + return f"Asset({self.unit_name} - {self.id})" + + def __eq__(self, other) -> bool: + return self.id == other.id + def fetch(self, algod): if self.id > 0: params = algod.asset_info(self.id)["params"] @@ -29,9 +35,6 @@ def fetch(self, algod): self.decimals = params["decimals"] return self - def __repr__(self) -> str: - return f"Asset({self.unit_name} - {self.id})" - @dataclass class AssetAmount: From a15acbbb71ac0184c47613ce62ed44d5defa4230 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 3 Nov 2022 23:58:17 +0300 Subject: [PATCH 52/73] fix fixed-input swap --- tinyman/v2/pools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tinyman/v2/pools.py b/tinyman/v2/pools.py index 6c50f41..17c9a82 100644 --- a/tinyman/v2/pools.py +++ b/tinyman/v2/pools.py @@ -888,12 +888,12 @@ def fetch_fixed_input_swap_quote( asset_out = self.asset_2 input_supply = self.asset_1_reserves output_supply = self.asset_2_reserves - elif amount_in.asset == self.asset_1: + elif amount_in.asset == self.asset_2: asset_out = self.asset_1 input_supply = self.asset_2_reserves output_supply = self.asset_1_reserves else: - raise False + assert False swap_output_amount, total_fee_amount, price_impact = calculate_fixed_input_swap( input_supply=input_supply, From c494a7c26172cdfac26dae5b5863f31db162c676 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Fri, 4 Nov 2022 15:19:04 +0300 Subject: [PATCH 53/73] handle swap cases with insufficient reserves --- tinyman/v2/exceptions.py | 4 ++++ tinyman/v2/formulas.py | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/tinyman/v2/exceptions.py b/tinyman/v2/exceptions.py index 97f122b..4d29274 100644 --- a/tinyman/v2/exceptions.py +++ b/tinyman/v2/exceptions.py @@ -6,6 +6,10 @@ class AlreadyBootstrapped(Exception): pass +class InsufficientReserve(Exception): + pass + + class PoolHasNoLiquidity(Exception): pass diff --git a/tinyman/v2/formulas.py b/tinyman/v2/formulas.py index 87664c7..865e455 100644 --- a/tinyman/v2/formulas.py +++ b/tinyman/v2/formulas.py @@ -2,6 +2,7 @@ from tinyman.utils import calculate_price_impact from tinyman.v2.constants import LOCKED_POOL_TOKENS +from tinyman.v2.exceptions import InsufficientReserve def calculate_protocol_fee_amount( @@ -245,6 +246,8 @@ def calculate_output_amount_of_fixed_input_swap( def calculate_swap_amount_of_fixed_output_swap( input_supply: int, output_supply: int, output_amount: int ) -> int: + assert output_supply > output_amount + k = input_supply * output_supply swap_amount = int(k / (output_supply - output_amount)) - input_supply swap_amount += 1 @@ -262,6 +265,9 @@ def calculate_fixed_input_swap( input_supply, output_supply, swap_amount ) + if swap_output_amount <= 0: + raise InsufficientReserve() + price_impact = calculate_price_impact( input_supply=input_supply, output_supply=output_supply, @@ -274,6 +280,9 @@ def calculate_fixed_input_swap( def calculate_fixed_output_swap( input_supply: int, output_supply: int, swap_output_amount: int, total_fee_share: int ) -> (int, int, float): + if output_supply <= swap_output_amount: + raise InsufficientReserve() + swap_amount = calculate_swap_amount_of_fixed_output_swap( input_supply, output_supply, swap_output_amount ) From 4a73cbd93f2901cd3e0c04d716e037f1234b3f82 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Tue, 15 Nov 2022 13:23:12 +0300 Subject: [PATCH 54/73] add deprecation warning for sign_with_logicisg --- tinyman/utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tinyman/utils.py b/tinyman/utils.py index 3a6042d..ab27c81 100644 --- a/tinyman/utils.py +++ b/tinyman/utils.py @@ -1,3 +1,4 @@ +import warnings from base64 import b64decode, b64encode from datetime import datetime from algosdk.future.transaction import ( @@ -7,6 +8,8 @@ ) from algosdk.error import AlgodHTTPError +warnings.simplefilter('always', DeprecationWarning) + def encode_value(value, type): if type == "int": @@ -102,8 +105,10 @@ def calculate_price_impact( class TransactionGroup: def __init__(self, transactions): + # Clear previously assigned group ids for txn in transactions: txn.group = None + transactions = assign_group_id(transactions) self.transactions = transactions self.signed_transactions = [None for _ in self.transactions] @@ -119,6 +124,10 @@ def id(self): return group_id def sign_with_logicisg(self, logicsig): + """ + Deprecated because of the typo. Use sign_with_logicsig instead. + """ + warnings.warn('tinyman.utils.TransactionGroup.sign_with_logicisg is deprecated. Use tinyman.utils.TransactionGroup.sign_with_logicsig instead.', DeprecationWarning, stacklevel=2) return self.sign_with_logicsig(logicsig) def sign_with_logicsig(self, logicsig): From 5f3b0d351f5ac36e7d60aec2cfab3ba8823effe5 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Tue, 15 Nov 2022 13:23:54 +0300 Subject: [PATCH 55/73] minor improvements --- .github/workflows/tests.yml | 3 +-- tinyman/v2/contracts.py | 1 + tinyman/v2/pools.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 84562cf..b1384da 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,5 +31,4 @@ jobs: run: black ${{ github.workspace }} --check - name: Run Unit tests - run: | - python -m unittest + run: python -m unittest diff --git a/tinyman/v2/contracts.py b/tinyman/v2/contracts.py index 69614a0..1658ac7 100644 --- a/tinyman/v2/contracts.py +++ b/tinyman/v2/contracts.py @@ -11,6 +11,7 @@ pool_logicsig_def = _contracts["contracts"]["pool_logicsig"]["logic"] +# TODO: Update "asc.json" # validator_app_def = _contracts["contracts"]["validator_app"] diff --git a/tinyman/v2/pools.py b/tinyman/v2/pools.py index 17c9a82..341fc6f 100644 --- a/tinyman/v2/pools.py +++ b/tinyman/v2/pools.py @@ -127,7 +127,7 @@ def __init__( self.asset_2 = asset_a self.exists = None - self.pool_token_asset: Asset = None + self.pool_token_asset: Optional[Asset] = None self.asset_1_reserves = None self.asset_2_reserves = None self.issued_pool_tokens = None From 4478f881fd4663242f4eb9e9185e05b6de3b7855 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Tue, 15 Nov 2022 13:29:22 +0300 Subject: [PATCH 56/73] update exception names --- tinyman/v2/exceptions.py | 10 +++++----- tinyman/v2/pools.py | 28 ++++++++++++++-------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tinyman/v2/exceptions.py b/tinyman/v2/exceptions.py index 4d29274..984e0b2 100644 --- a/tinyman/v2/exceptions.py +++ b/tinyman/v2/exceptions.py @@ -1,18 +1,18 @@ -class BootstrapIsRequired(Exception): +class PoolIsNotBootstrapped(Exception): pass -class AlreadyBootstrapped(Exception): +class PoolAlreadyBootstrapped(Exception): pass -class InsufficientReserve(Exception): +class PoolHasNoLiquidity(Exception): pass -class PoolHasNoLiquidity(Exception): +class PoolAlreadyInitialized(Exception): pass -class PoolAlreadyHasLiquidity(Exception): +class InsufficientReserve(Exception): pass diff --git a/tinyman/v2/pools.py b/tinyman/v2/pools.py index 341fc6f..8ce47c3 100644 --- a/tinyman/v2/pools.py +++ b/tinyman/v2/pools.py @@ -16,10 +16,10 @@ from .constants import MIN_POOL_BALANCE_ASA_ALGO_PAIR, MIN_POOL_BALANCE_ASA_ASA_PAIR from .contracts import get_pool_logicsig from .exceptions import ( - AlreadyBootstrapped, - BootstrapIsRequired, + PoolAlreadyBootstrapped, + PoolIsNotBootstrapped, PoolHasNoLiquidity, - PoolAlreadyHasLiquidity, + PoolAlreadyInitialized, ) from .fees import ( prepare_claim_fees_transactions, @@ -260,7 +260,7 @@ def asset_2_price(self) -> float: def info(self) -> dict: if not self.exists: - raise BootstrapIsRequired() + raise PoolIsNotBootstrapped() pool = { "address": self.address, @@ -338,7 +338,7 @@ def prepare_bootstrap_transactions( self.refresh() if self.exists: - raise AlreadyBootstrapped() + raise PoolAlreadyBootstrapped() if pool_algo_balance is None: pool_account_info = self.client.algod.account_info(self.address) @@ -390,7 +390,7 @@ def fetch_flexible_add_liquidity_quote( self.refresh() if not self.exists: - raise BootstrapIsRequired() + raise PoolIsNotBootstrapped() if not self.issued_pool_tokens: raise PoolHasNoLiquidity() @@ -447,7 +447,7 @@ def fetch_single_asset_add_liquidity_quote( self.refresh() if not self.exists: - raise BootstrapIsRequired() + raise PoolIsNotBootstrapped() if not self.issued_pool_tokens: raise PoolHasNoLiquidity() @@ -530,10 +530,10 @@ def fetch_initial_add_liquidity_quote( self.refresh() if not self.exists: - raise BootstrapIsRequired() + raise PoolIsNotBootstrapped() if self.issued_pool_tokens: - raise PoolAlreadyHasLiquidity() + raise PoolAlreadyInitialized() pool_token_asset_amount = calculate_initial_add_liquidity( asset_1_amount=amount_1.amount, asset_2_amount=amount_2.amount @@ -670,7 +670,7 @@ def fetch_remove_liquidity_quote( refresh: bool = True, ) -> RemoveLiquidityQuote: if not self.exists: - raise BootstrapIsRequired() + raise PoolIsNotBootstrapped() if isinstance(pool_token_asset_in, int): pool_token_asset_in = AssetAmount( @@ -707,7 +707,7 @@ def fetch_single_asset_remove_liquidity_quote( refresh: bool = True, ) -> SingleAssetRemoveLiquidityQuote: if not self.exists: - raise BootstrapIsRequired() + raise PoolIsNotBootstrapped() if isinstance(pool_token_asset_in, int): pool_token_asset_in = AssetAmount( @@ -879,7 +879,7 @@ def fetch_fixed_input_swap_quote( self.refresh() if not self.exists: - raise BootstrapIsRequired() + raise PoolIsNotBootstrapped() if not self.issued_pool_tokens: raise PoolHasNoLiquidity() @@ -920,7 +920,7 @@ def fetch_fixed_output_swap_quote( self.refresh() if not self.exists: - raise BootstrapIsRequired() + raise PoolIsNotBootstrapped() if not self.issued_pool_tokens: raise PoolHasNoLiquidity() @@ -1019,7 +1019,7 @@ def fetch_flash_loan_quote( self.refresh() if not self.exists: - raise BootstrapIsRequired() + raise PoolIsNotBootstrapped() if not self.issued_pool_tokens: raise PoolHasNoLiquidity() From c954e4fe3b6f097456656a00ef7cfe5e06370f28 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Tue, 15 Nov 2022 13:31:52 +0300 Subject: [PATCH 57/73] fix the code format --- tinyman/utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tinyman/utils.py b/tinyman/utils.py index ab27c81..929c788 100644 --- a/tinyman/utils.py +++ b/tinyman/utils.py @@ -8,7 +8,7 @@ ) from algosdk.error import AlgodHTTPError -warnings.simplefilter('always', DeprecationWarning) +warnings.simplefilter("always", DeprecationWarning) def encode_value(value, type): @@ -127,7 +127,11 @@ def sign_with_logicisg(self, logicsig): """ Deprecated because of the typo. Use sign_with_logicsig instead. """ - warnings.warn('tinyman.utils.TransactionGroup.sign_with_logicisg is deprecated. Use tinyman.utils.TransactionGroup.sign_with_logicsig instead.', DeprecationWarning, stacklevel=2) + warnings.warn( + "tinyman.utils.TransactionGroup.sign_with_logicisg is deprecated. Use tinyman.utils.TransactionGroup.sign_with_logicsig instead.", + DeprecationWarning, + stacklevel=2, + ) return self.sign_with_logicsig(logicsig) def sign_with_logicsig(self, logicsig): From 05f48ac2aac31ac1d2e0196681003329183abc51 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 16 Nov 2022 18:47:23 +0300 Subject: [PATCH 58/73] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 362b21e..b63d322 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ description="Tinyman Python SDK", author="Tinyman", author_email="hello@tinyman.org", - version="0.0.6", + version="1.0.0", long_description=long_description, long_description_content_type="text/markdown", license="MIT", From 9cc0546e28ca5dbee31640b6e80946779b9612af Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Tue, 22 Nov 2022 19:52:52 +0300 Subject: [PATCH 59/73] update misleading fetch_pool parameters --- tinyman/v2/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tinyman/v2/client.py b/tinyman/v2/client.py index 7e3eae2..789c262 100644 --- a/tinyman/v2/client.py +++ b/tinyman/v2/client.py @@ -12,10 +12,10 @@ class TinymanV2Client(BaseTinymanClient): - def fetch_pool(self, asset_1, asset_2, fetch=True): + def fetch_pool(self, asset_a, asset_b, fetch=True): from .pools import Pool - return Pool(self, asset_1, asset_2, fetch=fetch) + return Pool(self, asset_a, asset_a, fetch=fetch) class TinymanV2TestnetClient(TinymanV2Client): From d9b5c254a39e0c434f43ae387b4cac5995f51bde Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Tue, 6 Dec 2022 19:41:38 +0300 Subject: [PATCH 60/73] don't generate InternalSwapQuote if the output amount is zero --- tinyman/v2/pools.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/tinyman/v2/pools.py b/tinyman/v2/pools.py index 8ce47c3..11eec60 100644 --- a/tinyman/v2/pools.py +++ b/tinyman/v2/pools.py @@ -411,21 +411,25 @@ def fetch_flexible_add_liquidity_quote( asset_2_amount=amount_2.amount, ) - internal_swap_quote = InternalSwapQuote( - amount_in=AssetAmount( - self.asset_1 if swap_from_asset_1_to_asset_2 else self.asset_2, - swap_in_amount, - ), - amount_out=AssetAmount( - self.asset_2 if swap_from_asset_1_to_asset_2 else self.asset_1, - swap_out_amount, - ), - swap_fees=AssetAmount( - self.asset_1 if swap_from_asset_1_to_asset_2 else self.asset_2, - swap_total_fee_amount, - ), - price_impact=swap_price_impact, - ) + if not swap_out_amount: + # There is no output amount, ignore the integer roundings looks like a swap. + internal_swap_quote = None + else: + internal_swap_quote = InternalSwapQuote( + amount_in=AssetAmount( + self.asset_1 if swap_from_asset_1_to_asset_2 else self.asset_2, + swap_in_amount, + ), + amount_out=AssetAmount( + self.asset_2 if swap_from_asset_1_to_asset_2 else self.asset_1, + swap_out_amount, + ), + swap_fees=AssetAmount( + self.asset_1 if swap_from_asset_1_to_asset_2 else self.asset_2, + swap_total_fee_amount, + ), + price_impact=swap_price_impact, + ) quote = FlexibleAddLiquidityQuote( amounts_in={ From b8252ded905db269c73896c7d5d3b76fe440af4e Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Tue, 6 Dec 2022 19:41:50 +0300 Subject: [PATCH 61/73] update misleading asset parameters --- tinyman/v2/contracts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tinyman/v2/contracts.py b/tinyman/v2/contracts.py index 1658ac7..09422aa 100644 --- a/tinyman/v2/contracts.py +++ b/tinyman/v2/contracts.py @@ -16,9 +16,9 @@ def get_pool_logicsig( - validator_app_id: int, asset_1_id: int, asset_2_id: int + validator_app_id: int, asset_a_id: int, asset_b_id: int ) -> LogicSigAccount: - assets = [asset_1_id, asset_2_id] + assets = [asset_a_id, asset_b_id] asset_1_id = max(assets) asset_2_id = min(assets) From 713b52904fdb43f4ec937c3a35a4793bcc0279db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Abac=C4=B1?= Date: Tue, 13 Dec 2022 12:36:45 +0300 Subject: [PATCH 62/73] Fix typo --- tinyman/v2/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tinyman/v2/client.py b/tinyman/v2/client.py index 789c262..94f080f 100644 --- a/tinyman/v2/client.py +++ b/tinyman/v2/client.py @@ -15,7 +15,7 @@ class TinymanV2Client(BaseTinymanClient): def fetch_pool(self, asset_a, asset_b, fetch=True): from .pools import Pool - return Pool(self, asset_a, asset_a, fetch=fetch) + return Pool(self, asset_a, asset_b, fetch=fetch) class TinymanV2TestnetClient(TinymanV2Client): From d441b3324e0384fd72fb80980d1f7982b33719b7 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Wed, 14 Dec 2022 15:55:28 +0300 Subject: [PATCH 63/73] update testnet v2 app id --- tinyman/v2/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tinyman/v2/constants.py b/tinyman/v2/constants.py index cd2f456..d5d5cef 100644 --- a/tinyman/v2/constants.py +++ b/tinyman/v2/constants.py @@ -23,7 +23,7 @@ ADD_LIQUIDITY_SINGLE_MODE_APP_ARGUMENT = b"single" -TESTNET_VALIDATOR_APP_ID_V2 = 113134165 +TESTNET_VALIDATOR_APP_ID_V2 = 148607000 MAINNET_VALIDATOR_APP_ID_V2 = None TESTNET_VALIDATOR_APP_ID = TESTNET_VALIDATOR_APP_ID_V2 From 2e547035e1447802fc51499dd5aceb6a011d74ee Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 15 Dec 2022 15:11:36 +0300 Subject: [PATCH 64/73] update PYPI explanation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f15b6d8..309012b 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The SDK has been updated for Tinyman V1.1. ## Installation -tinyman-py-sdk is not yet released on PYPI. It can be installed directly from this repository with pip: +tinyman-py-sdk is not released on PYPI. It can be installed directly from this repository with pip: `pip install git+https://github.com/tinymanorg/tinyman-py-sdk.git` From 9f49941457f6adcb2d6a09ad6d0177c6e0191184 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 15 Dec 2022 18:04:47 +0300 Subject: [PATCH 65/73] don't allow opting into ALGO --- tinyman/optin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tinyman/optin.py b/tinyman/optin.py index efd1883..30c409b 100644 --- a/tinyman/optin.py +++ b/tinyman/optin.py @@ -14,6 +14,8 @@ def prepare_app_optin_transactions(validator_app_id, sender, suggested_params): def prepare_asset_optin_transactions(asset_id, sender, suggested_params): + assert asset_id != 0, "Cannot opt into ALGO" + txn = AssetOptInTxn( sender=sender, sp=suggested_params, From 70a2cb80bfafd600847514f98c0a4fb7e8e5e00b Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 15 Dec 2022 18:44:37 +0300 Subject: [PATCH 66/73] remove asc.json files --- setup.py | 2 -- tinyman/staking/__init__.py | 58 +++++++++++++++++------------------- tinyman/staking/asc.json | 32 -------------------- tinyman/staking/contracts.py | 8 ----- tinyman/v2/asc.json | 12 -------- tinyman/v2/constants.py | 4 ++- tinyman/v2/contracts.py | 14 ++------- 7 files changed, 32 insertions(+), 98 deletions(-) delete mode 100644 tinyman/staking/asc.json delete mode 100644 tinyman/staking/contracts.py delete mode 100644 tinyman/v2/asc.json diff --git a/setup.py b/setup.py index b63d322..e61b20a 100644 --- a/setup.py +++ b/setup.py @@ -21,8 +21,6 @@ python_requires=">=3.8", package_data={ "tinyman.v1": ["asc.json"], - "tinyman.v2": ["asc.json"], - "tinyman.staking": ["asc.json"], }, include_package_data=True, ) diff --git a/tinyman/staking/__init__.py b/tinyman/staking/__init__.py index 9d09cd9..e378e0b 100644 --- a/tinyman/staking/__init__.py +++ b/tinyman/staking/__init__.py @@ -9,12 +9,8 @@ from algosdk.encoding import is_valid_address from algosdk.future.transaction import ( ApplicationClearStateTxn, - ApplicationCreateTxn, ApplicationOptInTxn, - OnComplete, PaymentTxn, - StateSchema, - ApplicationUpdateTxn, ApplicationNoOpTxn, AssetTransferTxn, ) @@ -30,33 +26,33 @@ from tinyman.staking.constants import DATE_FORMAT -def prepare_create_transaction(args, sender, suggested_params): - from .contracts import staking_app_def - - txn = ApplicationCreateTxn( - sender=sender, - sp=suggested_params, - on_complete=OnComplete.NoOpOC.real, - approval_program=b64decode(staking_app_def["approval_program"]["bytecode"]), - clear_program=b64decode(staking_app_def["clear_program"]["bytecode"]), - global_schema=StateSchema(**staking_app_def["global_state_schema"]), - local_schema=StateSchema(**staking_app_def["local_state_schema"]), - app_args=args, - ) - return TransactionGroup([txn]) - - -def prepare_update_transaction(app_id: int, sender, suggested_params): - from .contracts import staking_app_def - - txn = ApplicationUpdateTxn( - index=app_id, - sender=sender, - sp=suggested_params, - approval_program=b64decode(staking_app_def["approval_program"]["bytecode"]), - clear_program=b64decode(staking_app_def["clear_program"]["bytecode"]), - ) - return TransactionGroup([txn]) +# def prepare_create_transaction(args, sender, suggested_params): +# from .contracts import staking_app_def +# +# txn = ApplicationCreateTxn( +# sender=sender, +# sp=suggested_params, +# on_complete=OnComplete.NoOpOC.real, +# approval_program=b64decode(staking_app_def["approval_program"]["bytecode"]), +# clear_program=b64decode(staking_app_def["clear_program"]["bytecode"]), +# global_schema=StateSchema(**staking_app_def["global_state_schema"]), +# local_schema=StateSchema(**staking_app_def["local_state_schema"]), +# app_args=args, +# ) +# return TransactionGroup([txn]) +# +# +# def prepare_update_transaction(app_id: int, sender, suggested_params): +# from .contracts import staking_app_def +# +# txn = ApplicationUpdateTxn( +# index=app_id, +# sender=sender, +# sp=suggested_params, +# approval_program=b64decode(staking_app_def["approval_program"]["bytecode"]), +# clear_program=b64decode(staking_app_def["clear_program"]["bytecode"]), +# ) +# return TransactionGroup([txn]) def prepare_commit_transaction( diff --git a/tinyman/staking/asc.json b/tinyman/staking/asc.json deleted file mode 100644 index 15f49aa..0000000 --- a/tinyman/staking/asc.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "repo": "https://github.com/tinymanorg/tinyman-staking", - "ref": "main", - "contracts": { - "staking_app": { - "type": "app", - "approval_program": { - "bytecode": "BSADAAEIJgcIZW5kX3RpbWUGYXNzZXRzBG1pbnMMdmVyaWZpY2F0aW9uAmlkD3Jld2FyZF9hc3NldF9pZA9wcm9ncmFtX2NvdW50ZXIxGSISQAAvMRkjEkAAGTEZgQISQAICMRmBBBJAAfwxGYEFEkAB+gA2GgCABXNldHVwEkABgwA2GgCABmNyZWF0ZRJAAGg2GgCABmNvbW1pdBJAAFs2GgCABnVwZGF0ZRJAAUU2GgCADnVwZGF0ZV9yZXdhcmRzEkAA6DYaAIANdXBkYXRlX2Fzc2V0cxJAAQE2GgCAC2VuZF9wcm9ncmFtEkAA+zYaACsSQAD+ACNDIycEYjYaAhcSRCMoYjIHDUQjKWIiSiQLWzYwABJAAAojCEmBDgxEQv/rTEg1ASMqYjQBJAtbNQI2GgEXQQAXNhoBFzQCD0QjJwViSUEABiJMcABESEgiNjAAcABESRaACGJhbGFuY2UgTFCwNhoBFw9EgBN0aW55bWFuU3Rha2luZy92MTpiMQVRABMSMQVXEwA1ATQBIls2GgIXEjQBJFs2MAASRDQBgRBbNhoBFxJEI0MigAJyMTYaAWYigAJyMjYaAmYigAJyMzYaA2YigAJyNDYaBGYigAJyNTYaBWYjQyIpNhoBZiIqNhoCZiNDIig2GgEXZiNDI0MyCTEAEkQiKzYaAWYjQycGZCMINQEnBjQBZyInBDQBZiKAA3VybDYaAWYiJwU2GgIXZiKADXJld2FyZF9wZXJpb2Q2GgMXZiKACnN0YXJ0X3RpbWU2GgQXZiIoNhoFF2YiKTYaBmYiKjYaB2YjQyNDMgkxABJDMgkxABJDAA==", - "address": "XHG22P7FZNM4FIUYYK3IV6BHWE7HOV5PEV5LHA64GFKB44PQBFGDNTE32E", - "size": 628, - "variables": [], - "source": "https://github.com/tinymanorg/tinyman-staking/tree/main/contracts/staking.teal" - }, - "clear_program": { - "bytecode": "BIEB", - "address": "P7GEWDXXW5IONRW6XRIRVPJCT2XXEQGOBGG65VJPBUOYZEJCBZWTPHS3VQ", - "size": 3, - "variables": [], - "source": "https://github.com/tinymanorg/tinyman-staking/tree/main/contracts/clear_state.teal" - }, - "global_state_schema": { - "num_uints": 2, - "num_byte_slices": 2 - }, - "local_state_schema": { - "num_uints": 5, - "num_byte_slices": 11 - }, - "name": "staking_app" - } - } -} \ No newline at end of file diff --git a/tinyman/staking/contracts.py b/tinyman/staking/contracts.py deleted file mode 100644 index ee9c7c3..0000000 --- a/tinyman/staking/contracts.py +++ /dev/null @@ -1,8 +0,0 @@ -import importlib.resources -import json - -import tinyman.staking - -_contracts = json.loads(importlib.resources.read_text(tinyman.v1.staking, "asc.json")) - -staking_app_def = _contracts["contracts"]["staking_app"] diff --git a/tinyman/v2/asc.json b/tinyman/v2/asc.json deleted file mode 100644 index 41bf73f..0000000 --- a/tinyman/v2/asc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "repo": "https://github.com/tinymanorg/tinyman-contracts-v2", - "contracts": { - "pool_logicsig": { - "type": "logicsig", - "logic": { - "bytecode": "BoAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgQBbNQA0ADEYEkQxGYEBEkSBAUM=" - }, - "name": "pool_logicsig" - } - } -} \ No newline at end of file diff --git a/tinyman/v2/constants.py b/tinyman/v2/constants.py index d5d5cef..4b423e1 100644 --- a/tinyman/v2/constants.py +++ b/tinyman/v2/constants.py @@ -1,5 +1,7 @@ from algosdk.logic import get_application_address +POOL_LOGICSIG_TEMPLATE = "BoAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgQBbNQA0ADEYEkQxGYEBEkSBAUM=" + BOOTSTRAP_APP_ARGUMENT = b"bootstrap" ADD_LIQUIDITY_APP_ARGUMENT = b"add_liquidity" ADD_INITIAL_LIQUIDITY_APP_ARGUMENT = b"add_initial_liquidity" @@ -30,7 +32,7 @@ MAINNET_VALIDATOR_APP_ID = None TESTNET_VALIDATOR_APP_ADDRESS = get_application_address(TESTNET_VALIDATOR_APP_ID) -# MAINNET_VALIDATOR_APP__ADDRESS = get_application_address(MAINNET_VALIDATOR_APP_ID) +# MAINNET_VALIDATOR_APP_ADDRESS = get_application_address(MAINNET_VALIDATOR_APP_ID) LOCKED_POOL_TOKENS = 1000 ASSET_MIN_TOTAL = 1000000 diff --git a/tinyman/v2/contracts.py b/tinyman/v2/contracts.py index 09422aa..7a2d433 100644 --- a/tinyman/v2/contracts.py +++ b/tinyman/v2/contracts.py @@ -1,18 +1,8 @@ -import json -import importlib.resources from base64 import b64decode from algosdk.future.transaction import LogicSigAccount -import tinyman.v1 - - -_contracts = json.loads(importlib.resources.read_text(tinyman.v2, "asc.json")) - -pool_logicsig_def = _contracts["contracts"]["pool_logicsig"]["logic"] - -# TODO: Update "asc.json" -# validator_app_def = _contracts["contracts"]["validator_app"] +from tinyman.v2.constants import POOL_LOGICSIG_TEMPLATE def get_pool_logicsig( @@ -22,7 +12,7 @@ def get_pool_logicsig( asset_1_id = max(assets) asset_2_id = min(assets) - program = bytearray(b64decode(pool_logicsig_def["bytecode"])) + program = bytearray(b64decode(POOL_LOGICSIG_TEMPLATE)) program[3:11] = validator_app_id.to_bytes(8, "big") program[11:19] = asset_1_id.to_bytes(8, "big") program[19:27] = asset_2_id.to_bytes(8, "big") From 3949033a3ee57c10a5bf866c4f7654d0dfa41e52 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 15 Dec 2022 19:28:02 +0300 Subject: [PATCH 67/73] minor fixes --- examples/staking/commitments.py | 3 ++- tinyman/staking/__init__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/staking/commitments.py b/examples/staking/commitments.py index ebb0f53..d914e5e 100644 --- a/examples/staking/commitments.py +++ b/examples/staking/commitments.py @@ -1,7 +1,8 @@ import requests from tinyman.staking import parse_commit_transaction +from tinyman.staking.constants import TESTNET_STAKING_APP_ID -app_id = 51948952 +app_id = TESTNET_STAKING_APP_ID result = requests.get( f"https://indexer.testnet.algoexplorerapi.io/v2/transactions?application-id={app_id}&latest=50" ).json() diff --git a/tinyman/staking/__init__.py b/tinyman/staking/__init__.py index e378e0b..650b50a 100644 --- a/tinyman/staking/__init__.py +++ b/tinyman/staking/__init__.py @@ -312,7 +312,7 @@ def prepare_payment_transaction( ): note = generate_note_from_metadata(metadata) # Compose a lease key from the distribution key (date, pool_address) and staker_address - # This is to prevent accidently submitting multiple payments for the same staker for the same cycles + # This is to prevent accidentally submitting multiple payments for the same staker for the same cycles # Note: the lease is only ensured unique between first_valid & last_valid lease_data = json.dumps( [metadata["rewards"]["distribution"], staker_address] From 898469ca41aa8c7ccb771cc5fdfd65d62581d202 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 15 Dec 2022 19:28:31 +0300 Subject: [PATCH 68/73] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e61b20a..8758e48 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ description="Tinyman Python SDK", author="Tinyman", author_email="hello@tinyman.org", - version="1.0.0", + version="2.0.0", long_description=long_description, long_description_content_type="text/markdown", license="MIT", From da0d141188388361497fb009c1fea21351ed8f6b Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 15 Dec 2022 19:29:03 +0300 Subject: [PATCH 69/73] add changelog --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e0f4c01 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Change Log + +## 2.0.0 + +### Added + +* Added Tinyman V2 support (`tinyman.v2`). +* Added Staking support (`tinyman.staking`). + - It allows creating commitment transaction by `prepare_commit_transaction` and tracking commitments by `parse_commit_transaction`. +* Added `calculate_price_impact` function to `tinyman.utils`. +* Improved `TransactionGroup` class. + - Added `+` operator support for composability, it allows creating a new transaction group (`txn_group_1 + txn_group_2`). + - Added `id` property, it returns the transactions group id. + - Added `TransactionGroup.sign_with_logicsig` function and deprecated `TransactionGroup.sign_with_logicisg` because of the typo. + +### Changed + +* `get_program` (V1) is moved from `tinyman.utils` to `tinyman.v1.contracts`. +* `get_state_from_account_info` (V1) is moved from `tinyman.utils` to `tinyman.v1.utils`. + +### Removed + +* Deprecated `wait_for_confirmation` function is removed. `wait_for_confirmation` is added to [Algorand SDK](https://github.com/algorand/py-algorand-sdk). +* Drop Python 3.7 support. + From 9b2365330166a02ba241cabe5f5134a0e96733e4 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 15 Dec 2022 19:32:09 +0300 Subject: [PATCH 70/73] fix formatting errors --- tinyman/v2/constants.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tinyman/v2/constants.py b/tinyman/v2/constants.py index 4b423e1..3d84002 100644 --- a/tinyman/v2/constants.py +++ b/tinyman/v2/constants.py @@ -1,6 +1,8 @@ from algosdk.logic import get_application_address -POOL_LOGICSIG_TEMPLATE = "BoAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgQBbNQA0ADEYEkQxGYEBEkSBAUM=" +POOL_LOGICSIG_TEMPLATE = ( + "BoAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgQBbNQA0ADEYEkQxGYEBEkSBAUM=" +) BOOTSTRAP_APP_ARGUMENT = b"bootstrap" ADD_LIQUIDITY_APP_ARGUMENT = b"add_liquidity" From 7f5e30392f753cc10c7acdcbc0c54ddf46ca5a21 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 15 Dec 2022 19:47:11 +0300 Subject: [PATCH 71/73] remove redundant functions --- tinyman/staking/__init__.py | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/tinyman/staking/__init__.py b/tinyman/staking/__init__.py index 650b50a..11191c3 100644 --- a/tinyman/staking/__init__.py +++ b/tinyman/staking/__init__.py @@ -26,35 +26,6 @@ from tinyman.staking.constants import DATE_FORMAT -# def prepare_create_transaction(args, sender, suggested_params): -# from .contracts import staking_app_def -# -# txn = ApplicationCreateTxn( -# sender=sender, -# sp=suggested_params, -# on_complete=OnComplete.NoOpOC.real, -# approval_program=b64decode(staking_app_def["approval_program"]["bytecode"]), -# clear_program=b64decode(staking_app_def["clear_program"]["bytecode"]), -# global_schema=StateSchema(**staking_app_def["global_state_schema"]), -# local_schema=StateSchema(**staking_app_def["local_state_schema"]), -# app_args=args, -# ) -# return TransactionGroup([txn]) -# -# -# def prepare_update_transaction(app_id: int, sender, suggested_params): -# from .contracts import staking_app_def -# -# txn = ApplicationUpdateTxn( -# index=app_id, -# sender=sender, -# sp=suggested_params, -# approval_program=b64decode(staking_app_def["approval_program"]["bytecode"]), -# clear_program=b64decode(staking_app_def["clear_program"]["bytecode"]), -# ) -# return TransactionGroup([txn]) - - def prepare_commit_transaction( app_id: int, program_id: int, From e97601cfab446b76665ab4f18d9dcc12f7ea5e44 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 15 Dec 2022 19:49:26 +0300 Subject: [PATCH 72/73] fix the exception name --- tinyman/v2/exceptions.py | 2 +- tinyman/v2/formulas.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tinyman/v2/exceptions.py b/tinyman/v2/exceptions.py index 984e0b2..8825ab4 100644 --- a/tinyman/v2/exceptions.py +++ b/tinyman/v2/exceptions.py @@ -14,5 +14,5 @@ class PoolAlreadyInitialized(Exception): pass -class InsufficientReserve(Exception): +class InsufficientReserves(Exception): pass diff --git a/tinyman/v2/formulas.py b/tinyman/v2/formulas.py index 865e455..258c9df 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 InsufficientReserve +from tinyman.v2.exceptions import InsufficientReserves def calculate_protocol_fee_amount( @@ -266,7 +266,7 @@ def calculate_fixed_input_swap( ) if swap_output_amount <= 0: - raise InsufficientReserve() + raise InsufficientReserves() price_impact = calculate_price_impact( input_supply=input_supply, @@ -281,7 +281,7 @@ def calculate_fixed_output_swap( input_supply: int, output_supply: int, swap_output_amount: int, total_fee_share: int ) -> (int, int, float): if output_supply <= swap_output_amount: - raise InsufficientReserve() + raise InsufficientReserves() swap_amount = calculate_swap_amount_of_fixed_output_swap( input_supply, output_supply, swap_output_amount From 709d66113567a02ac1cfcc167ba15e01c1471698 Mon Sep 17 00:00:00 2001 From: Goksel Coban Date: Thu, 15 Dec 2022 20:26:32 +0300 Subject: [PATCH 73/73] prefer submit function of the client --- examples/v2/tutorial/03_bootstrap_pool.py | 2 +- examples/v2/tutorial/04_add_initial_liquidity.py | 2 +- examples/v2/tutorial/05_add_flexible_liquidity.py | 2 +- examples/v2/tutorial/06_add_single_asset_liquidity.py | 2 +- examples/v2/tutorial/07_remove_liquidity.py | 2 +- .../v2/tutorial/08_single_asset_remove_liquidity.py | 2 +- examples/v2/tutorial/09_fixed_input_swap.py | 2 +- examples/v2/tutorial/10_fixed_output_swap.py | 2 +- examples/v2/tutorial/11_flash_loan_1_single_asset.py | 2 +- .../v2/tutorial/12_flash_loan_2_multiple_assets.py | 2 +- examples/v2/tutorial/13_flash_swap_1.py | 2 +- examples/v2/tutorial/14_flash_swap_2.py | 2 +- tinyman/client.py | 10 ++++++++-- tinyman/utils.py | 1 + 14 files changed, 21 insertions(+), 14 deletions(-) diff --git a/examples/v2/tutorial/03_bootstrap_pool.py b/examples/v2/tutorial/03_bootstrap_pool.py index c0bb7bc..d60307e 100644 --- a/examples/v2/tutorial/03_bootstrap_pool.py +++ b/examples/v2/tutorial/03_bootstrap_pool.py @@ -29,7 +29,7 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txn_info = txn_group.submit(algod, wait=True) +txn_info = client.submit(txn_group, wait=True) print("Transaction Info") pprint(txn_info) diff --git a/examples/v2/tutorial/04_add_initial_liquidity.py b/examples/v2/tutorial/04_add_initial_liquidity.py index 42f1d13..73546ec 100644 --- a/examples/v2/tutorial/04_add_initial_liquidity.py +++ b/examples/v2/tutorial/04_add_initial_liquidity.py @@ -40,7 +40,7 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txn_info = txn_group.submit(algod, wait=True) +txn_info = client.submit(txn_group, wait=True) print("Transaction Info") pprint(txn_info) diff --git a/examples/v2/tutorial/05_add_flexible_liquidity.py b/examples/v2/tutorial/05_add_flexible_liquidity.py index 5096e11..d862d88 100644 --- a/examples/v2/tutorial/05_add_flexible_liquidity.py +++ b/examples/v2/tutorial/05_add_flexible_liquidity.py @@ -49,7 +49,7 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txn_info = txn_group.submit(algod, wait=True) +txn_info = client.submit(txn_group, wait=True) print("Transaction Info") pprint(txn_info) diff --git a/examples/v2/tutorial/06_add_single_asset_liquidity.py b/examples/v2/tutorial/06_add_single_asset_liquidity.py index c83753a..68bd0b0 100644 --- a/examples/v2/tutorial/06_add_single_asset_liquidity.py +++ b/examples/v2/tutorial/06_add_single_asset_liquidity.py @@ -48,7 +48,7 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txn_info = txn_group.submit(algod, wait=True) +txn_info = client.submit(txn_group, wait=True) print("Transaction Info") pprint(txn_info) diff --git a/examples/v2/tutorial/07_remove_liquidity.py b/examples/v2/tutorial/07_remove_liquidity.py index 974838d..b93e544 100644 --- a/examples/v2/tutorial/07_remove_liquidity.py +++ b/examples/v2/tutorial/07_remove_liquidity.py @@ -34,7 +34,7 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txn_info = txn_group.submit(algod, wait=True) +txn_info = client.submit(txn_group, wait=True) print("Transaction Info") pprint(txn_info) diff --git a/examples/v2/tutorial/08_single_asset_remove_liquidity.py b/examples/v2/tutorial/08_single_asset_remove_liquidity.py index 7ffe1a8..7b2b6bc 100644 --- a/examples/v2/tutorial/08_single_asset_remove_liquidity.py +++ b/examples/v2/tutorial/08_single_asset_remove_liquidity.py @@ -35,7 +35,7 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txn_info = txn_group.submit(algod, wait=True) +txn_info = client.submit(txn_group, wait=True) print("Transaction Info") pprint(txn_info) diff --git a/examples/v2/tutorial/09_fixed_input_swap.py b/examples/v2/tutorial/09_fixed_input_swap.py index 5538944..822c7c8 100644 --- a/examples/v2/tutorial/09_fixed_input_swap.py +++ b/examples/v2/tutorial/09_fixed_input_swap.py @@ -36,7 +36,7 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txn_info = txn_group.submit(algod, wait=True) +txn_info = client.submit(txn_group, wait=True) print("Transaction Info") pprint(txn_info) diff --git a/examples/v2/tutorial/10_fixed_output_swap.py b/examples/v2/tutorial/10_fixed_output_swap.py index 57c4519..5694b06 100644 --- a/examples/v2/tutorial/10_fixed_output_swap.py +++ b/examples/v2/tutorial/10_fixed_output_swap.py @@ -36,7 +36,7 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txn_info = txn_group.submit(algod, wait=True) +txn_info = client.submit(txn_group, wait=True) print("Transaction Info") pprint(txn_info) diff --git a/examples/v2/tutorial/11_flash_loan_1_single_asset.py b/examples/v2/tutorial/11_flash_loan_1_single_asset.py index fd57254..87b556f 100644 --- a/examples/v2/tutorial/11_flash_loan_1_single_asset.py +++ b/examples/v2/tutorial/11_flash_loan_1_single_asset.py @@ -56,7 +56,7 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txn_info = txn_group.submit(algod, wait=True) +txn_info = client.submit(txn_group, wait=True) print("Transaction Info") pprint(txn_info) diff --git a/examples/v2/tutorial/12_flash_loan_2_multiple_assets.py b/examples/v2/tutorial/12_flash_loan_2_multiple_assets.py index 4de2ebe..7189f00 100644 --- a/examples/v2/tutorial/12_flash_loan_2_multiple_assets.py +++ b/examples/v2/tutorial/12_flash_loan_2_multiple_assets.py @@ -65,7 +65,7 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txn_info = txn_group.submit(algod, wait=True) +txn_info = client.submit(txn_group, wait=True) print("Transaction Info") pprint(txn_info) diff --git a/examples/v2/tutorial/13_flash_swap_1.py b/examples/v2/tutorial/13_flash_swap_1.py index 9d97aaf..dcc551a 100644 --- a/examples/v2/tutorial/13_flash_swap_1.py +++ b/examples/v2/tutorial/13_flash_swap_1.py @@ -101,7 +101,7 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txn_info = txn_group.submit(algod, wait=True) +txn_info = client.submit(txn_group, wait=True) print("Transaction Info") pprint(txn_info) diff --git a/examples/v2/tutorial/14_flash_swap_2.py b/examples/v2/tutorial/14_flash_swap_2.py index 60a47b7..f7e3809 100644 --- a/examples/v2/tutorial/14_flash_swap_2.py +++ b/examples/v2/tutorial/14_flash_swap_2.py @@ -102,7 +102,7 @@ txn_group.sign_with_private_key(account["address"], account["private_key"]) # Submit transactions to the network and wait for confirmation -txn_info = txn_group.submit(algod, wait=True) +txn_info = client.submit(txn_group, wait=True) print("Transaction Info") pprint(txn_info) diff --git a/tinyman/client.py b/tinyman/client.py index 0af997c..296f51d 100644 --- a/tinyman/client.py +++ b/tinyman/client.py @@ -1,7 +1,9 @@ from typing import Optional -from algosdk.v2client.algod import AlgodClient +from algosdk.error import AlgodHTTPError from algosdk.future.transaction import wait_for_confirmation +from algosdk.v2client.algod import AlgodClient + from tinyman.assets import Asset from tinyman.optin import prepare_asset_optin_transactions @@ -31,7 +33,11 @@ def fetch_asset(self, asset_id): return self.assets_cache[asset_id] def submit(self, transaction_group, wait=False): - txid = self.algod.send_transactions(transaction_group.signed_transactions) + try: + txid = self.algod.send_transactions(transaction_group.signed_transactions) + except AlgodHTTPError as e: + raise Exception(str(e)) + if wait: txn_info = wait_for_confirmation(self.algod, txid) txn_info["txid"] = txid diff --git a/tinyman/utils.py b/tinyman/utils.py index 929c788..9c868fc 100644 --- a/tinyman/utils.py +++ b/tinyman/utils.py @@ -150,6 +150,7 @@ def submit(self, algod, wait=False): txid = algod.send_transactions(self.signed_transactions) except AlgodHTTPError as e: raise Exception(str(e)) + if wait: txn_info = wait_for_confirmation(algod, txid) txn_info["txid"] = txid