├── .gitignore ├── LICENSE ├── README.md ├── SECURITY.md ├── contracts ├── amm_approval.tl ├── amm_clear_state.tl ├── build │ ├── amm_approval.teal │ ├── amm_approval.teal.tok │ ├── amm_clear_state.teal │ ├── amm_clear_state.teal.tok │ ├── pool_template.teal │ └── pool_template.teal.tok └── pool_template.tl ├── docs └── Tinyman AMM V2 Protocol Specification.pdf ├── requirements.txt └── tests ├── __init__.py ├── constants.py ├── core.py ├── dummy_program.tl ├── price_oracle_reader.tl ├── proxy_approval_program.tl ├── tests_add_liquidity.py ├── tests_bootstrap.py ├── tests_claim_extra.py ├── tests_claim_fees.py ├── tests_create_app.py ├── tests_flash_loan.py ├── tests_flash_swap.py ├── tests_price_oracle.py ├── tests_remove_liquidity.py ├── tests_set_fee.py ├── tests_set_fee_collector.py ├── tests_set_fee_manager.py ├── tests_set_fee_setter.py ├── tests_swap.py ├── tests_swap_groupped.py ├── tests_swap_proxy.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | !contracts/build 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | .DS_Store 133 | .idea 134 | 135 | # Ignore some tealish and ascjson related files 136 | *.map.json 137 | *.min.teal 138 | *.teal.tok 139 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Business Source License 1.1 2 | 3 | License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. 4 | "Business Source License" is a trademark of MariaDB Corporation Ab. 5 | 6 | ----------------------------------------------------------------------------- 7 | 8 | Parameters 9 | 10 | Licensor: TM Distributed Development Ltd 11 | 12 | Licensed Work: Tinyman AMM V2 Contracts 13 | The Licensed Work is (c) 2022 TM Distributed Development Ltd 14 | Additional Use Grant: None 15 | 16 | Change Date: 2026-01-01 17 | 18 | Change License: GNU General Public License v2.0 or later 19 | 20 | ----------------------------------------------------------------------------- 21 | 22 | Terms 23 | 24 | The Licensor hereby grants you the right to copy, modify, create derivative 25 | works, redistribute, and make non-production use of the Licensed Work. The 26 | Licensor may make an Additional Use Grant, above, permitting limited 27 | production use. 28 | 29 | Effective on the Change Date, or the fourth anniversary of the first publicly 30 | available distribution of a specific version of the Licensed Work under this 31 | License, whichever comes first, the Licensor hereby grants you rights under 32 | the terms of the Change License, and the rights granted in the paragraph 33 | above terminate. 34 | 35 | If your use of the Licensed Work does not comply with the requirements 36 | currently in effect as described in this License, you must purchase a 37 | commercial license from the Licensor, its affiliated entities, or authorized 38 | resellers, or you must refrain from using the Licensed Work. 39 | 40 | All copies of the original and modified Licensed Work, and derivative works 41 | of the Licensed Work, are subject to this License. This License applies 42 | separately for each version of the Licensed Work and the Change Date may vary 43 | for each version of the Licensed Work released by Licensor. 44 | 45 | You must conspicuously display this License on each original or modified copy 46 | of the Licensed Work. If you receive the Licensed Work in original or 47 | modified form from a third party, the terms and conditions set forth in this 48 | License apply to your use of that work. 49 | 50 | Any use of the Licensed Work in violation of this License will automatically 51 | terminate your rights under this License for the current and all other 52 | versions of the Licensed Work. 53 | 54 | This License does not grant you any right in any trademark or logo of 55 | Licensor or its affiliates (provided that you may use a trademark or logo of 56 | Licensor as expressly required by this License). 57 | 58 | TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON 59 | AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, 60 | EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF 61 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND 62 | TITLE. 63 | 64 | MariaDB hereby grants you permission to use this License’s text to license 65 | your works, and to refer to it using the trademark "Business Source License", 66 | as long as you comply with the Covenants of Licensor below. 67 | 68 | ----------------------------------------------------------------------------- 69 | 70 | Covenants of Licensor 71 | 72 | In consideration of the right to use this License’s text and the "Business 73 | Source License" name and trademark, Licensor covenants to MariaDB, and to all 74 | other recipients of the licensed work to be provided by Licensor: 75 | 76 | 1. To specify as the Change License the GPL Version 2.0 or any later version, 77 | or a license that is compatible with GPL Version 2.0 or a later version, 78 | where "compatible" means that software provided under the Change License can 79 | be included in a program with software provided under GPL Version 2.0 or a 80 | later version. Licensor may specify additional Change Licenses without 81 | limitation. 82 | 83 | 2. To either: (a) specify an additional grant of rights to use that does not 84 | impose any additional restriction on the right granted in this License, as 85 | the Additional Use Grant; or (b) insert the text "None". 86 | 87 | 3. To specify a Change Date. 88 | 89 | 4. Not to modify this License in any other way. 90 | 91 | ----------------------------------------------------------------------------- 92 | 93 | Notice 94 | 95 | The Business Source License (this document, or the "License") is not an Open 96 | Source license. However, the Licensed Work will eventually be made available 97 | under an Open Source License, as stated in this License. 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tinyman-amm-contracts-v2 2 | Tinyman AMM Contracts V2 3 | 4 | Tinyman is an automated market maker (AMM) implementation on Algorand. 5 | 6 | ### Docs 7 | 8 | The protocol is described in detail in the following document: 9 | [Tinyman AMM V2 Protocol Specification](docs/Tinyman%20AMM%20V2%20Protocol%20Specification.pdf) 10 | 11 | 12 | ### Contracts 13 | The contracts are written in [Tealish](https://github.com/tinymanorg/tealish). 14 | The specific version of Tealish is https://github.com/tinymanorg/tealish/tree/0cec751154b0083c2cb79da43b40aa26b367ecc4. 15 | 16 | The annotated TEAL outputs and compiled bytecode are available in the [build](contracts/build/) folder. 17 | 18 | The Tealish source can be compiled as follows: 19 | ``` 20 | tealish contracts/ 21 | ``` 22 | The `.teal` files will be output to the `contracts/build` directory. 23 | 24 | A VS Code extension for syntax highlighting of Tealish & TEAL is available [here](https://www.dropbox.com/s/zn3swrfxkyyelpi/tealish-0.0.1.vsix?dl=0) 25 | 26 | 27 | ### Tests 28 | Tests are included in the `tests/` directory. [AlgoJig](https://github.com/Hipo/algojig) and [Tealish](https://github.com/tinymanorg/tealish) are required to run the tests. 29 | 30 | Set up a new virtualenv and install the specific versions of AlgoJig & Tealish & AlgoSDK with `pip install -r requirements.txt`. 31 | 32 | ``` 33 | python -m unittest 34 | ``` 35 | 36 | 37 | ### Bug Bounty Program 38 | Details to be announced in the week of the 28th November. 39 | 40 | Reports of potential flaws must be responsibly disclosed to `security@tinyman.org`. Do not share details with anyone else until notified to do so by the team. 41 | 42 | ### Audit 43 | An audit of these contracts has been completed by [Runtime Verification](https://runtimeverification.com/). It can be found in [their GitHub repo](https://github.com/runtimeverification/publications/tree/main/reports/smart-contracts/Tinyman-amm-v2-audit). 44 | 45 | 46 | ### Acknowledgements 47 | The Tinyman team would like to thank Runtime Verification for their insightful comments and code improvement suggestions. 48 | 49 | 50 | ### Licensing 51 | 52 | The contents of this repository are licensed under the Business Source License 1.1 (BUSL-1.1), see [LICENSE](LICENSE). 53 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Reports of potential flaws must be responsibly disclosed to `security@tinyman.org`. Do not share details with anyone else until notified to do so by the team. 6 | 7 | ### Bug Bounty Program 8 | Please see details in the blog post announcing the program: 9 | https://tinymanorg.medium.com/tinyman-bug-bounty-campaign-b6c5e1ba7d6c 10 | -------------------------------------------------------------------------------- /contracts/amm_clear_state.tl: -------------------------------------------------------------------------------- 1 | #pragma version 7 2 | #tealish version git+https://github.com/tinymanorg/tealish.git@0cec751154b0083c2cb79da43b40aa26b367ecc4 3 | 4 | # Tinyman AMM V2 5 | # License: https://github.com/tinymanorg/tinyman-amm-contracts-v2/blob/main/LICENSE 6 | # Documentation: https://docs.tinyman.org 7 | 8 | exit(1) 9 | -------------------------------------------------------------------------------- /contracts/build/amm_approval.teal.tok: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinymanorg/tinyman-amm-contracts-v2/3de3a35e1ce0ca96576d18c2728f6042d1a07f72/contracts/build/amm_approval.teal.tok -------------------------------------------------------------------------------- /contracts/build/amm_clear_state.teal: -------------------------------------------------------------------------------- 1 | #pragma version 7 2 | // tealish version git+https://github.com/tinymanorg/tealish.git@0cec751154b0083c2cb79da43b40aa26b367ecc4 3 | 4 | // Tinyman AMM V2 5 | // License: https://github.com/tinymanorg/tinyman-amm-contracts-v2/blob/main/LICENSE 6 | // Documentation: https://docs.tinyman.org 7 | 8 | // exit(1) 9 | pushint 1 10 | return 11 | 12 | -------------------------------------------------------------------------------- /contracts/build/amm_clear_state.teal.tok: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinymanorg/tinyman-amm-contracts-v2/3de3a35e1ce0ca96576d18c2728f6042d1a07f72/contracts/build/amm_clear_state.teal.tok -------------------------------------------------------------------------------- /contracts/build/pool_template.teal: -------------------------------------------------------------------------------- 1 | #pragma version 6 2 | // tealish version git+https://github.com/tinymanorg/tealish.git@0cec751154b0083c2cb79da43b40aa26b367ecc4 3 | 4 | // Tinyman AMM V2 5 | // License: https://github.com/tinymanorg/tinyman-amm-contracts-v2/blob/main/LICENSE 6 | // Documentation: https://docs.tinyman.org 7 | 8 | // int application_id = extract_uint64(KEY, 0) [slot 0] 9 | pushbytes "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" // KEY 10 | pushint 0 11 | extract_uint64 12 | store 0 // application_id 13 | // Only allow OptIn calls to a specific application id 14 | // assert(application_id == Txn.ApplicationID) 15 | load 0 // application_id 16 | txn ApplicationID 17 | == 18 | assert 19 | // assert(Txn.OnCompletion == OptIn) 20 | txn OnCompletion 21 | pushint 1 // OptIn 22 | == 23 | assert 24 | // exit(1) 25 | pushint 1 26 | return 27 | 28 | -------------------------------------------------------------------------------- /contracts/build/pool_template.teal.tok: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinymanorg/tinyman-amm-contracts-v2/3de3a35e1ce0ca96576d18c2728f6042d1a07f72/contracts/build/pool_template.teal.tok -------------------------------------------------------------------------------- /contracts/pool_template.tl: -------------------------------------------------------------------------------- 1 | #pragma version 6 2 | #tealish version git+https://github.com/tinymanorg/tealish.git@0cec751154b0083c2cb79da43b40aa26b367ecc4 3 | 4 | # Tinyman AMM V2 5 | # License: https://github.com/tinymanorg/tinyman-amm-contracts-v2/blob/main/LICENSE 6 | # Documentation: https://docs.tinyman.org 7 | 8 | const bytes KEY = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 9 | int application_id = extract_uint64(KEY, 0) 10 | # Only allow OptIn calls to a specific application id 11 | assert(application_id == Txn.ApplicationID) 12 | assert(Txn.OnCompletion == OptIn) 13 | exit(1) 14 | -------------------------------------------------------------------------------- /docs/Tinyman AMM V2 Protocol Specification.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinymanorg/tinyman-amm-contracts-v2/3de3a35e1ce0ca96576d18c2728f6042d1a07f72/docs/Tinyman AMM V2 Protocol Specification.pdf -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | py-algorand-sdk==1.17 2 | git+https://github.com/tinymanorg/tealish.git@0cec751154b0083c2cb79da43b40aa26b367ecc4 3 | git+https://github.com/Hipo/algojig.git@282719479f22cb1b46c82c1a80981df2cc777574 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinymanorg/tinyman-amm-contracts-v2/3de3a35e1ce0ca96576d18c2728f6042d1a07f72/tests/__init__.py -------------------------------------------------------------------------------- /tests/constants.py: -------------------------------------------------------------------------------- 1 | from algojig import TealishProgram 2 | from algosdk.logic import get_application_address 3 | 4 | amm_pool_template = TealishProgram('contracts/pool_template.tl') 5 | amm_approval_program = TealishProgram('contracts/amm_approval.tl') 6 | amm_clear_state_program = TealishProgram('contracts/amm_clear_state.tl') 7 | 8 | METHOD_BOOTSTRAP = "bootstrap" 9 | METHOD_ADD_LIQUIDITY = "add_liquidity" 10 | METHOD_ADD_INITIAL_LIQUIDITY = "add_initial_liquidity" 11 | METHOD_REMOVE_LIQUIDITY = "remove_liquidity" 12 | METHOD_SWAP = "swap" 13 | METHOD_FLASH_LOAN = "flash_loan" 14 | METHOD_VERIFY_FLASH_LOAN = "verify_flash_loan" 15 | METHOD_FLASH_SWAP = "flash_swap" 16 | METHOD_VERIFY_FLASH_SWAP = "verify_flash_swap" 17 | METHOD_CLAIM_FEES = "claim_fees" 18 | METHOD_CLAIM_EXTRA = "claim_extra" 19 | METHOD_SET_FEE = "set_fee" 20 | METHOD_SET_FEE_COLLECTOR = "set_fee_collector" 21 | METHOD_SET_FEE_SETTER = "set_fee_setter" 22 | METHOD_SET_FEE_MANAGER = "set_fee_manager" 23 | 24 | TOTAL_FEE_SHARE = 30 25 | PROTOCOL_FEE_RATIO = 6 26 | 27 | LOCKED_POOL_TOKENS = 1_000 28 | PRICE_SCALE_FACTOR = 2**64 # 18446744073709551616 29 | BLOCK_TIME_DELTA = 1000 30 | BYTE_ZERO = b'\x00\x00\x00\x00\x00\x00\x00\x00' 31 | 32 | MAX_UINT64 = 2**64 - 1 # 18446744073709551615 33 | MAX_ASSET_AMOUNT = MAX_UINT64 34 | POOL_TOKEN_TOTAL_SUPPLY = MAX_ASSET_AMOUNT 35 | ALGO_ASSET_ID = 0 36 | APPLICATION_ID = 1 37 | APPLICATION_ADDRESS = get_application_address(APPLICATION_ID) 38 | 39 | # State 40 | APP_LOCAL_INTS = 12 41 | APP_LOCAL_BYTES = 2 42 | APP_GLOBAL_INTS = 0 43 | APP_GLOBAL_BYTES = 3 44 | 45 | # 100,000 Algo 46 | # + 100,000 ASA 1 47 | # + 100,000 ASA 2 48 | # + 100,000 Pool Token 49 | # + 542,500 App Optin (100000 + (25000+3500)*12 + (25000+25000)*2) 50 | MIN_POOL_BALANCE_ASA_ALGO_PAIR = 300_000 + (100_000 + (25_000 + 3_500) * APP_LOCAL_INTS + (25_000 + 25_000) * APP_LOCAL_BYTES) 51 | MIN_POOL_BALANCE_ASA_ASA_PAIR = MIN_POOL_BALANCE_ASA_ALGO_PAIR + 100_000 52 | -------------------------------------------------------------------------------- /tests/core.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from decimal import Decimal 3 | 4 | from algosdk.encoding import decode_address 5 | from algosdk.future import transaction 6 | 7 | from .constants import * 8 | from .utils import get_pool_logicsig_bytecode 9 | 10 | 11 | class BaseTestCase(unittest.TestCase): 12 | maxDiff = None 13 | 14 | def create_amm_app(self): 15 | if self.app_creator_address not in self.ledger.accounts: 16 | self.ledger.set_account_balance(self.app_creator_address, 1_000_000) 17 | 18 | self.ledger.create_app( 19 | app_id=APPLICATION_ID, 20 | approval_program=amm_approval_program, 21 | creator=self.app_creator_address, 22 | local_ints=APP_LOCAL_INTS, 23 | local_bytes=APP_LOCAL_BYTES, 24 | global_ints=APP_GLOBAL_INTS, 25 | global_bytes=APP_GLOBAL_BYTES 26 | ) 27 | # 100_000 for basic min balance requirement 28 | # + 100_000 for increase_cost_budget app creation min balance requirement 29 | self.ledger.set_account_balance(APPLICATION_ADDRESS, 200_000) 30 | self.ledger.set_global_state( 31 | APPLICATION_ID, 32 | { 33 | b'fee_collector': decode_address(self.app_creator_address), 34 | b'fee_manager': decode_address(self.app_creator_address), 35 | b'fee_setter': decode_address(self.app_creator_address), 36 | } 37 | ) 38 | 39 | def bootstrap_pool(self, asset_1_id, asset_2_id): 40 | lsig = get_pool_logicsig_bytecode(amm_pool_template, APPLICATION_ID, asset_1_id, asset_2_id) 41 | pool_address = lsig.address() 42 | 43 | if asset_2_id: 44 | minimum_balance = MIN_POOL_BALANCE_ASA_ASA_PAIR 45 | else: 46 | minimum_balance = MIN_POOL_BALANCE_ASA_ALGO_PAIR 47 | 48 | # Algojig cannot account application opt-in requirements right now. 49 | local_state_requirements = (25000 + 3500) * APP_LOCAL_INTS + (25000 + 25000) * APP_LOCAL_BYTES 50 | minimum_balance -= local_state_requirements 51 | 52 | # Set Algo balance (min balance + 100_000 to be transferred to the app account) 53 | self.ledger.set_account_balance(pool_address, minimum_balance + 100_000) 54 | 55 | # Rekey to application address 56 | self.ledger.set_auth_addr(pool_address, APPLICATION_ADDRESS) 57 | 58 | # Opt-in to assets 59 | self.ledger.set_account_balance(pool_address, 0, asset_id=asset_1_id) 60 | if asset_2_id != 0: 61 | self.ledger.set_account_balance(pool_address, 0, asset_id=asset_2_id) 62 | 63 | # Create pool token 64 | pool_token_asset_id = self.ledger.create_asset(asset_id=None, params=dict(creator=APPLICATION_ADDRESS)) 65 | 66 | # Transfer Algo to application address 67 | self.ledger.move(100_000, asset_id=0, sender=pool_address, receiver=APPLICATION_ADDRESS) 68 | 69 | # Transfer pool tokens from application adress to pool 70 | self.ledger.set_account_balance(APPLICATION_ADDRESS, 0, asset_id=pool_token_asset_id) 71 | self.ledger.set_account_balance(pool_address, POOL_TOKEN_TOTAL_SUPPLY, asset_id=pool_token_asset_id) 72 | 73 | self.ledger.set_local_state( 74 | address=pool_address, 75 | app_id=APPLICATION_ID, 76 | state={ 77 | b'asset_1_id': asset_1_id, 78 | b'asset_2_id': asset_2_id, 79 | b'pool_token_asset_id': pool_token_asset_id, 80 | 81 | b'total_fee_share': TOTAL_FEE_SHARE, 82 | b'protocol_fee_ratio': PROTOCOL_FEE_RATIO, 83 | 84 | b'asset_1_reserves': 0, 85 | b'asset_2_reserves': 0, 86 | b'issued_pool_tokens': 0, 87 | 88 | b'asset_1_cumulative_price': BYTE_ZERO, 89 | b'asset_2_cumulative_price': BYTE_ZERO, 90 | b'cumulative_price_update_timestamp': 0, 91 | 92 | b'lock': 0, 93 | 94 | b'asset_1_protocol_fees': 0, 95 | b'asset_2_protocol_fees': 0, 96 | } 97 | ) 98 | self.assertEqual(self.ledger.get_account_balance(pool_address)[0], minimum_balance) 99 | return pool_address, pool_token_asset_id 100 | 101 | def set_initial_pool_liquidity(self, pool_address, asset_1_id, asset_2_id, pool_token_asset_id, asset_1_reserves, asset_2_reserves, liquidity_provider_address=None): 102 | issued_pool_token_amount = int(Decimal.sqrt(Decimal(asset_1_reserves) * Decimal(asset_2_reserves))) 103 | pool_token_out_amount = issued_pool_token_amount - LOCKED_POOL_TOKENS 104 | assert pool_token_out_amount > 0 105 | 106 | self.ledger.update_local_state( 107 | address=pool_address, 108 | app_id=APPLICATION_ID, 109 | state_delta={ 110 | b'asset_1_reserves': asset_1_reserves, 111 | b'asset_2_reserves': asset_2_reserves, 112 | b'issued_pool_tokens': issued_pool_token_amount, 113 | } 114 | ) 115 | 116 | self.ledger.move(sender=liquidity_provider_address, receiver=pool_address, amount=asset_1_reserves, asset_id=asset_1_id) 117 | self.ledger.move(sender=liquidity_provider_address, receiver=pool_address, amount=asset_2_reserves, asset_id=asset_2_id) 118 | self.ledger.move(sender=pool_address, receiver=liquidity_provider_address, amount=pool_token_out_amount, asset_id=pool_token_asset_id) 119 | 120 | def set_pool_protocol_fees(self, asset_1_protocol_fees, asset_2_protocol_fees): 121 | self.ledger.update_local_state( 122 | address=self.pool_address, 123 | app_id=APPLICATION_ID, 124 | state_delta={ 125 | b'asset_1_protocol_fees': asset_1_protocol_fees, 126 | b'asset_2_protocol_fees': asset_2_protocol_fees, 127 | } 128 | ) 129 | 130 | self.ledger.move(receiver=self.pool_address, amount=asset_1_protocol_fees, asset_id=self.asset_1_id) 131 | self.ledger.move(receiver=self.pool_address, amount=asset_2_protocol_fees, asset_id=self.asset_1_id) 132 | 133 | def get_add_initial_liquidity_transactions(self, asset_1_amount, asset_2_amount, app_call_fee=None): 134 | txn_group = [] 135 | txn_group.append( 136 | transaction.AssetTransferTxn( 137 | sender=self.user_addr, 138 | sp=self.sp, 139 | receiver=self.pool_address, 140 | index=self.asset_1_id, 141 | amt=asset_1_amount, 142 | ) 143 | ) 144 | txn_group.append( 145 | transaction.AssetTransferTxn( 146 | sender=self.user_addr, 147 | sp=self.sp, 148 | receiver=self.pool_address, 149 | index=self.asset_2_id, 150 | amt=asset_2_amount, 151 | ) if self.asset_2_id else transaction.PaymentTxn( 152 | sender=self.user_addr, 153 | sp=self.sp, 154 | receiver=self.pool_address, 155 | amt=asset_2_amount, 156 | ) 157 | ) 158 | txn_group.append( 159 | transaction.ApplicationNoOpTxn( 160 | sender=self.user_addr, 161 | sp=self.sp, 162 | index=APPLICATION_ID, 163 | app_args=[METHOD_ADD_INITIAL_LIQUIDITY], 164 | foreign_assets=[self.pool_token_asset_id], 165 | accounts=[self.pool_address], 166 | ) 167 | ) 168 | txn_group[-1].fee = app_call_fee or self.sp.fee 169 | return txn_group 170 | 171 | def get_add_liquidity_transactions(self, asset_1_amount, asset_2_amount, min_output=0, app_call_fee=None): 172 | txn_group = [] 173 | if asset_1_amount is not None and asset_2_amount is not None: 174 | mode = 'flexible' 175 | else: 176 | mode = 'single' 177 | if asset_1_amount is not None: 178 | txn_group.append( 179 | transaction.AssetTransferTxn( 180 | sender=self.user_addr, 181 | sp=self.sp, 182 | receiver=self.pool_address, 183 | index=self.asset_1_id, 184 | amt=asset_1_amount, 185 | ) 186 | ) 187 | if asset_2_amount is not None: 188 | txn_group.append( 189 | transaction.AssetTransferTxn( 190 | sender=self.user_addr, 191 | sp=self.sp, 192 | receiver=self.pool_address, 193 | index=self.asset_2_id, 194 | amt=asset_2_amount, 195 | ) if self.asset_2_id else transaction.PaymentTxn( 196 | sender=self.user_addr, 197 | sp=self.sp, 198 | receiver=self.pool_address, 199 | amt=asset_2_amount, 200 | ) 201 | ) 202 | txn_group.append( 203 | transaction.ApplicationNoOpTxn( 204 | sender=self.user_addr, 205 | sp=self.sp, 206 | index=APPLICATION_ID, 207 | app_args=[METHOD_ADD_LIQUIDITY, mode, min_output], 208 | foreign_assets=[self.pool_token_asset_id], 209 | accounts=[self.pool_address], 210 | ) 211 | ) 212 | txn_group[-1].fee = app_call_fee or (self.sp.fee * 3) 213 | return txn_group 214 | 215 | def get_remove_liquidity_transactions(self, liquidity_asset_amount, min_output_1=0, min_output_2=0, app_call_fee=None): 216 | txn_group = [ 217 | transaction.AssetTransferTxn( 218 | sender=self.user_addr, 219 | sp=self.sp, 220 | receiver=self.pool_address, 221 | index=self.pool_token_asset_id, 222 | amt=liquidity_asset_amount, 223 | ), 224 | transaction.ApplicationNoOpTxn( 225 | sender=self.user_addr, 226 | sp=self.sp, 227 | index=APPLICATION_ID, 228 | app_args=[METHOD_REMOVE_LIQUIDITY, min_output_1, min_output_2], 229 | foreign_assets=[self.asset_1_id, self.asset_2_id], 230 | accounts=[self.pool_address], 231 | ) 232 | ] 233 | txn_group[1].fee = app_call_fee or self.sp.fee 234 | return txn_group 235 | 236 | def get_remove_liquidity_single_transactions(self, liquidity_asset_amount, asset_id, min_output=0, app_call_fee=None): 237 | min_output_1 = min_output if asset_id == self.asset_1_id else 0 238 | min_output_2 = min_output if asset_id == self.asset_1_id else 0 239 | txn_group = [ 240 | transaction.AssetTransferTxn( 241 | sender=self.user_addr, 242 | sp=self.sp, 243 | receiver=self.pool_address, 244 | index=self.pool_token_asset_id, 245 | amt=liquidity_asset_amount, 246 | ), 247 | transaction.ApplicationNoOpTxn( 248 | sender=self.user_addr, 249 | sp=self.sp, 250 | index=APPLICATION_ID, 251 | app_args=[METHOD_REMOVE_LIQUIDITY, min_output_1, min_output_2], 252 | foreign_assets=[asset_id], 253 | accounts=[self.pool_address], 254 | ) 255 | ] 256 | txn_group[1].fee = app_call_fee or self.sp.fee 257 | return txn_group 258 | 259 | def get_claim_fee_transactions(self, sender, fee_collector, app_call_fee=None): 260 | txn_group = [ 261 | transaction.ApplicationNoOpTxn( 262 | sender=sender, 263 | sp=self.sp, 264 | index=APPLICATION_ID, 265 | app_args=[METHOD_CLAIM_FEES], 266 | foreign_assets=[self.asset_1_id, self.asset_2_id], 267 | accounts=[self.pool_address, fee_collector], 268 | ) 269 | ] 270 | txn_group[0].fee = app_call_fee or self.sp.fee 271 | return txn_group 272 | 273 | def get_claim_extra_transactions(self, sender, asset_id, address, fee_collector, app_call_fee=None): 274 | txn_group = [ 275 | transaction.ApplicationNoOpTxn( 276 | sender=sender, 277 | sp=self.sp, 278 | index=APPLICATION_ID, 279 | app_args=[METHOD_CLAIM_EXTRA], 280 | foreign_assets=[asset_id], 281 | accounts=[address, fee_collector], 282 | ) 283 | ] 284 | txn_group[0].fee = app_call_fee or self.sp.fee 285 | return txn_group 286 | 287 | @classmethod 288 | def sign_txns(cls, txns, secret_key): 289 | return [txn.sign(secret_key) for txn in txns] 290 | -------------------------------------------------------------------------------- /tests/dummy_program.tl: -------------------------------------------------------------------------------- 1 | #pragma version 7 2 | exit(1) -------------------------------------------------------------------------------- /tests/price_oracle_reader.tl: -------------------------------------------------------------------------------- 1 | #pragma version 7 2 | 3 | const int TINYMAN_APP_ID = 1 4 | const bytes TWO_TO_THE_64 = "\x01\x00\x00\x00\x00\x00\x00\x00\x00" 5 | 6 | bytes pool_asset_1_cumulative_price_key = concat(Txn.Accounts[1], "_asset_1_cumulative_price") 7 | bytes pool_asset_2_cumulative_price_key = concat(Txn.Accounts[1], "_asset_2_cumulative_price") 8 | bytes pool_cumulative_price_update_timestamp_key = concat(Txn.Accounts[1], "_price_update_timestamp") 9 | bytes pool_asset_1_price_key = concat(Txn.Accounts[1], "_asset_1_price") 10 | bytes pool_asset_2_price_key = concat(Txn.Accounts[1], "_asset_2_price") 11 | 12 | bytes asset_1_cumulative_price 13 | bytes asset_2_cumulative_price 14 | int cumulative_price_update_timestamp 15 | int exists 16 | 17 | exists, asset_1_cumulative_price = app_local_get_ex(1, TINYMAN_APP_ID, "asset_1_cumulative_price") 18 | assert(exists) 19 | exists, asset_2_cumulative_price = app_local_get_ex(1, TINYMAN_APP_ID, "asset_2_cumulative_price") 20 | assert(exists) 21 | exists, cumulative_price_update_timestamp = app_local_get_ex(1, TINYMAN_APP_ID, "cumulative_price_update_timestamp") 22 | assert(exists) 23 | 24 | int time_delta = cumulative_price_update_timestamp - app_global_get(pool_cumulative_price_update_timestamp_key) 25 | 26 | if time_delta: 27 | if app_global_get(pool_cumulative_price_update_timestamp_key): 28 | bytes asset_1_price = (asset_1_cumulative_price b- app_global_get(pool_asset_1_cumulative_price_key)) b/ itob(time_delta) 29 | bytes asset_2_price = (asset_2_cumulative_price b- app_global_get(pool_asset_2_cumulative_price_key)) b/ itob(time_delta) 30 | app_global_put(pool_asset_1_price_key, asset_1_price) 31 | app_global_put(pool_asset_2_price_key, asset_2_price) 32 | end 33 | 34 | app_global_put(pool_asset_1_cumulative_price_key, asset_1_cumulative_price) 35 | app_global_put(pool_asset_2_cumulative_price_key, asset_2_cumulative_price) 36 | app_global_put(pool_cumulative_price_update_timestamp_key, cumulative_price_update_timestamp) 37 | end 38 | exit(1) -------------------------------------------------------------------------------- /tests/proxy_approval_program.tl: -------------------------------------------------------------------------------- 1 | #pragma version 7 2 | 3 | const int TINYMAN_APP_ID = 1 4 | const int FEE_BASIS_POINTS = 100 5 | 6 | assert(Gtxn[0].AssetReceiver == Global.CurrentApplicationAddress) 7 | int swap_amount = (Gtxn[0].AssetAmount * (10000 - FEE_BASIS_POINTS)) / 10000 8 | int initial_output_balance 9 | _, initial_output_balance = asset_holding_get(AssetBalance, Global.CurrentApplicationAddress, Txn.Assets[1]) 10 | inner_group: 11 | inner_txn: 12 | TypeEnum: Axfer 13 | Fee: 0 14 | AssetReceiver: Txn.Accounts[1] 15 | XferAsset: Gtxn[0].XferAsset 16 | AssetAmount: swap_amount 17 | end 18 | inner_txn: 19 | TypeEnum: Appl 20 | Fee: 0 21 | ApplicationID: TINYMAN_APP_ID 22 | ApplicationArgs[0]: "swap" 23 | ApplicationArgs[1]: "fixed-input" 24 | ApplicationArgs[2]: Txn.ApplicationArgs[1] 25 | Accounts[0]: Txn.Accounts[1] 26 | Assets[0]: Txn.Assets[0] 27 | Assets[1]: Txn.Assets[1] 28 | end 29 | end 30 | 31 | int new_output_balance 32 | _, new_output_balance = asset_holding_get(AssetBalance, Global.CurrentApplicationAddress, Txn.Assets[1]) 33 | int output_amount = new_output_balance - initial_output_balance 34 | inner_txn: 35 | TypeEnum: Axfer 36 | Fee: 0 37 | AssetReceiver: Txn.Sender 38 | XferAsset: Txn.Assets[1] 39 | AssetAmount: output_amount 40 | end 41 | exit(1) -------------------------------------------------------------------------------- /tests/tests_bootstrap.py: -------------------------------------------------------------------------------- 1 | from algojig import get_suggested_params 2 | from algojig.exceptions import LogicEvalError 3 | from algojig.ledger import JigLedger 4 | from algosdk.account import generate_account 5 | from algosdk.encoding import decode_address 6 | from algosdk.future import transaction 7 | 8 | from .constants import * 9 | from .core import BaseTestCase 10 | from .utils import get_pool_logicsig_bytecode 11 | 12 | 13 | class TestBootstrap(BaseTestCase): 14 | 15 | @classmethod 16 | def setUpClass(cls): 17 | cls.sp = get_suggested_params() 18 | cls.app_creator_sk, cls.app_creator_address = generate_account() 19 | cls.user_sk, cls.user_addr = generate_account() 20 | 21 | cls.sp.fee = 7000 22 | cls.asset_1_id = 5 23 | cls.asset_2_id = 2 24 | cls.pool_token_total_supply = 18446744073709551615 25 | 26 | def setUp(self): 27 | self.ledger = JigLedger() 28 | self.create_amm_app() 29 | self.ledger.set_account_balance(self.user_addr, 1_000_000) 30 | self.asset_2_id = self.ledger.create_asset(asset_id=None, params=dict(unit_name="BTC")) 31 | self.asset_1_id = self.ledger.create_asset(asset_id=None, params=dict(unit_name="USD")) 32 | self.ledger.set_account_balance(self.user_addr, 0, asset_id=self.asset_1_id) 33 | self.ledger.set_account_balance(self.user_addr, 0, asset_id=self.asset_2_id) 34 | 35 | def test_pass(self): 36 | lsig = get_pool_logicsig_bytecode(amm_pool_template, APPLICATION_ID, self.asset_1_id, self.asset_2_id) 37 | pool_address = lsig.address() 38 | self.ledger.set_account_balance(pool_address, MIN_POOL_BALANCE_ASA_ASA_PAIR + self.sp.fee + 100_000) 39 | transactions = [ 40 | transaction.LogicSigTransaction( 41 | transaction.ApplicationOptInTxn( 42 | sender=lsig.address(), 43 | sp=self.sp, 44 | index=APPLICATION_ID, 45 | app_args=[METHOD_BOOTSTRAP], 46 | foreign_assets=[self.asset_1_id, self.asset_2_id], 47 | rekey_to=APPLICATION_ADDRESS, 48 | ), 49 | lsig 50 | ) 51 | ] 52 | 53 | block = self.ledger.eval_transactions(transactions) 54 | block_txns = block[b'txns'] 55 | 56 | # outer transactions 57 | self.assertEqual(len(block_txns), 1) 58 | txn = block_txns[0] 59 | self.assertEqual( 60 | txn[b'txn'], 61 | { 62 | b'apaa': [b'bootstrap'], 63 | b'apan': transaction.OnComplete.OptInOC, 64 | b'apas': [self.asset_1_id, self.asset_2_id], 65 | b'apid': APPLICATION_ID, 66 | b'fee': self.sp.fee, 67 | b'fv': self.sp.first, 68 | b'lv': self.sp.last, 69 | b'rekey': decode_address(APPLICATION_ADDRESS), 70 | b'snd': decode_address(pool_address), 71 | b'type': b'appl' 72 | } 73 | ) 74 | 75 | # inner transactions 76 | inner_transactions = txn[b'dt'][b'itx'] 77 | self.assertEqual(len(inner_transactions), 6) 78 | 79 | # inner transactions - [0] 80 | self.assertDictEqual( 81 | inner_transactions[0][b'txn'], 82 | { 83 | b'amt': 100000, 84 | b'fv': self.sp.first, 85 | b'lv': self.sp.last, 86 | b'rcv': decode_address(APPLICATION_ADDRESS), 87 | b'snd': decode_address(pool_address), 88 | b'type': b'pay' 89 | } 90 | ) 91 | 92 | # inner transactions - [1] 93 | created_asset_id = inner_transactions[1][b'caid'] 94 | 95 | self.assertDictEqual( 96 | inner_transactions[1][b'txn'], 97 | { 98 | b'apar': { 99 | b'an': b'TinymanPool2.0 USD-BTC', 100 | b'au': b'https://tinyman.org', 101 | b'dc': 6, 102 | b't': self.pool_token_total_supply, 103 | b'un': b'TMPOOL2', 104 | b'r': decode_address(pool_address), 105 | b'am': self.asset_1_id.to_bytes(8, 'big') + self.asset_2_id.to_bytes(8, 'big') + (0).to_bytes(16, 'big') 106 | }, 107 | b'fv': self.sp.first, 108 | b'lv': self.sp.last, 109 | b'snd': decode_address(APPLICATION_ADDRESS), 110 | b'type': b'acfg' 111 | } 112 | ) 113 | 114 | # inner transactions - [2] 115 | self.assertDictEqual( 116 | inner_transactions[2][b'txn'], 117 | { 118 | b'arcv': decode_address(pool_address), 119 | b'fv': self.sp.first, 120 | b'lv': self.sp.last, 121 | b'snd': decode_address(pool_address), 122 | b'type': b'axfer', 123 | b'xaid': self.asset_1_id 124 | } 125 | ) 126 | 127 | # inner transactions - [3] 128 | self.assertDictEqual( 129 | inner_transactions[3][b'txn'], 130 | { 131 | b'arcv': decode_address(pool_address), 132 | b'fv': self.sp.first, 133 | b'lv': self.sp.last, 134 | b'snd': decode_address(pool_address), 135 | b'type': b'axfer', 136 | b'xaid': self.asset_2_id 137 | } 138 | ) 139 | 140 | # inner transactions - [4] 141 | self.assertDictEqual( 142 | inner_transactions[4][b'txn'], 143 | { 144 | b'arcv': decode_address(pool_address), 145 | b'fv': self.sp.first, 146 | b'lv': self.sp.last, 147 | b'snd': decode_address(pool_address), 148 | b'type': b'axfer', 149 | b'xaid': created_asset_id 150 | } 151 | ) 152 | 153 | # inner transactions - [5] 154 | self.assertDictEqual( 155 | inner_transactions[5][b'txn'], 156 | { 157 | b'aamt': 18446744073709551615, 158 | b'arcv': decode_address(pool_address), 159 | b'fv': self.sp.first, 160 | b'lv': self.sp.last, 161 | b'snd': decode_address(APPLICATION_ADDRESS), 162 | b'type': b'axfer', 163 | b'xaid': created_asset_id 164 | } 165 | ) 166 | 167 | # local state delta 168 | pool_delta = txn[b'dt'][b'ld'][0] 169 | self.assertDictEqual( 170 | pool_delta, 171 | { 172 | b'asset_1_id': {b'at': 2, b'ui': self.asset_1_id}, 173 | b'asset_1_reserves': {b'at': 2}, 174 | b'asset_2_id': {b'at': 2, b'ui': self.asset_2_id}, 175 | b'asset_2_reserves': {b'at': 2}, 176 | b'asset_1_cumulative_price': {b'at': 1, b'bs': BYTE_ZERO}, 177 | b'asset_2_cumulative_price': {b'at': 1, b'bs': BYTE_ZERO}, 178 | b'cumulative_price_update_timestamp': {b'at': 2, b'ui': BLOCK_TIME_DELTA}, 179 | b'issued_pool_tokens': {b'at': 2}, 180 | b'lock': {b'at': 2}, 181 | b'pool_token_asset_id': {b'at': 2, b'ui': created_asset_id}, 182 | b'total_fee_share': {b'at': 2, b'ui': TOTAL_FEE_SHARE}, 183 | b'protocol_fee_ratio': {b'at': 2, b'ui': PROTOCOL_FEE_RATIO}, 184 | b'asset_1_protocol_fees': {b'at': 2}, 185 | b'asset_2_protocol_fees': {b'at': 2}, 186 | } 187 | ) 188 | 189 | def test_fail_rekey(self): 190 | lsig = get_pool_logicsig_bytecode(amm_pool_template, APPLICATION_ID, self.asset_1_id, self.asset_2_id) 191 | pool_address = lsig.address() 192 | self.ledger.set_account_balance(pool_address, MIN_POOL_BALANCE_ASA_ASA_PAIR + self.sp.fee + 100_000) 193 | 194 | # Rekey is missing 195 | transactions = [ 196 | transaction.LogicSigTransaction( 197 | transaction.ApplicationOptInTxn( 198 | sender=lsig.address(), 199 | sp=self.sp, 200 | index=APPLICATION_ID, 201 | app_args=[METHOD_BOOTSTRAP], 202 | foreign_assets=[self.asset_1_id, self.asset_2_id], 203 | ), 204 | lsig 205 | ) 206 | ] 207 | 208 | with self.assertRaises(LogicEvalError) as e: 209 | self.ledger.eval_transactions(transactions) 210 | self.assertEqual(e.exception.source['line'], 'assert(Txn.RekeyTo == Global.CurrentApplicationAddress)') 211 | 212 | # Rekey address is wrong 213 | transactions = [ 214 | transaction.LogicSigTransaction( 215 | transaction.ApplicationOptInTxn( 216 | sender=lsig.address(), 217 | sp=self.sp, 218 | index=APPLICATION_ID, 219 | app_args=[METHOD_BOOTSTRAP], 220 | foreign_assets=[self.asset_1_id, self.asset_2_id], 221 | rekey_to=generate_account()[1], 222 | ), 223 | lsig 224 | ) 225 | ] 226 | 227 | with self.assertRaises(LogicEvalError) as e: 228 | self.ledger.eval_transactions(transactions) 229 | self.assertEqual(e.exception.source['line'], 'assert(Txn.RekeyTo == Global.CurrentApplicationAddress)') 230 | 231 | def test_fail_wrong_logicsig(self): 232 | wrong_asset_1_id = self.asset_1_id + 1 233 | lsig = get_pool_logicsig_bytecode(amm_pool_template, APPLICATION_ID, wrong_asset_1_id, self.asset_2_id) 234 | pool_address = lsig.address() 235 | self.ledger.set_account_balance(pool_address, MIN_POOL_BALANCE_ASA_ASA_PAIR + self.sp.fee + 100_000) 236 | transactions = [ 237 | transaction.LogicSigTransaction( 238 | transaction.ApplicationOptInTxn( 239 | sender=lsig.address(), 240 | sp=self.sp, 241 | index=APPLICATION_ID, 242 | app_args=[METHOD_BOOTSTRAP], 243 | foreign_assets=[self.asset_1_id, self.asset_2_id], 244 | rekey_to=APPLICATION_ADDRESS, 245 | ), 246 | lsig 247 | ) 248 | ] 249 | 250 | with self.assertRaises(LogicEvalError) as e: 251 | self.ledger.eval_transactions(transactions) 252 | self.assertEqual(e.exception.source['line'], 'assert(pool_address == sha512_256(concat("Program", program)))') 253 | 254 | def test_fail_wrong_asset_order(self): 255 | lsig = get_pool_logicsig_bytecode(amm_pool_template, APPLICATION_ID, self.asset_2_id, self.asset_1_id) 256 | pool_address = lsig.address() 257 | self.ledger.set_account_balance(pool_address, MIN_POOL_BALANCE_ASA_ASA_PAIR + self.sp.fee + 100_000) 258 | transactions = [ 259 | transaction.LogicSigTransaction( 260 | transaction.ApplicationOptInTxn( 261 | sender=lsig.address(), 262 | sp=self.sp, 263 | index=APPLICATION_ID, 264 | app_args=[METHOD_BOOTSTRAP], 265 | foreign_assets=[self.asset_2_id, self.asset_1_id], 266 | rekey_to=APPLICATION_ADDRESS, 267 | ), 268 | lsig 269 | ) 270 | ] 271 | 272 | with self.assertRaises(LogicEvalError) as e: 273 | self.ledger.eval_transactions(transactions) 274 | self.assertEqual(e.exception.source['line'], 'assert(asset_1_id > asset_2_id)') 275 | 276 | def test_fail_insufficient_fee(self): 277 | lsig = get_pool_logicsig_bytecode(amm_pool_template, APPLICATION_ID, self.asset_1_id, self.asset_2_id) 278 | pool_address = lsig.address() 279 | self.ledger.set_account_balance(pool_address, MIN_POOL_BALANCE_ASA_ASA_PAIR + self.sp.fee + 100_000) 280 | transactions = [ 281 | transaction.LogicSigTransaction( 282 | transaction.ApplicationOptInTxn( 283 | sender=lsig.address(), 284 | sp=self.sp, 285 | index=APPLICATION_ID, 286 | app_args=[METHOD_BOOTSTRAP], 287 | foreign_assets=[self.asset_1_id, self.asset_2_id], 288 | rekey_to=APPLICATION_ADDRESS, 289 | ), 290 | lsig 291 | ) 292 | ] 293 | transactions[0].transaction.fee = self.sp.fee - 1 294 | 295 | with self.assertRaises(LogicEvalError) as e: 296 | self.ledger.eval_transactions(transactions) 297 | self.assertEqual(e.exception.source['line'], 'inner_txn:') 298 | 299 | def test_fail_wrong_method_name(self): 300 | lsig = get_pool_logicsig_bytecode(amm_pool_template, APPLICATION_ID, self.asset_1_id, self.asset_2_id) 301 | pool_address = lsig.address() 302 | self.ledger.set_account_balance(pool_address, MIN_POOL_BALANCE_ASA_ASA_PAIR + self.sp.fee + 100_000) 303 | transactions = [ 304 | transaction.LogicSigTransaction( 305 | transaction.ApplicationOptInTxn( 306 | sender=lsig.address(), 307 | sp=self.sp, 308 | index=APPLICATION_ID, 309 | app_args=["invalid"], 310 | foreign_assets=[self.asset_1_id, self.asset_2_id], 311 | rekey_to=APPLICATION_ADDRESS, 312 | ), 313 | lsig 314 | ) 315 | ] 316 | 317 | with self.assertRaises(LogicEvalError) as e: 318 | self.ledger.eval_transactions(transactions) 319 | self.assertEqual(e.exception.source['line'], f'assert(Txn.ApplicationArgs[0] == "{METHOD_BOOTSTRAP}")') 320 | 321 | def test_fail_bad_asset_1_total(self): 322 | self.asset_2_id = self.ledger.create_asset(asset_id=None, params=dict(unit_name="NFT", total=100)) 323 | self.asset_1_id = self.ledger.create_asset(asset_id=None, params=dict(unit_name="BTC")) 324 | lsig = get_pool_logicsig_bytecode(amm_pool_template, APPLICATION_ID, self.asset_1_id, self.asset_2_id) 325 | pool_address = lsig.address() 326 | self.ledger.set_account_balance(pool_address, MIN_POOL_BALANCE_ASA_ASA_PAIR + self.sp.fee + 100_000) 327 | transactions = [ 328 | transaction.LogicSigTransaction( 329 | transaction.ApplicationOptInTxn( 330 | sender=lsig.address(), 331 | sp=self.sp, 332 | index=APPLICATION_ID, 333 | app_args=[METHOD_BOOTSTRAP], 334 | foreign_assets=[self.asset_1_id, self.asset_2_id], 335 | rekey_to=APPLICATION_ADDRESS, 336 | ), 337 | lsig 338 | ) 339 | ] 340 | 341 | with self.assertRaises(LogicEvalError) as e: 342 | self.ledger.eval_transactions(transactions) 343 | self.assertEqual(e.exception.source['line'], 'assert(asset_total >= ASSET_MIN_TOTAL)') 344 | 345 | def test_fail_bad_asset_2_total(self): 346 | self.asset_2_id = self.ledger.create_asset(asset_id=None, params=dict(unit_name="USDC")) 347 | self.asset_1_id = self.ledger.create_asset(asset_id=None, params=dict(unit_name="NFT", total=1)) 348 | lsig = get_pool_logicsig_bytecode(amm_pool_template, APPLICATION_ID, self.asset_1_id, self.asset_2_id) 349 | pool_address = lsig.address() 350 | self.ledger.set_account_balance(pool_address, MIN_POOL_BALANCE_ASA_ASA_PAIR + self.sp.fee + 100_000) 351 | transactions = [ 352 | transaction.LogicSigTransaction( 353 | transaction.ApplicationOptInTxn( 354 | sender=lsig.address(), 355 | sp=self.sp, 356 | index=APPLICATION_ID, 357 | app_args=[METHOD_BOOTSTRAP], 358 | foreign_assets=[self.asset_1_id, self.asset_2_id], 359 | rekey_to=APPLICATION_ADDRESS, 360 | ), 361 | lsig 362 | ) 363 | ] 364 | 365 | with self.assertRaises(LogicEvalError) as e: 366 | self.ledger.eval_transactions(transactions) 367 | self.assertEqual(e.exception.source['line'], 'assert(asset_total >= ASSET_MIN_TOTAL)') 368 | 369 | def test_asset_creation_is_funded_properly(self): 370 | pool_1_asset_2_id = self.ledger.create_asset(asset_id=None) 371 | pool_1_asset_1_id = self.ledger.create_asset(asset_id=None) 372 | pool_2_asset_2_id = ALGO_ASSET_ID 373 | pool_2_asset_1_id = self.ledger.create_asset(asset_id=None) 374 | 375 | logic_sig_pool_1 = get_pool_logicsig_bytecode(amm_pool_template, APPLICATION_ID, pool_1_asset_1_id, pool_1_asset_2_id) 376 | logic_sig_pool_2 = get_pool_logicsig_bytecode(amm_pool_template, APPLICATION_ID, pool_2_asset_1_id, pool_2_asset_2_id) 377 | 378 | self.ledger.set_account_balance(APPLICATION_ADDRESS, 100_000) 379 | self.ledger.set_account_balance(logic_sig_pool_1.address(), MIN_POOL_BALANCE_ASA_ASA_PAIR + 100000 + self.sp.fee) 380 | self.ledger.set_account_balance(logic_sig_pool_2.address(), MIN_POOL_BALANCE_ASA_ALGO_PAIR + 100000 + self.sp.fee) 381 | 382 | transactions = [ 383 | transaction.LogicSigTransaction( 384 | transaction.ApplicationOptInTxn( 385 | sender=logic_sig_pool_1.address(), 386 | sp=self.sp, 387 | index=APPLICATION_ID, 388 | app_args=[METHOD_BOOTSTRAP], 389 | foreign_assets=[pool_1_asset_1_id, pool_1_asset_2_id], 390 | rekey_to=APPLICATION_ADDRESS, 391 | ), 392 | logic_sig_pool_1 393 | ), 394 | transaction.LogicSigTransaction( 395 | transaction.ApplicationOptInTxn( 396 | sender=logic_sig_pool_2.address(), 397 | sp=self.sp, 398 | index=APPLICATION_ID, 399 | app_args=[METHOD_BOOTSTRAP], 400 | foreign_assets=[pool_2_asset_1_id, pool_2_asset_2_id], 401 | rekey_to=APPLICATION_ADDRESS, 402 | ), 403 | logic_sig_pool_2 404 | ) 405 | ] 406 | 407 | block = self.ledger.eval_transactions(transactions) 408 | block_txns = block[b'txns'] 409 | self.assertEqual(len(block_txns), 2) 410 | 411 | def test_asset_1_does_not_exist(self): 412 | self.asset_1_id = 999999 413 | 414 | lsig = get_pool_logicsig_bytecode(amm_pool_template, APPLICATION_ID, self.asset_1_id, self.asset_2_id) 415 | pool_address = lsig.address() 416 | self.ledger.set_account_balance(pool_address, MIN_POOL_BALANCE_ASA_ASA_PAIR + self.sp.fee + 100_000) 417 | transactions = [ 418 | transaction.LogicSigTransaction( 419 | transaction.ApplicationOptInTxn( 420 | sender=lsig.address(), 421 | sp=self.sp, 422 | index=APPLICATION_ID, 423 | app_args=[METHOD_BOOTSTRAP], 424 | foreign_assets=[self.asset_1_id, self.asset_2_id], 425 | rekey_to=APPLICATION_ADDRESS, 426 | ), 427 | lsig 428 | ) 429 | ] 430 | 431 | with self.assertRaises(LogicEvalError) as e: 432 | self.ledger.eval_transactions(transactions) 433 | self.assertEqual(e.exception.source['line'], 'assert(exists)') 434 | 435 | def test_asset_2_does_not_exist(self): 436 | self.asset_2_id = APPLICATION_ID 437 | self.asset_1_id = self.ledger.create_asset(asset_id=999) 438 | 439 | lsig = get_pool_logicsig_bytecode(amm_pool_template, APPLICATION_ID, self.asset_1_id, self.asset_2_id) 440 | pool_address = lsig.address() 441 | self.ledger.set_account_balance(pool_address, MIN_POOL_BALANCE_ASA_ASA_PAIR + self.sp.fee + 100_000) 442 | transactions = [ 443 | transaction.LogicSigTransaction( 444 | transaction.ApplicationOptInTxn( 445 | sender=lsig.address(), 446 | sp=self.sp, 447 | index=APPLICATION_ID, 448 | app_args=[METHOD_BOOTSTRAP], 449 | foreign_assets=[self.asset_1_id, self.asset_2_id], 450 | rekey_to=APPLICATION_ADDRESS, 451 | ), 452 | lsig 453 | ) 454 | ] 455 | 456 | with self.assertRaises(LogicEvalError) as e: 457 | self.ledger.eval_transactions(transactions) 458 | self.assertEqual(e.exception.source['line'], 'assert(exists)') 459 | 460 | 461 | class TestBootstrapAlgoPair(BaseTestCase): 462 | 463 | @classmethod 464 | def setUpClass(cls): 465 | cls.sp = get_suggested_params() 466 | cls.app_creator_sk, cls.app_creator_address = generate_account() 467 | cls.user_sk, cls.user_addr = generate_account() 468 | 469 | cls.sp.fee = 6000 470 | cls.asset_1_id = 5 471 | cls.asset_2_id = ALGO_ASSET_ID 472 | cls.pool_token_total_supply = 18446744073709551615 473 | 474 | def setUp(self): 475 | self.ledger = JigLedger() 476 | self.create_amm_app() 477 | self.ledger.set_account_balance(self.user_addr, 1_000_000) 478 | self.ledger.create_asset(self.asset_1_id, params=dict(unit_name="USD")) 479 | self.ledger.set_account_balance(self.user_addr, 0, asset_id=self.asset_1_id) 480 | 481 | def test_pass(self): 482 | lsig = get_pool_logicsig_bytecode(amm_pool_template, APPLICATION_ID, self.asset_1_id, ALGO_ASSET_ID) 483 | pool_address = lsig.address() 484 | self.ledger.set_account_balance(pool_address, MIN_POOL_BALANCE_ASA_ALGO_PAIR + self.sp.fee + 100_000) 485 | transactions = [ 486 | transaction.LogicSigTransaction( 487 | transaction.ApplicationOptInTxn( 488 | sender=lsig.address(), 489 | sp=self.sp, 490 | index=APPLICATION_ID, 491 | app_args=[METHOD_BOOTSTRAP], 492 | foreign_assets=[self.asset_1_id, ALGO_ASSET_ID], 493 | rekey_to=APPLICATION_ADDRESS, 494 | ), 495 | lsig 496 | ) 497 | ] 498 | 499 | block = self.ledger.eval_transactions(transactions) 500 | block_txns = block[b'txns'] 501 | 502 | # outer transactions 503 | self.assertEqual(len(block_txns), 1) 504 | txn = block_txns[0] 505 | self.assertEqual( 506 | txn[b'txn'], 507 | { 508 | b'apaa': [b'bootstrap'], 509 | b'apan': transaction.OnComplete.OptInOC, 510 | b'apas': [self.asset_1_id, ALGO_ASSET_ID], 511 | b'apid': APPLICATION_ID, 512 | b'fee': self.sp.fee, 513 | b'fv': self.sp.first, 514 | b'lv': self.sp.last, 515 | b'rekey': decode_address(APPLICATION_ADDRESS), 516 | b'snd': decode_address(pool_address), 517 | b'type': b'appl' 518 | } 519 | ) 520 | 521 | # inner transactions 522 | inner_transactions = txn[b'dt'][b'itx'] 523 | self.assertEqual(len(inner_transactions), 5) 524 | 525 | # inner transactions - [0] 526 | self.assertDictEqual( 527 | inner_transactions[0][b'txn'], 528 | { 529 | b'amt': 100000, 530 | b'fv': self.sp.first, 531 | b'lv': self.sp.last, 532 | b'rcv': decode_address(APPLICATION_ADDRESS), 533 | b'snd': decode_address(pool_address), 534 | b'type': b'pay' 535 | } 536 | ) 537 | 538 | # inner transactions - [1] 539 | created_asset_id = inner_transactions[1][b'caid'] 540 | self.assertDictEqual( 541 | inner_transactions[1][b'txn'], 542 | { 543 | b'apar': { 544 | b'an': b'TinymanPool2.0 USD-ALGO', 545 | b'au': b'https://tinyman.org', 546 | b'dc': 6, 547 | b't': self.pool_token_total_supply, 548 | b'un': b'TMPOOL2', 549 | b'r': decode_address(pool_address), 550 | b'am': self.asset_1_id.to_bytes(8, 'big') + self.asset_2_id.to_bytes(8, 'big') + (0).to_bytes(16, 'big') 551 | }, 552 | b'fv': self.sp.first, 553 | b'lv': self.sp.last, 554 | b'snd': decode_address(APPLICATION_ADDRESS), 555 | b'type': b'acfg' 556 | } 557 | ) 558 | 559 | # inner transactions - [2] 560 | self.assertDictEqual( 561 | inner_transactions[2][b'txn'], 562 | { 563 | b'arcv': decode_address(pool_address), 564 | b'fv': self.sp.first, 565 | b'lv': self.sp.last, 566 | b'snd': decode_address(pool_address), 567 | b'type': b'axfer', 568 | b'xaid': self.asset_1_id 569 | } 570 | ) 571 | 572 | # inner transactions - [3] 573 | self.assertDictEqual( 574 | inner_transactions[3][b'txn'], 575 | { 576 | b'arcv': decode_address(pool_address), 577 | b'fv': self.sp.first, 578 | b'lv': self.sp.last, 579 | b'snd': decode_address(pool_address), 580 | b'type': b'axfer', 581 | b'xaid': created_asset_id 582 | } 583 | ) 584 | 585 | # inner transactions - [4] 586 | self.assertDictEqual( 587 | inner_transactions[4][b'txn'], 588 | { 589 | b'aamt': 18446744073709551615, 590 | b'arcv': decode_address(pool_address), 591 | b'fv': self.sp.first, 592 | b'lv': self.sp.last, 593 | b'snd': decode_address(APPLICATION_ADDRESS), 594 | b'type': b'axfer', 595 | b'xaid': created_asset_id 596 | } 597 | ) 598 | 599 | # local state delta 600 | pool_delta = txn[b'dt'][b'ld'][0] 601 | self.assertDictEqual( 602 | pool_delta, 603 | { 604 | b'asset_1_id': {b'at': 2, b'ui': self.asset_1_id}, 605 | b'asset_1_reserves': {b'at': 2}, 606 | b'asset_2_id': {b'at': 2}, # b'ui': ALGO_ASSET_ID 607 | b'asset_2_reserves': {b'at': 2}, 608 | b'asset_1_cumulative_price': {b'at': 1, b'bs': BYTE_ZERO}, 609 | b'asset_2_cumulative_price': {b'at': 1, b'bs': BYTE_ZERO}, 610 | b'cumulative_price_update_timestamp': {b'at': 2, b'ui': BLOCK_TIME_DELTA}, 611 | b'issued_pool_tokens': {b'at': 2}, 612 | b'lock': {b'at': 2}, 613 | b'pool_token_asset_id': {b'at': 2, b'ui': created_asset_id}, 614 | b'total_fee_share': {b'at': 2, b'ui': TOTAL_FEE_SHARE}, 615 | b'protocol_fee_ratio': {b'at': 2, b'ui': PROTOCOL_FEE_RATIO}, 616 | b'asset_1_protocol_fees': {b'at': 2}, 617 | b'asset_2_protocol_fees': {b'at': 2}, 618 | } 619 | ) 620 | -------------------------------------------------------------------------------- /tests/tests_claim_extra.py: -------------------------------------------------------------------------------- 1 | from unittest import skip 2 | from unittest.mock import ANY 3 | 4 | from algojig import get_suggested_params 5 | from algojig.exceptions import LogicEvalError 6 | from algojig.ledger import JigLedger 7 | from algosdk.account import generate_account 8 | from algosdk.encoding import decode_address 9 | from algosdk.future import transaction 10 | 11 | from .constants import * 12 | from .core import BaseTestCase 13 | 14 | 15 | class TestClaimExtra(BaseTestCase): 16 | @classmethod 17 | def setUpClass(cls): 18 | cls.sp = get_suggested_params() 19 | cls.app_creator_sk, cls.app_creator_address = generate_account() 20 | cls.user_sk, cls.user_addr = generate_account() 21 | cls.asset_1_id = 5 22 | cls.asset_2_id = 2 23 | 24 | def setUp(self): 25 | self.ledger = JigLedger() 26 | self.create_amm_app() 27 | self.ledger.set_account_balance(self.user_addr, 1_000_000) 28 | self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_1_id) 29 | self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_2_id) 30 | 31 | self.pool_address, self.pool_token_asset_id = self.bootstrap_pool(self.asset_1_id, self.asset_2_id) 32 | self.ledger.opt_in_asset(self.user_addr, self.pool_token_asset_id) 33 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000, liquidity_provider_address=self.user_addr) 34 | 35 | def test_claim_asset_1_from_pool(self): 36 | fee_collector = self.app_creator_address 37 | self.ledger.set_account_balance(fee_collector, 1_000_000) 38 | self.ledger.opt_in_asset(fee_collector, self.asset_1_id) 39 | 40 | extra = 5_000 41 | self.ledger.move(extra, self.asset_1_id, receiver=self.pool_address) 42 | 43 | txn_group = self.get_claim_extra_transactions(sender=self.user_addr, asset_id=self.asset_1_id, address=self.pool_address, fee_collector=fee_collector, app_call_fee=2_000) 44 | txn_group = transaction.assign_group_id(txn_group) 45 | stxns = self.sign_txns(txn_group, self.user_sk) 46 | 47 | block = self.ledger.eval_transactions(stxns) 48 | block_txns = block[b'txns'] 49 | 50 | # outer transactions 51 | self.assertEqual(len(block_txns), 1) 52 | txn = block_txns[0] 53 | self.assertDictEqual( 54 | txn[b'txn'], 55 | { 56 | b'apaa': [b'claim_extra'], 57 | b'apas': [self.asset_1_id], 58 | b'apat': [decode_address(self.pool_address), decode_address(fee_collector)], 59 | b'apid': APPLICATION_ID, 60 | b'fee': ANY, 61 | b'fv': ANY, 62 | b'grp': ANY, 63 | b'lv': ANY, 64 | b'snd': decode_address(self.user_addr), 65 | b'type': b'appl' 66 | } 67 | ) 68 | 69 | inner_transactions = txn[b'dt'][b'itx'] 70 | self.assertEqual(len(inner_transactions), 1) 71 | 72 | # inner transactions - [0] 73 | self.assertDictEqual( 74 | inner_transactions[0][b'txn'], 75 | { 76 | b'aamt': extra, 77 | b'arcv': decode_address(fee_collector), 78 | b'fv': self.sp.first, 79 | b'lv': self.sp.last, 80 | b'snd': decode_address(self.pool_address), 81 | b'type': b'axfer', 82 | b'xaid': self.asset_1_id 83 | }, 84 | ) 85 | 86 | # There is no extra to claim 87 | txn_group = self.get_claim_extra_transactions(sender=self.user_addr, asset_id=self.asset_1_id, address=self.pool_address, fee_collector=fee_collector, app_call_fee=2_000) 88 | txn_group = transaction.assign_group_id(txn_group) 89 | stxns = self.sign_txns(txn_group, self.user_sk) 90 | 91 | with self.assertRaises(LogicEvalError) as e: 92 | self.ledger.eval_transactions(stxns) 93 | self.assertEqual(e.exception.source['line'], 'assert(asset_amount)') 94 | 95 | def test_claim_asset_2_from_pool(self): 96 | fee_collector = self.app_creator_address 97 | self.ledger.set_account_balance(fee_collector, 1_000_000) 98 | self.ledger.opt_in_asset(fee_collector, self.asset_2_id) 99 | 100 | extra = 10_000 101 | self.ledger.move(extra, self.asset_2_id, receiver=self.pool_address) 102 | 103 | txn_group = self.get_claim_extra_transactions(sender=self.user_addr, asset_id=self.asset_2_id, address=self.pool_address, fee_collector=fee_collector, app_call_fee=2_000) 104 | txn_group = transaction.assign_group_id(txn_group) 105 | stxns = self.sign_txns(txn_group, self.user_sk) 106 | 107 | block = self.ledger.eval_transactions(stxns) 108 | block_txns = block[b'txns'] 109 | 110 | # outer transactions 111 | self.assertEqual(len(block_txns), 1) 112 | txn = block_txns[0] 113 | self.assertDictEqual( 114 | txn[b'txn'], 115 | { 116 | b'apaa': [b'claim_extra'], 117 | b'apas': [self.asset_2_id], 118 | b'apat': [decode_address(self.pool_address), decode_address(fee_collector)], 119 | b'apid': APPLICATION_ID, 120 | b'fee': ANY, 121 | b'fv': ANY, 122 | b'grp': ANY, 123 | b'lv': ANY, 124 | b'snd': decode_address(self.user_addr), 125 | b'type': b'appl' 126 | } 127 | ) 128 | 129 | inner_transactions = txn[b'dt'][b'itx'] 130 | self.assertEqual(len(inner_transactions), 1) 131 | 132 | # inner transactions - [0] 133 | self.assertDictEqual( 134 | inner_transactions[0][b'txn'], 135 | { 136 | b'aamt': extra, 137 | b'arcv': decode_address(fee_collector), 138 | b'fv': self.sp.first, 139 | b'lv': self.sp.last, 140 | b'snd': decode_address(self.pool_address), 141 | b'type': b'axfer', 142 | b'xaid': self.asset_2_id 143 | }, 144 | ) 145 | 146 | # There is no extra to claim 147 | txn_group = self.get_claim_extra_transactions(sender=self.user_addr, asset_id=self.asset_2_id, address=self.pool_address, fee_collector=fee_collector, app_call_fee=2_000) 148 | txn_group = transaction.assign_group_id(txn_group) 149 | stxns = self.sign_txns(txn_group, self.user_sk) 150 | 151 | with self.assertRaises(LogicEvalError) as e: 152 | self.ledger.eval_transactions(stxns) 153 | self.assertEqual(e.exception.source['line'], 'assert(asset_amount)') 154 | 155 | def test_claim_pool_token_asset_from_pool(self): 156 | fee_collector = self.app_creator_address 157 | self.ledger.set_account_balance(fee_collector, 1_000_000) 158 | self.ledger.opt_in_asset(fee_collector, self.pool_token_asset_id) 159 | 160 | extra = 15_000 161 | self.ledger.move(extra, self.pool_token_asset_id, receiver=self.pool_address) 162 | 163 | txn_group = self.get_claim_extra_transactions(sender=self.user_addr, asset_id=self.pool_token_asset_id, address=self.pool_address, fee_collector=fee_collector, app_call_fee=2_000) 164 | txn_group = transaction.assign_group_id(txn_group) 165 | stxns = self.sign_txns(txn_group, self.user_sk) 166 | 167 | block = self.ledger.eval_transactions(stxns) 168 | block_txns = block[b'txns'] 169 | 170 | # outer transactions 171 | self.assertEqual(len(block_txns), 1) 172 | txn = block_txns[0] 173 | self.assertDictEqual( 174 | txn[b'txn'], 175 | { 176 | b'apaa': [b'claim_extra'], 177 | b'apas': [self.pool_token_asset_id], 178 | b'apat': [decode_address(self.pool_address), decode_address(fee_collector)], 179 | b'apid': APPLICATION_ID, 180 | b'fee': ANY, 181 | b'fv': ANY, 182 | b'grp': ANY, 183 | b'lv': ANY, 184 | b'snd': decode_address(self.user_addr), 185 | b'type': b'appl' 186 | } 187 | ) 188 | 189 | inner_transactions = txn[b'dt'][b'itx'] 190 | self.assertEqual(len(inner_transactions), 1) 191 | 192 | # inner transactions - [0] 193 | self.assertDictEqual( 194 | inner_transactions[0][b'txn'], 195 | { 196 | b'aamt': extra, 197 | b'arcv': decode_address(fee_collector), 198 | b'fv': self.sp.first, 199 | b'lv': self.sp.last, 200 | b'snd': decode_address(self.pool_address), 201 | b'type': b'axfer', 202 | b'xaid': self.pool_token_asset_id 203 | }, 204 | ) 205 | 206 | # There is no extra to claim 207 | txn_group = self.get_claim_extra_transactions(sender=self.user_addr, asset_id=self.pool_token_asset_id, address=self.pool_address, fee_collector=fee_collector, app_call_fee=2_000) 208 | txn_group = transaction.assign_group_id(txn_group) 209 | stxns = self.sign_txns(txn_group, self.user_sk) 210 | 211 | with self.assertRaises(LogicEvalError) as e: 212 | self.ledger.eval_transactions(stxns) 213 | self.assertEqual(e.exception.source['line'], 'assert(asset_amount)') 214 | 215 | def test_claim_algo_from_pool(self): 216 | fee_collector = self.app_creator_address 217 | self.ledger.set_account_balance(fee_collector, 1_000_000) 218 | 219 | extra = 1_345 220 | self.ledger.move(extra, ALGO_ASSET_ID, receiver=self.pool_address) 221 | 222 | txn_group = self.get_claim_extra_transactions(sender=self.user_addr, asset_id=ALGO_ASSET_ID, address=self.pool_address, fee_collector=fee_collector, app_call_fee=2_000) 223 | txn_group = transaction.assign_group_id(txn_group) 224 | stxns = self.sign_txns(txn_group, self.user_sk) 225 | 226 | block = self.ledger.eval_transactions(stxns) 227 | block_txns = block[b'txns'] 228 | 229 | # outer transactions 230 | self.assertEqual(len(block_txns), 1) 231 | txn = block_txns[0] 232 | self.assertDictEqual( 233 | txn[b'txn'], 234 | { 235 | b'apaa': [b'claim_extra'], 236 | b'apas': [ALGO_ASSET_ID], 237 | b'apat': [decode_address(self.pool_address), decode_address(fee_collector)], 238 | b'apid': APPLICATION_ID, 239 | b'fee': ANY, 240 | b'fv': ANY, 241 | b'grp': ANY, 242 | b'lv': ANY, 243 | b'snd': decode_address(self.user_addr), 244 | b'type': b'appl' 245 | } 246 | ) 247 | 248 | inner_transactions = txn[b'dt'][b'itx'] 249 | self.assertEqual(len(inner_transactions), 1) 250 | 251 | # inner transactions - [0] 252 | self.assertDictEqual( 253 | inner_transactions[0][b'txn'], 254 | { 255 | b'amt': extra, 256 | b'rcv': decode_address(fee_collector), 257 | b'fv': self.sp.first, 258 | b'lv': self.sp.last, 259 | b'snd': decode_address(self.pool_address), 260 | b'type': b'pay', 261 | }, 262 | ) 263 | 264 | # There is no extra to claim 265 | txn_group = self.get_claim_extra_transactions(sender=self.user_addr, asset_id=self.pool_token_asset_id, address=self.pool_address, fee_collector=fee_collector, app_call_fee=2_000) 266 | txn_group = transaction.assign_group_id(txn_group) 267 | stxns = self.sign_txns(txn_group, self.user_sk) 268 | 269 | with self.assertRaises(LogicEvalError) as e: 270 | self.ledger.eval_transactions(stxns) 271 | self.assertEqual(e.exception.source['line'], 'assert(asset_amount)') 272 | 273 | def test_fail_fee_collector_did_not_opt_in(self): 274 | fee_collector = self.app_creator_address 275 | self.ledger.set_account_balance(fee_collector, 1_000_000) 276 | 277 | asset_1_extra = 5_000 278 | asset_2_extra = 10_000 279 | self.ledger.move(asset_1_extra, self.asset_1_id, receiver=self.pool_address) 280 | self.ledger.move(asset_2_extra, self.asset_2_id, receiver=self.pool_address) 281 | 282 | txn_group = self.get_claim_extra_transactions(sender=self.user_addr, asset_id=self.asset_1_id, address=self.pool_address, fee_collector=fee_collector, app_call_fee=2_000) 283 | txn_group = transaction.assign_group_id(txn_group) 284 | stxns = self.sign_txns(txn_group, self.user_sk) 285 | 286 | with self.assertRaises(LogicEvalError) as e: 287 | self.ledger.eval_transactions(stxns) 288 | self.assertEqual(e.exception.source['line'], 'inner_txn:') 289 | 290 | def test_claim_algo_from_application_account(self): 291 | fee_collector = self.app_creator_address 292 | self.ledger.set_account_balance(fee_collector, 1_000_000) 293 | 294 | extra = 1_345 295 | self.ledger.move(extra, ALGO_ASSET_ID, receiver=APPLICATION_ADDRESS) 296 | 297 | txn_group = self.get_claim_extra_transactions(sender=self.user_addr, asset_id=ALGO_ASSET_ID, address=APPLICATION_ADDRESS, fee_collector=fee_collector, app_call_fee=2_000) 298 | txn_group = transaction.assign_group_id(txn_group) 299 | stxns = self.sign_txns(txn_group, self.user_sk) 300 | 301 | block = self.ledger.eval_transactions(stxns) 302 | block_txns = block[b'txns'] 303 | 304 | # outer transactions 305 | self.assertEqual(len(block_txns), 1) 306 | txn = block_txns[0] 307 | self.assertDictEqual( 308 | txn[b'txn'], 309 | { 310 | b'apaa': [b'claim_extra'], 311 | b'apas': [ALGO_ASSET_ID], 312 | b'apat': [decode_address(APPLICATION_ADDRESS), decode_address(fee_collector)], 313 | b'apid': APPLICATION_ID, 314 | b'fee': ANY, 315 | b'fv': ANY, 316 | b'grp': ANY, 317 | b'lv': ANY, 318 | b'snd': decode_address(self.user_addr), 319 | b'type': b'appl' 320 | } 321 | ) 322 | 323 | inner_transactions = txn[b'dt'][b'itx'] 324 | self.assertEqual(len(inner_transactions), 1) 325 | 326 | # inner transactions - [0] 327 | self.assertDictEqual( 328 | inner_transactions[0][b'txn'], 329 | { 330 | b'amt': extra, 331 | b'rcv': decode_address(fee_collector), 332 | b'fv': self.sp.first, 333 | b'lv': self.sp.last, 334 | b'snd': decode_address(APPLICATION_ADDRESS), 335 | b'type': b'pay', 336 | }, 337 | ) 338 | 339 | # There is no extra to claim 340 | txn_group = self.get_claim_extra_transactions(sender=self.user_addr, asset_id=ALGO_ASSET_ID, address=APPLICATION_ADDRESS, fee_collector=fee_collector, app_call_fee=2_000) 341 | txn_group = transaction.assign_group_id(txn_group) 342 | stxns = self.sign_txns(txn_group, self.user_sk) 343 | 344 | with self.assertRaises(LogicEvalError) as e: 345 | self.ledger.eval_transactions(stxns) 346 | self.assertEqual(e.exception.source['line'], 'assert(asset_amount)') 347 | 348 | @skip 349 | def test_claim_pool_token_asset_from_application_account(self): 350 | fee_collector = self.app_creator_address 351 | self.ledger.set_account_balance(fee_collector, 1_000_000) 352 | self.ledger.opt_in_asset(fee_collector, self.pool_token_asset_id) 353 | self.ledger.opt_in_asset(APPLICATION_ADDRESS, self.pool_token_asset_id) 354 | 355 | extra = 100_000 356 | self.ledger.move(extra, self.pool_token_asset_id, sender=self.user_addr, receiver=APPLICATION_ADDRESS) 357 | txn_group = self.get_claim_extra_transactions(sender=self.user_addr, asset_id=self.pool_token_asset_id, address=APPLICATION_ADDRESS, fee_collector=fee_collector, app_call_fee=2_000) 358 | txn_group = transaction.assign_group_id(txn_group) 359 | stxns = self.sign_txns(txn_group, self.user_sk) 360 | 361 | block = self.ledger.eval_transactions(stxns) 362 | block_txns = block[b'txns'] 363 | 364 | # outer transactions 365 | self.assertEqual(len(block_txns), 1) 366 | txn = block_txns[0] 367 | self.assertDictEqual( 368 | txn[b'txn'], 369 | { 370 | b'apaa': [b'claim_extra'], 371 | b'apas': [self.pool_token_asset_id], 372 | b'apat': [decode_address(APPLICATION_ADDRESS), decode_address(fee_collector)], 373 | b'apid': APPLICATION_ID, 374 | b'fee': ANY, 375 | b'fv': ANY, 376 | b'grp': ANY, 377 | b'lv': ANY, 378 | b'snd': decode_address(self.user_addr), 379 | b'type': b'appl' 380 | } 381 | ) 382 | 383 | inner_transactions = txn[b'dt'][b'itx'] 384 | self.assertEqual(len(inner_transactions), 1) 385 | 386 | # inner transactions - [0] 387 | self.assertDictEqual( 388 | inner_transactions[0][b'txn'], 389 | { 390 | # b'aamt': extra, 391 | b'arcv': decode_address(fee_collector), 392 | b'fv': self.sp.first, 393 | b'lv': self.sp.last, 394 | b'snd': decode_address(APPLICATION_ADDRESS), 395 | b'type': b'axfer', 396 | b'xaid': self.pool_token_asset_id 397 | }, 398 | ) 399 | self.ledger.get_account_balance(APPLICATION_ADDRESS, self.pool_token_asset_id) 400 | 401 | # There is no extra to claim 402 | txn_group = self.get_claim_extra_transactions(sender=self.user_addr, asset_id=self.pool_token_asset_id, address=APPLICATION_ADDRESS, fee_collector=fee_collector, app_call_fee=2_000) 403 | txn_group = transaction.assign_group_id(txn_group) 404 | stxns = self.sign_txns(txn_group, self.user_sk) 405 | 406 | with self.assertRaises(LogicEvalError) as e: 407 | self.ledger.eval_transactions(stxns) 408 | self.assertEqual(e.exception.source['line'], 'assert(asset_amount)') 409 | 410 | 411 | class TestClaimExtraAlgoPair(BaseTestCase): 412 | @classmethod 413 | def setUpClass(cls): 414 | cls.sp = get_suggested_params() 415 | cls.app_creator_sk, cls.app_creator_address = generate_account() 416 | cls.user_sk, cls.user_addr = generate_account() 417 | cls.asset_1_id = 5 418 | cls.asset_2_id = ALGO_ASSET_ID 419 | 420 | def setUp(self): 421 | self.ledger = JigLedger() 422 | self.create_amm_app() 423 | self.ledger.set_account_balance(self.user_addr, 100_000_000) 424 | self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_1_id) 425 | 426 | self.pool_address, self.pool_token_asset_id = self.bootstrap_pool(self.asset_1_id, self.asset_2_id) 427 | self.ledger.opt_in_asset(self.user_addr, self.pool_token_asset_id) 428 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000, liquidity_provider_address=self.user_addr) 429 | 430 | def test_claim_asset_2_from_pool(self): 431 | fee_collector = self.app_creator_address 432 | self.ledger.set_account_balance(fee_collector, 1_000_000) 433 | 434 | extra = 10_000 435 | self.ledger.move(extra, self.asset_2_id, receiver=self.pool_address) 436 | 437 | txn_group = self.get_claim_extra_transactions(sender=self.user_addr, asset_id=self.asset_2_id, address=self.pool_address, fee_collector=fee_collector, app_call_fee=2_000) 438 | txn_group = transaction.assign_group_id(txn_group) 439 | stxns = self.sign_txns(txn_group, self.user_sk) 440 | 441 | block = self.ledger.eval_transactions(stxns) 442 | block_txns = block[b'txns'] 443 | 444 | # outer transactions 445 | self.assertEqual(len(block_txns), 1) 446 | txn = block_txns[0] 447 | self.assertDictEqual( 448 | txn[b'txn'], 449 | { 450 | b'apaa': [b'claim_extra'], 451 | b'apas': [self.asset_2_id], 452 | b'apat': [decode_address(self.pool_address), decode_address(fee_collector)], 453 | b'apid': APPLICATION_ID, 454 | b'fee': ANY, 455 | b'fv': ANY, 456 | b'grp': ANY, 457 | b'lv': ANY, 458 | b'snd': decode_address(self.user_addr), 459 | b'type': b'appl' 460 | } 461 | ) 462 | 463 | inner_transactions = txn[b'dt'][b'itx'] 464 | self.assertEqual(len(inner_transactions), 1) 465 | 466 | # inner transactions - [0] 467 | self.assertDictEqual( 468 | inner_transactions[0][b'txn'], 469 | { 470 | b'amt': extra, 471 | b'rcv': decode_address(fee_collector), 472 | b'fv': self.sp.first, 473 | b'lv': self.sp.last, 474 | b'snd': decode_address(self.pool_address), 475 | b'type': b'pay', 476 | }, 477 | ) 478 | 479 | # There is no extra to claim 480 | txn_group = self.get_claim_extra_transactions(sender=self.user_addr, asset_id=self.asset_2_id, address=self.pool_address, fee_collector=fee_collector, app_call_fee=2_000) 481 | txn_group = transaction.assign_group_id(txn_group) 482 | stxns = self.sign_txns(txn_group, self.user_sk) 483 | 484 | with self.assertRaises(LogicEvalError) as e: 485 | self.ledger.eval_transactions(stxns) 486 | self.assertEqual(e.exception.source['line'], 'assert(asset_amount)') 487 | -------------------------------------------------------------------------------- /tests/tests_claim_fees.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import ANY 2 | 3 | from algojig import get_suggested_params 4 | from algojig.exceptions import LogicEvalError 5 | from algojig.ledger import JigLedger 6 | from algosdk.account import generate_account 7 | from algosdk.encoding import decode_address 8 | 9 | from .constants import * 10 | from .core import BaseTestCase 11 | 12 | 13 | class TestClaimFees(BaseTestCase): 14 | @classmethod 15 | def setUpClass(cls): 16 | cls.sp = get_suggested_params() 17 | cls.app_creator_sk, cls.app_creator_address = generate_account() 18 | cls.user_sk, cls.user_addr = generate_account() 19 | cls.asset_1_id = 5 20 | cls.asset_2_id = 2 21 | 22 | def setUp(self): 23 | self.ledger = JigLedger() 24 | self.create_amm_app() 25 | self.ledger.set_account_balance(self.user_addr, 1_000_000) 26 | self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_1_id) 27 | self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_2_id) 28 | 29 | self.pool_address, self.pool_token_asset_id = self.bootstrap_pool(self.asset_1_id, self.asset_2_id) 30 | self.ledger.opt_in_asset(self.user_addr, self.pool_token_asset_id) 31 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000, liquidity_provider_address=self.user_addr) 32 | 33 | def test_pass(self): 34 | fee_collector = self.app_creator_address 35 | self.ledger.set_account_balance(fee_collector, 1_000_000) 36 | self.ledger.opt_in_asset(fee_collector, self.asset_1_id) 37 | self.ledger.opt_in_asset(fee_collector, self.asset_2_id) 38 | 39 | asset_1_fee_amount = 5_000 40 | asset_2_fee_amount = 10_000 41 | self.set_pool_protocol_fees(asset_1_fee_amount, asset_2_fee_amount) 42 | 43 | txn_group = self.get_claim_fee_transactions(sender=self.user_addr, fee_collector=fee_collector, app_call_fee=3_000) 44 | stxns = self.sign_txns(txn_group, self.user_sk) 45 | 46 | block = self.ledger.eval_transactions(stxns) 47 | block_txns = block[b'txns'] 48 | 49 | # outer transactions 50 | self.assertEqual(len(block_txns), 1) 51 | txn = block_txns[0] 52 | self.assertDictEqual( 53 | txn[b'txn'], 54 | { 55 | b'apaa': [b'claim_fees'], 56 | b'apas': [self.asset_1_id, self.asset_2_id], 57 | b'apat': [decode_address(self.pool_address), decode_address(fee_collector)], 58 | b'apid': APPLICATION_ID, 59 | b'fee': ANY, 60 | b'fv': ANY, 61 | b'lv': ANY, 62 | b'snd': decode_address(self.user_addr), 63 | b'type': b'appl' 64 | } 65 | ) 66 | 67 | inner_transactions = txn[b'dt'][b'itx'] 68 | self.assertEqual(len(inner_transactions), 2) 69 | 70 | # inner transactions - [0] 71 | self.assertDictEqual( 72 | inner_transactions[0][b'txn'], 73 | { 74 | b'aamt': asset_1_fee_amount, 75 | b'arcv': decode_address(fee_collector), 76 | b'fv': self.sp.first, 77 | b'lv': self.sp.last, 78 | b'snd': decode_address(self.pool_address), 79 | b'type': b'axfer', 80 | b'xaid': self.asset_1_id 81 | }, 82 | ) 83 | 84 | # inner transactions - [1] 85 | self.assertDictEqual( 86 | inner_transactions[1][b'txn'], 87 | { 88 | b'aamt': asset_2_fee_amount, 89 | b'arcv': decode_address(fee_collector), 90 | b'fv': self.sp.first, 91 | b'lv': self.sp.last, 92 | b'snd': decode_address(self.pool_address), 93 | b'type': b'axfer', 94 | b'xaid': self.asset_2_id 95 | }, 96 | ) 97 | 98 | # local state delta 99 | pool_local_state_delta = txn[b'dt'][b'ld'][1] 100 | self.assertDictEqual( 101 | pool_local_state_delta, 102 | { 103 | b'asset_1_protocol_fees': {b'at': 2}, # -> 0 104 | b'asset_2_protocol_fees': {b'at': 2}, # -> 0 105 | } 106 | ) 107 | 108 | def test_pass_only_one_of_the_asset_has_fee(self): 109 | fee_collector = self.app_creator_address 110 | self.ledger.set_account_balance(fee_collector, 1_000_000) 111 | self.ledger.opt_in_asset(fee_collector, self.asset_1_id) 112 | self.ledger.opt_in_asset(fee_collector, self.asset_2_id) 113 | 114 | asset_1_fee_amount = 5_000 115 | asset_2_fee_amount = 0 116 | self.set_pool_protocol_fees(asset_1_fee_amount, asset_2_fee_amount) 117 | 118 | txn_group = self.get_claim_fee_transactions(sender=self.user_addr, fee_collector=fee_collector, app_call_fee=3_000) 119 | stxns = self.sign_txns(txn_group, self.user_sk) 120 | 121 | block = self.ledger.eval_transactions(stxns) 122 | block_txns = block[b'txns'] 123 | 124 | # outer transactions 125 | self.assertEqual(len(block_txns), 1) 126 | 127 | txn = block_txns[0] 128 | inner_transactions = txn[b'dt'][b'itx'] 129 | self.assertEqual(len(inner_transactions), 2) 130 | 131 | # inner transactions - [0] 132 | self.assertDictEqual( 133 | inner_transactions[0][b'txn'], 134 | { 135 | b'aamt': asset_1_fee_amount, 136 | b'arcv': decode_address(fee_collector), 137 | b'fv': self.sp.first, 138 | b'lv': self.sp.last, 139 | b'snd': decode_address(self.pool_address), 140 | b'type': b'axfer', 141 | b'xaid': self.asset_1_id 142 | }, 143 | ) 144 | 145 | # inner transactions - [1] 146 | self.assertDictEqual( 147 | inner_transactions[1][b'txn'], 148 | { 149 | b'arcv': decode_address(fee_collector), 150 | b'fv': self.sp.first, 151 | b'lv': self.sp.last, 152 | b'snd': decode_address(self.pool_address), 153 | b'type': b'axfer', 154 | b'xaid': self.asset_2_id 155 | }, 156 | ) 157 | 158 | # local state delta 159 | pool_local_state_delta = txn[b'dt'][b'ld'][1] 160 | self.assertDictEqual( 161 | pool_local_state_delta, 162 | { 163 | b'asset_1_protocol_fees': {b'at': 2}, # -> 0 164 | } 165 | ) 166 | 167 | def test_fail_there_is_no_fee(self): 168 | fee_collector = self.app_creator_address 169 | self.ledger.set_account_balance(fee_collector, 1_000_000) 170 | self.ledger.opt_in_asset(fee_collector, self.asset_1_id) 171 | self.ledger.opt_in_asset(fee_collector, self.asset_2_id) 172 | 173 | asset_1_fee_amount = 0 174 | asset_2_fee_amount = 0 175 | self.set_pool_protocol_fees(asset_1_fee_amount, asset_2_fee_amount) 176 | 177 | txn_group = self.get_claim_fee_transactions(sender=self.user_addr, fee_collector=fee_collector, app_call_fee=3_000) 178 | stxns = self.sign_txns(txn_group, self.user_sk) 179 | 180 | with self.assertRaises(LogicEvalError) as e: 181 | self.ledger.eval_transactions(stxns) 182 | self.assertEqual(e.exception.source['line'], 'assert(asset_1_protocol_fees || asset_2_protocol_fees)') 183 | 184 | def test_fail_fee_collector_did_not_opt_in(self): 185 | fee_collector = self.app_creator_address 186 | self.ledger.set_account_balance(fee_collector, 1_000_000) 187 | 188 | asset_1_fee_amount = 5_000 189 | asset_2_fee_amount = 10_000 190 | self.set_pool_protocol_fees(asset_1_fee_amount, asset_2_fee_amount) 191 | 192 | txn_group = self.get_claim_fee_transactions(sender=self.user_addr, fee_collector=fee_collector, app_call_fee=3_000) 193 | stxns = self.sign_txns(txn_group, self.user_sk) 194 | 195 | with self.assertRaises(LogicEvalError) as e: 196 | self.ledger.eval_transactions(stxns) 197 | self.assertEqual(e.exception.source['line'], 'inner_txn:') 198 | 199 | 200 | class TestClaimFeesAlgoPair(BaseTestCase): 201 | @classmethod 202 | def setUpClass(cls): 203 | cls.sp = get_suggested_params() 204 | cls.app_creator_sk, cls.app_creator_address = generate_account() 205 | cls.user_sk, cls.user_addr = generate_account() 206 | cls.asset_1_id = 5 207 | cls.asset_2_id = ALGO_ASSET_ID 208 | 209 | def setUp(self): 210 | self.ledger = JigLedger() 211 | self.create_amm_app() 212 | self.ledger.set_account_balance(self.user_addr, 100_000_000) 213 | self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_1_id) 214 | 215 | self.pool_address, self.pool_token_asset_id = self.bootstrap_pool(self.asset_1_id, self.asset_2_id) 216 | self.ledger.opt_in_asset(self.user_addr, self.pool_token_asset_id) 217 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000, liquidity_provider_address=self.user_addr) 218 | 219 | def test_pass(self): 220 | fee_collector = self.app_creator_address 221 | self.ledger.set_account_balance(fee_collector, 1_000_000) 222 | self.ledger.opt_in_asset(fee_collector, self.asset_1_id) 223 | 224 | asset_1_fee_amount = 5_000 225 | asset_2_fee_amount = 10_000 226 | self.set_pool_protocol_fees(asset_1_fee_amount, asset_2_fee_amount) 227 | 228 | txn_group = self.get_claim_fee_transactions(sender=self.user_addr, fee_collector=fee_collector, app_call_fee=3_000) 229 | stxns = self.sign_txns(txn_group, self.user_sk) 230 | 231 | block = self.ledger.eval_transactions(stxns) 232 | block_txns = block[b'txns'] 233 | 234 | # outer transactions 235 | self.assertEqual(len(block_txns), 1) 236 | txn = block_txns[0] 237 | self.assertDictEqual( 238 | txn[b'txn'], 239 | { 240 | b'apaa': [b'claim_fees'], 241 | b'apas': [self.asset_1_id, self.asset_2_id], 242 | b'apat': [decode_address(self.pool_address), decode_address(fee_collector)], 243 | b'apid': APPLICATION_ID, 244 | b'fee': ANY, 245 | b'fv': ANY, 246 | b'lv': ANY, 247 | b'snd': decode_address(self.user_addr), 248 | b'type': b'appl' 249 | } 250 | ) 251 | 252 | inner_transactions = txn[b'dt'][b'itx'] 253 | self.assertEqual(len(inner_transactions), 2) 254 | 255 | # inner transactions - [0] 256 | self.assertDictEqual( 257 | inner_transactions[0][b'txn'], 258 | { 259 | b'aamt': asset_1_fee_amount, 260 | b'arcv': decode_address(fee_collector), 261 | b'fv': self.sp.first, 262 | b'lv': self.sp.last, 263 | b'snd': decode_address(self.pool_address), 264 | b'type': b'axfer', 265 | b'xaid': self.asset_1_id 266 | }, 267 | ) 268 | 269 | # inner transactions - [1] 270 | self.assertDictEqual( 271 | inner_transactions[1][b'txn'], 272 | { 273 | b'amt': asset_2_fee_amount, 274 | b'rcv': decode_address(fee_collector), 275 | b'fv': self.sp.first, 276 | b'lv': self.sp.last, 277 | b'snd': decode_address(self.pool_address), 278 | b'type': b'pay', 279 | }, 280 | ) 281 | 282 | # local state delta 283 | pool_local_state_delta = txn[b'dt'][b'ld'][1] 284 | self.assertDictEqual( 285 | pool_local_state_delta, 286 | { 287 | b'asset_1_protocol_fees': {b'at': 2}, # -> 0 288 | b'asset_2_protocol_fees': {b'at': 2} # -> 0 289 | } 290 | ) 291 | -------------------------------------------------------------------------------- /tests/tests_create_app.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import ANY 2 | 3 | from algojig import get_suggested_params 4 | from algojig.ledger import JigLedger 5 | from algosdk.account import generate_account 6 | from algosdk.encoding import decode_address 7 | from algosdk.future import transaction 8 | 9 | from .constants import APP_GLOBAL_BYTES, APP_GLOBAL_INTS, APP_LOCAL_BYTES, APP_LOCAL_INTS 10 | from .core import BaseTestCase, amm_approval_program, amm_clear_state_program 11 | 12 | 13 | class TestCreateApp(BaseTestCase): 14 | 15 | @classmethod 16 | def setUpClass(cls): 17 | cls.sp = get_suggested_params() 18 | cls.app_creator_sk, cls.app_creator_address = generate_account() 19 | cls.user_sk, cls.user_addr = generate_account() 20 | 21 | def setUp(self): 22 | self.ledger = JigLedger() 23 | self.ledger.set_account_balance(self.app_creator_address, 1_000_000) 24 | 25 | def test_create_app(self): 26 | extra_pages = 3 27 | txn = transaction.ApplicationCreateTxn( 28 | sender=self.app_creator_address, 29 | sp=self.sp, 30 | on_complete=transaction.OnComplete.NoOpOC, 31 | approval_program=amm_approval_program.bytecode, 32 | clear_program=amm_clear_state_program.bytecode, 33 | global_schema=transaction.StateSchema(num_uints=APP_GLOBAL_INTS, num_byte_slices=APP_GLOBAL_BYTES), 34 | local_schema=transaction.StateSchema(num_uints=APP_LOCAL_INTS, num_byte_slices=APP_LOCAL_BYTES), 35 | extra_pages=extra_pages, 36 | ) 37 | stxn = txn.sign(self.app_creator_sk) 38 | 39 | block = self.ledger.eval_transactions(transactions=[stxn]) 40 | block_txns = block[b'txns'] 41 | 42 | self.assertAlmostEqual(len(block_txns), 1) 43 | txn = block_txns[0] 44 | self.assertTrue(txn[b'apid'] > 0) 45 | self.assertDictEqual( 46 | txn[b'txn'], 47 | { 48 | b'apap': amm_approval_program.bytecode, 49 | b'apep': extra_pages, 50 | b'apgs': ANY, 51 | b'apls': ANY, 52 | b'apsu': amm_clear_state_program.bytecode, 53 | b'fee': self.sp.fee, 54 | b'fv': self.sp.first, 55 | b'lv': self.sp.last, 56 | b'snd': decode_address(self.app_creator_address), 57 | b'type': b'appl' 58 | } 59 | ) 60 | 61 | self.assertDictEqual( 62 | txn[b'dt'][b'gd'], 63 | { 64 | b'fee_collector': {b'at': 1, b'bs': decode_address(self.app_creator_address)}, 65 | b'fee_manager': {b'at': 1, b'bs': decode_address(self.app_creator_address)}, 66 | b'fee_setter': {b'at': 1, b'bs': decode_address(self.app_creator_address)}, 67 | } 68 | ) 69 | -------------------------------------------------------------------------------- /tests/tests_price_oracle.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from unittest.mock import ANY 3 | from zoneinfo import ZoneInfo 4 | 5 | from algojig import get_suggested_params 6 | from algojig.ledger import JigLedger 7 | from algosdk.account import generate_account 8 | from algosdk.encoding import decode_address 9 | from algosdk.future import transaction 10 | 11 | from .constants import * 12 | from .core import BaseTestCase 13 | from .utils import int_to_bytes_without_zero_padding 14 | 15 | price_oracle_reader_program = TealishProgram('tests/price_oracle_reader.tl') 16 | PRICE_ORACLE_READER_APP_ID = 10 17 | 18 | 19 | class TestPriceOracle(BaseTestCase): 20 | 21 | @classmethod 22 | def setUpClass(cls): 23 | cls.sp = get_suggested_params() 24 | cls.app_creator_sk, cls.app_creator_address = generate_account() 25 | cls.user_sk, cls.user_addr = generate_account() 26 | cls.asset_1_id = 5 27 | cls.asset_2_id = 2 28 | 29 | def setUp(self): 30 | self.ledger = JigLedger() 31 | self.create_amm_app() 32 | self.ledger.set_account_balance(self.user_addr, 1_000_000) 33 | self.ledger.set_account_balance(self.user_addr, 1_000_000, asset_id=self.asset_1_id) 34 | self.ledger.set_account_balance(self.user_addr, 0, asset_id=self.asset_2_id) 35 | 36 | self.pool_address, self.pool_token_asset_id = self.bootstrap_pool(self.asset_1_id, self.asset_2_id) 37 | 38 | def test_overflow(self): 39 | bootstrap_datetime = datetime(year=2022, month=1, day=1, tzinfo=ZoneInfo("UTC")) 40 | two_hundred_years_later = datetime(year=2222, month=1, day=1, tzinfo=ZoneInfo("UTC")) 41 | self.assertEqual(two_hundred_years_later.year, 2222) 42 | 43 | # Maximum possible price 44 | asset_1_reserves = 1 45 | asset_2_reserves = MAX_ASSET_AMOUNT 46 | 47 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves, asset_2_reserves) 48 | self.ledger.update_local_state(address=self.pool_address, app_id=APPLICATION_ID, state_delta={b'cumulative_price_update_timestamp': int(bootstrap_datetime.timestamp())}) 49 | 50 | min_output = 0 51 | txn_group = [ 52 | transaction.AssetTransferTxn( 53 | sender=self.user_addr, 54 | sp=self.sp, 55 | receiver=self.pool_address, 56 | index=self.asset_1_id, 57 | amt=334, 58 | ), 59 | transaction.ApplicationNoOpTxn( 60 | sender=self.user_addr, 61 | sp=self.sp, 62 | index=APPLICATION_ID, 63 | app_args=[METHOD_SWAP, "fixed-input", min_output], 64 | foreign_assets=[self.asset_1_id, self.asset_2_id], 65 | accounts=[self.pool_address], 66 | ) 67 | ] 68 | txn_group[1].fee = 2000 69 | txn_group = transaction.assign_group_id(txn_group) 70 | stxns = [ 71 | txn_group[0].sign(self.user_sk), 72 | txn_group[1].sign(self.user_sk) 73 | ] 74 | block = self.ledger.eval_transactions(stxns, block_timestamp=int(two_hundred_years_later.timestamp())) 75 | block_txns = block[b'txns'] 76 | 77 | # outer transactions 78 | self.assertEqual(len(block_txns), 2) 79 | 80 | # outer transactions - [1] 81 | txn = block_txns[1] 82 | # local state delta 83 | pool_local_state_delta = txn[b'dt'][b'ld'][1] 84 | asset_1_cumulative_price = asset_2_reserves * PRICE_SCALE_FACTOR * (int(two_hundred_years_later.timestamp()) - int(bootstrap_datetime.timestamp())) // asset_1_reserves 85 | asset_2_cumulative_price = asset_1_reserves * PRICE_SCALE_FACTOR * (int(two_hundred_years_later.timestamp()) - int(bootstrap_datetime.timestamp())) // asset_2_reserves 86 | 87 | self.assertDictEqual( 88 | pool_local_state_delta, 89 | { 90 | b'asset_1_reserves': {b'at': 2, b'ui': 335}, 91 | b'asset_2_reserves': {b'at': 2, b'ui': 55229772675777101}, 92 | b'asset_1_cumulative_price': {b'at': 1, b'bs': int_to_bytes_without_zero_padding(asset_1_cumulative_price)}, 93 | b'asset_2_cumulative_price': {b'at': 1, b'bs': int_to_bytes_without_zero_padding(asset_2_cumulative_price)}, 94 | b'cumulative_price_update_timestamp': {b'at': 2, b'ui': int(two_hundred_years_later.timestamp())}, 95 | } 96 | ) 97 | self.assertEqual(asset_1_cumulative_price, 2147640163675837592635447824606866120216936448000) 98 | self.assertEqual(asset_2_cumulative_price, 6311347200) 99 | 100 | time_delta = int(two_hundred_years_later.timestamp()) - int(bootstrap_datetime.timestamp()) 101 | self.assertEqual(asset_2_reserves / asset_1_reserves, asset_1_cumulative_price / time_delta / PRICE_SCALE_FACTOR) 102 | self.assertEqual(asset_1_reserves / asset_2_reserves, asset_2_cumulative_price / time_delta / PRICE_SCALE_FACTOR) 103 | 104 | def test_updated_once_in_a_block(self): 105 | one_day = timedelta(days=1) 106 | bootstrap_datetime = datetime(year=2022, month=1, day=1, tzinfo=ZoneInfo("UTC")) 107 | last_update_datetime = bootstrap_datetime + one_day 108 | new_block_datetime = last_update_datetime + one_day 109 | 110 | # Random initial cumulative prices 111 | asset_1_cumulative_price = 2 * PRICE_SCALE_FACTOR * int(timedelta(days=7).total_seconds()) 112 | asset_2_cumulative_price = 3 * PRICE_SCALE_FACTOR * int(timedelta(days=7).total_seconds()) 113 | 114 | asset_1_reserves = 12_345 115 | asset_2_reserves = 29_876 116 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves, asset_2_reserves) 117 | self.ledger.update_local_state( 118 | address=self.pool_address, 119 | app_id=APPLICATION_ID, 120 | state_delta={ 121 | b'asset_1_cumulative_price': int_to_bytes_without_zero_padding(asset_1_cumulative_price), 122 | b'asset_2_cumulative_price': int_to_bytes_without_zero_padding(asset_2_cumulative_price), 123 | b'cumulative_price_update_timestamp': int(last_update_datetime.timestamp()) 124 | } 125 | ) 126 | 127 | new_asset_1_cumulative_price = asset_1_cumulative_price + (asset_2_reserves * PRICE_SCALE_FACTOR * int(one_day.total_seconds()) // asset_1_reserves) 128 | new_asset_2_cumulative_price = asset_2_cumulative_price + (asset_1_reserves * PRICE_SCALE_FACTOR * int(one_day.total_seconds()) // asset_2_reserves) 129 | 130 | min_output = 0 131 | txn_group_1 = [ 132 | transaction.AssetTransferTxn( 133 | sender=self.user_addr, 134 | sp=self.sp, 135 | receiver=self.pool_address, 136 | index=self.asset_1_id, 137 | amt=334, 138 | ), 139 | transaction.ApplicationNoOpTxn( 140 | sender=self.user_addr, 141 | sp=self.sp, 142 | index=APPLICATION_ID, 143 | app_args=[METHOD_SWAP, "fixed-input", min_output], 144 | foreign_assets=[self.asset_1_id, self.asset_2_id], 145 | accounts=[self.pool_address], 146 | ) 147 | ] 148 | txn_group_1[1].fee = 2000 149 | txn_group_1 = transaction.assign_group_id(txn_group_1) 150 | 151 | txn_group_2 = [ 152 | transaction.AssetTransferTxn( 153 | sender=self.user_addr, 154 | sp=self.sp, 155 | receiver=self.pool_address, 156 | index=self.asset_2_id, 157 | amt=334, 158 | ), 159 | transaction.ApplicationNoOpTxn( 160 | sender=self.user_addr, 161 | sp=self.sp, 162 | index=APPLICATION_ID, 163 | app_args=[METHOD_SWAP, "fixed-input", min_output], 164 | foreign_assets=[self.asset_1_id, self.asset_2_id], 165 | accounts=[self.pool_address], 166 | ) 167 | ] 168 | txn_group_2[1].fee = 2000 169 | txn_group_2 = transaction.assign_group_id(txn_group_2) 170 | 171 | stxns = [ 172 | txn_group_1[0].sign(self.user_sk), 173 | txn_group_1[1].sign(self.user_sk), 174 | txn_group_2[0].sign(self.user_sk), 175 | txn_group_2[1].sign(self.user_sk), 176 | ] 177 | block = self.ledger.eval_transactions(stxns, block_timestamp=int(new_block_datetime.timestamp())) 178 | block_txns = block[b'txns'] 179 | 180 | # outer transactions 181 | self.assertEqual(len(block_txns), 4) 182 | 183 | # outer transactions - [1] 184 | txn = block_txns[1] 185 | # local state delta 186 | pool_local_state_delta = txn[b'dt'][b'ld'][1] 187 | self.assertDictEqual( 188 | pool_local_state_delta, 189 | { 190 | b'asset_1_reserves': ANY, 191 | b'asset_2_reserves': ANY, 192 | b'asset_1_cumulative_price': {b'at': 1, b'bs': int_to_bytes_without_zero_padding(new_asset_1_cumulative_price)}, 193 | b'asset_2_cumulative_price': {b'at': 1, b'bs': int_to_bytes_without_zero_padding(new_asset_2_cumulative_price)}, 194 | b'cumulative_price_update_timestamp': {b'at': 2, b'ui': int(new_block_datetime.timestamp())}, 195 | } 196 | ) 197 | 198 | # Cumulative prices are not updated with second swap 199 | # outer transactions - [3] 200 | txn = block_txns[3] 201 | # local state delta 202 | pool_local_state_delta = txn[b'dt'][b'ld'][1] 203 | self.assertDictEqual( 204 | pool_local_state_delta, 205 | { 206 | b'asset_1_reserves': ANY, 207 | b'asset_2_reserves': ANY, 208 | } 209 | ) 210 | 211 | def test_read_price(self): 212 | """ 213 | A dummy app reads the local state of the pool and calculates the price. 214 | """ 215 | self.ledger.create_app(app_id=PRICE_ORACLE_READER_APP_ID, approval_program=price_oracle_reader_program) 216 | asset_1_reserves = asset_2_reserves = 1_000_000 217 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves, asset_2_reserves) 218 | 219 | byte_pool_address = decode_address(self.pool_address) 220 | 221 | min_output = 0 222 | txn_group = [ 223 | transaction.AssetTransferTxn( 224 | sender=self.user_addr, 225 | sp=self.sp, 226 | receiver=self.pool_address, 227 | index=self.asset_1_id, 228 | amt=334, 229 | ), 230 | transaction.ApplicationNoOpTxn( 231 | sender=self.user_addr, 232 | sp=self.sp, 233 | index=APPLICATION_ID, 234 | app_args=[METHOD_SWAP, "fixed-input", min_output], 235 | foreign_assets=[self.asset_1_id, self.asset_2_id], 236 | accounts=[self.pool_address], 237 | ) 238 | ] 239 | txn_group[1].fee = 2000 240 | 241 | txn_group = transaction.assign_group_id(txn_group) 242 | stxns = [ 243 | txn_group[0].sign(self.user_sk), 244 | txn_group[1].sign(self.user_sk), 245 | transaction.ApplicationNoOpTxn( 246 | sender=self.user_addr, 247 | sp=self.sp, 248 | index=PRICE_ORACLE_READER_APP_ID, 249 | foreign_apps=[APPLICATION_ID], 250 | accounts=[self.pool_address], 251 | ).sign(self.user_sk) 252 | ] 253 | 254 | block = self.ledger.eval_transactions(stxns, block_timestamp=2000) 255 | block_txns = block[b'txns'] 256 | self.assertDictEqual( 257 | block_txns[2][b'dt'][b'gd'], 258 | { 259 | byte_pool_address + b'_asset_1_cumulative_price': {b'at': 1, b'bs': int_to_bytes_without_zero_padding(1 * 2000 * PRICE_SCALE_FACTOR)}, 260 | byte_pool_address + b'_asset_2_cumulative_price': {b'at': 1, b'bs': int_to_bytes_without_zero_padding(1 * 2000 * PRICE_SCALE_FACTOR)}, 261 | byte_pool_address + b'_price_update_timestamp': {b'at': 2, b'ui': 2000} 262 | } 263 | ) 264 | 265 | block = self.ledger.eval_transactions(stxns, block_timestamp=3000) 266 | block_txns = block[b'txns'] 267 | self.assertDictEqual( 268 | block_txns[2][b'dt'][b'gd'], 269 | { 270 | byte_pool_address + b'_asset_1_price': {b'at': 1, b'bs': ANY}, 271 | byte_pool_address + b'_asset_2_price': {b'at': 1, b'bs': ANY}, 272 | byte_pool_address + b'_asset_1_cumulative_price': {b'at': 1, b'bs': ANY}, 273 | byte_pool_address + b'_asset_2_cumulative_price': {b'at': 1, b'bs': ANY}, 274 | byte_pool_address + b'_price_update_timestamp': {b'at': 2, b'ui': 3000} 275 | } 276 | ) 277 | self.assertEqual(int.from_bytes(block_txns[2][b'dt'][b'gd'][byte_pool_address + b'_asset_1_price'][b'bs'], "big"), 18434462644153932631) 278 | self.assertEqual(int.from_bytes(block_txns[2][b'dt'][b'gd'][byte_pool_address + b'_asset_2_price'][b'bs'], "big"), 18459033685413727963) 279 | self.assertAlmostEqual(int.from_bytes(block_txns[2][b'dt'][b'gd'][byte_pool_address + b'_asset_1_price'][b'bs'], "big") / PRICE_SCALE_FACTOR, 0.9999, delta=0.001) 280 | self.assertAlmostEqual(int.from_bytes(block_txns[2][b'dt'][b'gd'][byte_pool_address + b'_asset_2_price'][b'bs'], "big") / PRICE_SCALE_FACTOR, 1.0000, delta=0.001) 281 | self.assertEqual(int.from_bytes(block_txns[2][b'dt'][b'gd'][byte_pool_address + b'_asset_1_cumulative_price'][b'bs'], "big"), 55327950791573035863364) 282 | self.assertEqual(int.from_bytes(block_txns[2][b'dt'][b'gd'][byte_pool_address + b'_asset_2_cumulative_price'][b'bs'], "big"), 55352521832832831195923) 283 | -------------------------------------------------------------------------------- /tests/tests_remove_liquidity.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import ANY 2 | 3 | from algojig import get_suggested_params 4 | from algojig.exceptions import LogicEvalError 5 | from algojig.ledger import JigLedger 6 | from algosdk.account import generate_account 7 | from algosdk.encoding import decode_address 8 | from algosdk.future import transaction 9 | 10 | from .constants import * 11 | from .core import BaseTestCase 12 | from .utils import itob 13 | 14 | 15 | class TestRemoveLiquidity(BaseTestCase): 16 | 17 | @classmethod 18 | def setUpClass(cls): 19 | cls.sp = get_suggested_params() 20 | cls.app_creator_sk, cls.app_creator_address = generate_account() 21 | cls.user_sk, cls.user_addr = generate_account() 22 | cls.asset_1_id = 5 23 | cls.asset_2_id = 2 24 | 25 | def reset_ledger(self): 26 | self.ledger = JigLedger() 27 | self.create_amm_app() 28 | self.ledger.set_account_balance(self.user_addr, 1_000_000) 29 | self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_1_id) 30 | self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_2_id) 31 | 32 | self.pool_address, self.pool_token_asset_id = self.bootstrap_pool(self.asset_1_id, self.asset_2_id) 33 | self.ledger.opt_in_asset(self.user_addr, self.pool_token_asset_id) 34 | 35 | def setUp(self): 36 | self.reset_ledger() 37 | 38 | def test_remove_liquidity(self): 39 | test_cases = [ 40 | dict( 41 | msg="Test basic remove liquidity.", 42 | initials=dict( 43 | asset_1_reserves=1_000_000, 44 | asset_2_reserves=1_000_000, 45 | issued_pool_token_amount=1_000_000, 46 | ), 47 | inputs=dict( 48 | removed_pool_token_amount=5_000, 49 | ), 50 | outputs=dict( 51 | asset_1_out=5_000, 52 | asset_2_out=5_000, 53 | local_state_delta={ 54 | b'asset_1_cumulative_price': ANY, 55 | b'asset_1_reserves': {b'at': 2, b'ui': 1_000_000 - 5_000}, 56 | b'asset_2_reserves': {b'at': 2, b'ui': 1_000_000 - 5_000}, 57 | b'asset_2_cumulative_price': ANY, 58 | b'cumulative_price_update_timestamp': ANY, 59 | b'issued_pool_tokens': {b'at': 2, b'ui': 1_000_000 - 5_000}, 60 | } 61 | ) 62 | ), 63 | dict( 64 | msg="Test removing 0 pool token.", 65 | initials=dict( 66 | asset_1_reserves=1_000_000, 67 | asset_2_reserves=1_000_000, 68 | issued_pool_token_amount=1_000_000, 69 | ), 70 | inputs=dict( 71 | removed_pool_token_amount=0, 72 | ), 73 | exception=dict( 74 | source_line='assert(removed_pool_token_amount)' 75 | ) 76 | ), 77 | dict( 78 | msg="One of the asset out is 0 and asset out amounts are rounded down.", 79 | initials=dict( 80 | asset_1_reserves=100_000_000, 81 | asset_2_reserves=1, 82 | issued_pool_token_amount=10_000, 83 | ), 84 | inputs=dict( 85 | removed_pool_token_amount=500, 86 | ), 87 | exception=dict( 88 | source_line='assert(asset_1_amount && asset_2_amount)' 89 | ) 90 | ), 91 | dict( 92 | msg="Remove mistakenly added NFT (Remove all circulating pool tokens).", 93 | initials=dict( 94 | asset_1_reserves=100_000_000, 95 | asset_2_reserves=1, 96 | issued_pool_token_amount=10_000, 97 | ), 98 | inputs=dict( 99 | removed_pool_token_amount=10_000 - LOCKED_POOL_TOKENS, 100 | ), 101 | outputs=dict( 102 | asset_1_out=100_000_000, 103 | asset_2_out=1, 104 | local_state_delta={ 105 | b'asset_1_cumulative_price': ANY, 106 | b'asset_1_reserves': {b'at': 2}, 107 | b'asset_2_reserves': {b'at': 2}, 108 | b'asset_2_cumulative_price': ANY, 109 | b'cumulative_price_update_timestamp': ANY, 110 | b'issued_pool_tokens': {b'at': 2}, 111 | } 112 | ) 113 | ), 114 | dict( 115 | msg="Remove 0 pool token.", 116 | initials=dict( 117 | asset_1_reserves=10_000_000, 118 | asset_2_reserves=10_000_000, 119 | issued_pool_token_amount=10_000_000, 120 | ), 121 | inputs=dict( 122 | removed_pool_token_amount=0, 123 | ), 124 | exception=dict( 125 | source_line='assert(removed_pool_token_amount)' 126 | ) 127 | ) 128 | ] 129 | 130 | for test_case in test_cases: 131 | with self.subTest(**test_case): 132 | initials = test_case["initials"] 133 | inputs = test_case["inputs"] 134 | 135 | self.reset_ledger() 136 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=initials["asset_1_reserves"], asset_2_reserves=initials["asset_2_reserves"], liquidity_provider_address=self.user_addr) 137 | self.assertEqual(initials["issued_pool_token_amount"], self.ledger.accounts[self.pool_address]['local_states'][APPLICATION_ID][b'issued_pool_tokens']) 138 | 139 | txn_group = self.get_remove_liquidity_transactions(liquidity_asset_amount=inputs["removed_pool_token_amount"], app_call_fee=3_000) 140 | txn_group = transaction.assign_group_id(txn_group) 141 | stxns = self.sign_txns(txn_group, self.user_sk) 142 | 143 | if exception := test_case.get("exception"): 144 | with self.assertRaises(LogicEvalError) as e: 145 | self.ledger.eval_transactions(stxns) 146 | 147 | self.assertEqual(e.exception.source['line'], exception.get("source_line")) 148 | 149 | else: 150 | outputs = test_case["outputs"] 151 | 152 | block = self.ledger.eval_transactions(stxns) 153 | block_txns = block[b'txns'] 154 | 155 | # outer transactions 156 | self.assertEqual(len(block_txns), 2) 157 | 158 | # outer transactions [0] 159 | txn = block_txns[0] 160 | self.assertEqual( 161 | txn[b'txn'], 162 | { 163 | b'aamt': inputs["removed_pool_token_amount"], 164 | b'arcv': decode_address(self.pool_address), 165 | b'fee': self.sp.fee, 166 | b'fv': self.sp.first, 167 | b'grp': ANY, 168 | b'lv': self.sp.last, 169 | b'snd': decode_address(self.user_addr), 170 | b'type': b'axfer', 171 | b'xaid': self.pool_token_asset_id 172 | } 173 | ) 174 | 175 | # outer transactions [1] 176 | txn = block_txns[1] 177 | self.assertEqual( 178 | txn[b'txn'], 179 | { 180 | b'apaa': [b'remove_liquidity', itob(0), itob(0)], 181 | b'apas': [self.asset_1_id, self.asset_2_id], 182 | b'apat': [decode_address(self.pool_address)], 183 | b'apid': APPLICATION_ID, 184 | b'fee': self.sp.fee * 3, 185 | b'fv': self.sp.first, 186 | b'grp': ANY, 187 | b'lv': self.sp.last, 188 | b'snd': decode_address(self.user_addr), 189 | b'type': b'appl' 190 | } 191 | ) 192 | 193 | # inner transactions 194 | inner_transactions = txn[b'dt'][b'itx'] 195 | self.assertEqual(len(inner_transactions), 2) 196 | 197 | # inner transactions - [0] 198 | self.assertDictEqual( 199 | inner_transactions[0][b'txn'], 200 | { 201 | b'aamt': outputs["asset_1_out"], 202 | b'arcv': decode_address(self.user_addr), 203 | b'fv': self.sp.first, 204 | b'lv': self.sp.last, 205 | b'snd': decode_address(self.pool_address), 206 | b'type': b'axfer', 207 | b'xaid': self.asset_1_id 208 | } 209 | ) 210 | 211 | # inner transactions - [1] 212 | self.assertDictEqual( 213 | inner_transactions[1][b'txn'], 214 | { 215 | b'aamt': outputs["asset_2_out"], 216 | b'arcv': decode_address(self.user_addr), 217 | b'fv': self.sp.first, 218 | b'lv': self.sp.last, 219 | b'snd': decode_address(self.pool_address), 220 | b'type': b'axfer', 221 | b'xaid': self.asset_2_id 222 | } 223 | ) 224 | 225 | # local state delta 226 | pool_local_state_delta = txn[b'dt'][b'ld'][1] 227 | self.assertDictEqual(pool_local_state_delta, outputs["local_state_delta"]) 228 | 229 | def test_fail_asset_receiver_is_not_the_pool(self): 230 | asset_1_reserves = 1_000_000 231 | asset_2_reserves = 1_000_000 232 | removed_pool_token_amount = 5_000 233 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=asset_1_reserves, asset_2_reserves=asset_2_reserves, liquidity_provider_address=self.user_addr) 234 | 235 | txn_group = self.get_remove_liquidity_transactions(liquidity_asset_amount=removed_pool_token_amount, app_call_fee=3_000) 236 | txn_group[0].receiver = self.user_addr 237 | txn_group = transaction.assign_group_id(txn_group) 238 | stxns = self.sign_txns(txn_group, self.user_sk) 239 | 240 | with self.assertRaises(LogicEvalError) as e: 241 | self.ledger.eval_transactions(stxns) 242 | self.assertEqual(e.exception.source['line'], "assert(Gtxn[pool_token_txn_index].AssetReceiver == pool_address)") 243 | 244 | def test_fail_wrong_asset_id(self): 245 | asset_1_reserves = 1_000_000 246 | asset_2_reserves = 1_000_000 247 | removed_pool_token_amount = 5_000 248 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=asset_1_reserves, asset_2_reserves=asset_2_reserves, liquidity_provider_address=self.user_addr) 249 | 250 | txn_group = self.get_remove_liquidity_transactions(liquidity_asset_amount=removed_pool_token_amount, app_call_fee=3_000) 251 | txn_group[0].index = self.asset_1_id 252 | txn_group = transaction.assign_group_id(txn_group) 253 | stxns = self.sign_txns(txn_group, self.user_sk) 254 | 255 | with self.assertRaises(LogicEvalError) as e: 256 | self.ledger.eval_transactions(stxns) 257 | self.assertEqual(e.exception.source['line'], "assert(Gtxn[pool_token_txn_index].XferAsset == pool_token_asset_id)") 258 | 259 | def test_remove_liquidity_asset_1(self): 260 | test_cases = [ 261 | dict( 262 | msg="Test basic remove liquidity.", 263 | initials=dict( 264 | asset_1_reserves=1_000_000, 265 | asset_2_reserves=1_000_000, 266 | issued_pool_token_amount=1_000_000, 267 | ), 268 | inputs=dict( 269 | removed_pool_token_amount=5_000, 270 | ), 271 | outputs=dict( 272 | asset_1_out=9960, 273 | local_state_delta={ 274 | b'asset_1_cumulative_price': ANY, 275 | b'asset_1_reserves': {b'at': 2, b'ui': 1_000_000 - 9960}, 276 | b'asset_2_protocol_fees': {b'at': 2, b'ui': 2}, 277 | b'asset_2_reserves': {b'at': 2, b'ui': 1_000_000 - 2}, 278 | b'asset_2_cumulative_price': ANY, 279 | b'cumulative_price_update_timestamp': ANY, 280 | b'issued_pool_tokens': {b'at': 2, b'ui': 1_000_000 - 5_000}, 281 | } 282 | ) 283 | ), 284 | dict( 285 | msg="Test removing 0 pool token.", 286 | initials=dict( 287 | asset_1_reserves=1_000_000, 288 | asset_2_reserves=1_000_000, 289 | issued_pool_token_amount=1_000_000, 290 | ), 291 | inputs=dict( 292 | removed_pool_token_amount=0, 293 | ), 294 | exception=dict( 295 | source_line='assert(removed_pool_token_amount)' 296 | ) 297 | ), 298 | dict( 299 | msg="Test removing all pool tokens. It should fail because the swap will be impossible.", 300 | initials=dict( 301 | asset_1_reserves=1_000_000, 302 | asset_2_reserves=1_000_000, 303 | issued_pool_token_amount=1_000_000, 304 | ), 305 | inputs=dict( 306 | removed_pool_token_amount=999_000, 307 | ), 308 | exception=dict( 309 | source_line='assert(issued_pool_tokens > 0)' 310 | ) 311 | ), 312 | ] 313 | 314 | for test_case in test_cases: 315 | with self.subTest(**test_case): 316 | initials = test_case["initials"] 317 | inputs = test_case["inputs"] 318 | 319 | self.reset_ledger() 320 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=initials["asset_1_reserves"], asset_2_reserves=initials["asset_2_reserves"], liquidity_provider_address=self.user_addr) 321 | self.assertEqual(initials["issued_pool_token_amount"], self.ledger.accounts[self.pool_address]['local_states'][APPLICATION_ID][b'issued_pool_tokens']) 322 | 323 | txn_group = self.get_remove_liquidity_single_transactions(liquidity_asset_amount=inputs["removed_pool_token_amount"], asset_id=self.asset_1_id, app_call_fee=3_000) 324 | txn_group = transaction.assign_group_id(txn_group) 325 | stxns = self.sign_txns(txn_group, self.user_sk) 326 | 327 | if exception := test_case.get("exception"): 328 | with self.assertRaises(LogicEvalError) as e: 329 | self.ledger.eval_transactions(stxns) 330 | self.assertEqual(e.exception.source['line'], exception.get("source_line")) 331 | 332 | else: 333 | outputs = test_case["outputs"] 334 | 335 | block = self.ledger.eval_transactions(stxns) 336 | block_txns = block[b'txns'] 337 | 338 | # outer transactions 339 | self.assertEqual(len(block_txns), 2) 340 | 341 | # outer transactions [0] 342 | txn = block_txns[0] 343 | self.assertEqual( 344 | txn[b'txn'], 345 | { 346 | b'aamt': inputs["removed_pool_token_amount"], 347 | b'arcv': decode_address(self.pool_address), 348 | b'fee': self.sp.fee, 349 | b'fv': self.sp.first, 350 | b'grp': ANY, 351 | b'lv': self.sp.last, 352 | b'snd': decode_address(self.user_addr), 353 | b'type': b'axfer', 354 | b'xaid': self.pool_token_asset_id 355 | } 356 | ) 357 | 358 | # outer transactions [1] 359 | txn = block_txns[1] 360 | self.assertEqual( 361 | txn[b'txn'], 362 | { 363 | b'apaa': [b'remove_liquidity', itob(0), itob(0)], 364 | b'apas': [self.asset_1_id], 365 | b'apat': [decode_address(self.pool_address)], 366 | b'apid': APPLICATION_ID, 367 | b'fee': self.sp.fee * 3, 368 | b'fv': self.sp.first, 369 | b'grp': ANY, 370 | b'lv': self.sp.last, 371 | b'snd': decode_address(self.user_addr), 372 | b'type': b'appl' 373 | } 374 | ) 375 | 376 | # inner transactions 377 | inner_transactions = txn[b'dt'][b'itx'] 378 | self.assertEqual(len(inner_transactions), 2) 379 | 380 | # inner transactions - [1] 381 | self.assertDictEqual( 382 | inner_transactions[1][b'txn'], 383 | { 384 | b'aamt': outputs["asset_1_out"], 385 | b'arcv': decode_address(self.user_addr), 386 | b'fv': self.sp.first, 387 | b'lv': self.sp.last, 388 | b'snd': decode_address(self.pool_address), 389 | b'type': b'axfer', 390 | b'xaid': self.asset_1_id 391 | } 392 | ) 393 | 394 | # local state delta 395 | pool_local_state_delta = txn[b'dt'][b'ld'][1] 396 | self.assertDictEqual(pool_local_state_delta, outputs["local_state_delta"]) 397 | 398 | def test_remove_liquidity_asset_2(self): 399 | test_cases = [ 400 | dict( 401 | msg="Test basic remove liquidity.", 402 | initials=dict( 403 | asset_1_reserves=1_000_000, 404 | asset_2_reserves=1_000_000, 405 | issued_pool_token_amount=1_000_000, 406 | ), 407 | inputs=dict( 408 | removed_pool_token_amount=5_000, 409 | ), 410 | outputs=dict( 411 | asset_2_out=9960, 412 | local_state_delta={ 413 | b'asset_1_cumulative_price': ANY, 414 | b'asset_1_protocol_fees': {b'at': 2, b'ui': 2}, 415 | b'asset_1_reserves': {b'at': 2, b'ui': 1_000_000 - 2}, 416 | b'asset_2_reserves': {b'at': 2, b'ui': 1_000_000 - 9960}, 417 | b'asset_2_cumulative_price': ANY, 418 | b'cumulative_price_update_timestamp': ANY, 419 | b'issued_pool_tokens': {b'at': 2, b'ui': 1_000_000 - 5_000}, 420 | } 421 | ) 422 | ), 423 | dict( 424 | msg="Test removing 0 pool token.", 425 | initials=dict( 426 | asset_1_reserves=1_000_000, 427 | asset_2_reserves=1_000_000, 428 | issued_pool_token_amount=1_000_000, 429 | ), 430 | inputs=dict( 431 | removed_pool_token_amount=0, 432 | ), 433 | exception=dict( 434 | source_line='assert(removed_pool_token_amount)' 435 | ) 436 | ), 437 | dict( 438 | msg="Test removing all pool tokens. It should fail because the swap will be impossible.", 439 | initials=dict( 440 | asset_1_reserves=1_000_000, 441 | asset_2_reserves=1_000_000, 442 | issued_pool_token_amount=1_000_000, 443 | ), 444 | inputs=dict( 445 | removed_pool_token_amount=999_000, 446 | ), 447 | exception=dict( 448 | source_line='assert(issued_pool_tokens > 0)' 449 | ) 450 | ), 451 | ] 452 | 453 | for test_case in test_cases: 454 | with self.subTest(**test_case): 455 | initials = test_case["initials"] 456 | inputs = test_case["inputs"] 457 | 458 | self.reset_ledger() 459 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=initials["asset_1_reserves"], asset_2_reserves=initials["asset_2_reserves"], liquidity_provider_address=self.user_addr) 460 | self.assertEqual(initials["issued_pool_token_amount"], self.ledger.accounts[self.pool_address]['local_states'][APPLICATION_ID][b'issued_pool_tokens']) 461 | 462 | txn_group = self.get_remove_liquidity_single_transactions(liquidity_asset_amount=inputs["removed_pool_token_amount"], asset_id=self.asset_2_id, app_call_fee=3_000) 463 | txn_group = transaction.assign_group_id(txn_group) 464 | stxns = self.sign_txns(txn_group, self.user_sk) 465 | 466 | if exception := test_case.get("exception"): 467 | with self.assertRaises(LogicEvalError) as e: 468 | self.ledger.eval_transactions(stxns) 469 | 470 | self.assertEqual(e.exception.source['line'], exception.get("source_line")) 471 | 472 | else: 473 | outputs = test_case["outputs"] 474 | 475 | block = self.ledger.eval_transactions(stxns) 476 | block_txns = block[b'txns'] 477 | 478 | # outer transactions 479 | self.assertEqual(len(block_txns), 2) 480 | 481 | # outer transactions [0] 482 | txn = block_txns[0] 483 | self.assertEqual( 484 | txn[b'txn'], 485 | { 486 | b'aamt': inputs["removed_pool_token_amount"], 487 | b'arcv': decode_address(self.pool_address), 488 | b'fee': self.sp.fee, 489 | b'fv': self.sp.first, 490 | b'grp': ANY, 491 | b'lv': self.sp.last, 492 | b'snd': decode_address(self.user_addr), 493 | b'type': b'axfer', 494 | b'xaid': self.pool_token_asset_id 495 | } 496 | ) 497 | 498 | # outer transactions [1] 499 | txn = block_txns[1] 500 | self.assertEqual( 501 | txn[b'txn'], 502 | { 503 | b'apaa': [b'remove_liquidity', itob(0), itob(0)], 504 | b'apas': [self.asset_2_id], 505 | b'apat': [decode_address(self.pool_address)], 506 | b'apid': APPLICATION_ID, 507 | b'fee': self.sp.fee * 3, 508 | b'fv': self.sp.first, 509 | b'grp': ANY, 510 | b'lv': self.sp.last, 511 | b'snd': decode_address(self.user_addr), 512 | b'type': b'appl' 513 | } 514 | ) 515 | 516 | # inner transactions 517 | inner_transactions = txn[b'dt'][b'itx'] 518 | self.assertEqual(len(inner_transactions), 2) 519 | 520 | # inner transactions - [1] 521 | self.assertDictEqual( 522 | inner_transactions[1][b'txn'], 523 | { 524 | b'aamt': outputs["asset_2_out"], 525 | b'arcv': decode_address(self.user_addr), 526 | b'fv': self.sp.first, 527 | b'lv': self.sp.last, 528 | b'snd': decode_address(self.pool_address), 529 | b'type': b'axfer', 530 | b'xaid': self.asset_2_id 531 | } 532 | ) 533 | 534 | # local state delta 535 | pool_local_state_delta = txn[b'dt'][b'ld'][1] 536 | self.assertDictEqual(pool_local_state_delta, outputs["local_state_delta"]) 537 | -------------------------------------------------------------------------------- /tests/tests_set_fee.py: -------------------------------------------------------------------------------- 1 | from algojig import get_suggested_params 2 | from algojig.exceptions import LogicEvalError 3 | from algojig.ledger import JigLedger 4 | from algosdk.account import generate_account 5 | from algosdk.encoding import decode_address 6 | from algosdk.future import transaction 7 | 8 | from .constants import * 9 | from .core import BaseTestCase 10 | 11 | 12 | class TestSetFee(BaseTestCase): 13 | @classmethod 14 | def setUpClass(cls): 15 | cls.sp = get_suggested_params() 16 | cls.app_creator_sk, cls.app_creator_address = generate_account() 17 | cls.user_sk, cls.user_addr = generate_account() 18 | cls.asset_1_id = 5 19 | cls.asset_2_id = 2 20 | 21 | def reset_ledger(self): 22 | self.ledger = JigLedger() 23 | self.create_amm_app() 24 | self.ledger.set_account_balance(self.user_addr, 1_000_000) 25 | self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_1_id) 26 | self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_2_id) 27 | 28 | self.pool_address, self.pool_token_asset_id = self.bootstrap_pool(self.asset_1_id, self.asset_2_id) 29 | self.ledger.opt_in_asset(self.user_addr, self.pool_token_asset_id) 30 | 31 | def setUp(self): 32 | self.reset_ledger() 33 | 34 | def test_set_fee(self): 35 | test_cases = [ 36 | dict( 37 | msg="Test maximum.", 38 | inputs=dict( 39 | total_fee_share=100, 40 | protocol_fee_ratio=10 41 | ), 42 | ), 43 | dict( 44 | msg="Test minimums.", 45 | inputs=dict( 46 | total_fee_share=1, 47 | protocol_fee_ratio=3 48 | ), 49 | ), 50 | dict( 51 | msg="Test total share upper bound.", 52 | inputs=dict( 53 | total_fee_share=101, 54 | protocol_fee_ratio=10 55 | ), 56 | exception=dict( 57 | source_line='assert(total_fee_share <= 100)', 58 | ) 59 | ), 60 | dict( 61 | msg="Test total share lower bound.", 62 | inputs=dict( 63 | total_fee_share=0, 64 | protocol_fee_ratio=10 65 | ), 66 | exception=dict( 67 | source_line='assert(total_fee_share >= 1)', 68 | ) 69 | ), 70 | dict( 71 | msg="Test protocol ratio upper bound.", 72 | inputs=dict( 73 | total_fee_share=50, 74 | protocol_fee_ratio=11 75 | ), 76 | exception=dict( 77 | source_line='assert(protocol_fee_ratio <= 10)', 78 | ) 79 | ), 80 | dict( 81 | msg="Test protocol ratio lower bound.", 82 | inputs=dict( 83 | total_fee_share=50, 84 | protocol_fee_ratio=2 85 | ), 86 | exception=dict( 87 | source_line='assert(protocol_fee_ratio >= 3)', 88 | ) 89 | ), 90 | ] 91 | 92 | for test_case in test_cases: 93 | with self.subTest(**test_case): 94 | self.reset_ledger() 95 | inputs = test_case["inputs"] 96 | 97 | stxns = [ 98 | transaction.ApplicationNoOpTxn( 99 | sender=self.app_creator_address, 100 | sp=self.sp, 101 | index=APPLICATION_ID, 102 | app_args=[METHOD_SET_FEE, inputs["total_fee_share"], inputs["protocol_fee_ratio"]], 103 | accounts=[self.pool_address], 104 | ).sign(self.app_creator_sk) 105 | ] 106 | 107 | if exception := test_case.get("exception"): 108 | with self.assertRaises(LogicEvalError) as e: 109 | self.ledger.eval_transactions(stxns) 110 | 111 | self.assertEqual(e.exception.source['line'], exception.get("source_line")) 112 | 113 | else: 114 | block = self.ledger.eval_transactions(stxns) 115 | block_txns = block[b'txns'] 116 | 117 | # outer transactions 118 | self.assertEqual(len(block_txns), 1) 119 | 120 | # outer transactions[0] 121 | txn = block_txns[0] 122 | # there is no inner transaction 123 | self.assertIsNone(txn[b'dt'].get(b'itx')) 124 | self.assertDictEqual( 125 | txn[b'txn'], 126 | { 127 | b'apaa': [b'set_fee', inputs["total_fee_share"].to_bytes(8, 'big'), inputs["protocol_fee_ratio"].to_bytes(8, 'big')], 128 | b'apat': [decode_address(self.pool_address)], 129 | b'apid': APPLICATION_ID, 130 | b'fee': self.sp.fee, 131 | b'fv': self.sp.first, 132 | b'lv': self.sp.last, 133 | b'snd': decode_address(self.app_creator_address), 134 | b'type': b'appl' 135 | } 136 | ) 137 | 138 | # outer transactions[0] - Pool State Delta 139 | self.assertDictEqual( 140 | txn[b'dt'][b'ld'], 141 | { 142 | 1: { 143 | b'total_fee_share': {b'at': 2, **({b'ui': inputs["total_fee_share"]} if inputs["total_fee_share"] else {})}, 144 | b'protocol_fee_ratio': {b'at': 2, **({b'ui': inputs["protocol_fee_ratio"]} if inputs["protocol_fee_ratio"] else {})} 145 | } 146 | } 147 | ) 148 | 149 | def test_sender(self): 150 | self.ledger.set_account_balance(self.app_creator_address, 1_000_000) 151 | 152 | # Sender is not fee setter (app creator default) 153 | new_account_sk, new_account_address = generate_account() 154 | self.ledger.set_account_balance(new_account_address, 1_000_000) 155 | total_fee_share = 10 156 | protocol_fee_ratio = 3 157 | stxns = [ 158 | transaction.ApplicationNoOpTxn( 159 | sender=new_account_address, 160 | sp=self.sp, 161 | index=APPLICATION_ID, 162 | app_args=[METHOD_SET_FEE, total_fee_share, protocol_fee_ratio], 163 | accounts=[self.pool_address], 164 | ).sign(new_account_sk) 165 | ] 166 | 167 | with self.assertRaises(LogicEvalError) as e: 168 | self.ledger.eval_transactions(stxns) 169 | self.assertEqual(e.exception.source['line'], 'assert(user_address == app_global_get("fee_setter"))') 170 | 171 | self.ledger.update_global_state(app_id=APPLICATION_ID, state_delta={b"fee_setter": decode_address(new_account_address)}) 172 | block = self.ledger.eval_transactions(stxns) 173 | block_txns = block[b'txns'] 174 | 175 | # outer transactions 176 | self.assertEqual(len(block_txns), 1) 177 | 178 | # outer transactions[0] 179 | txn = block_txns[0] 180 | # there is no inner transaction 181 | self.assertIsNone(txn[b'dt'].get(b'itx')) 182 | self.assertDictEqual( 183 | txn[b'txn'], 184 | { 185 | b'apaa': [b'set_fee', total_fee_share.to_bytes(8, 'big'), protocol_fee_ratio.to_bytes(8, 'big')], 186 | b'apat': [decode_address(self.pool_address)], 187 | b'apid': APPLICATION_ID, 188 | b'fee': self.sp.fee, 189 | b'fv': self.sp.first, 190 | b'lv': self.sp.last, 191 | b'snd': decode_address(new_account_address), 192 | b'type': b'appl' 193 | } 194 | ) 195 | 196 | # outer transactions[0] - Pool State Delta 197 | self.assertDictEqual( 198 | txn[b'dt'][b'ld'], 199 | { 200 | 1: { 201 | b'total_fee_share': {b'at': 2, b'ui': total_fee_share}, 202 | b'protocol_fee_ratio': {b'at': 2, b'ui': protocol_fee_ratio} 203 | } 204 | } 205 | ) 206 | -------------------------------------------------------------------------------- /tests/tests_set_fee_collector.py: -------------------------------------------------------------------------------- 1 | from algojig import get_suggested_params 2 | from algojig.exceptions import LogicEvalError 3 | from algojig.ledger import JigLedger 4 | from algosdk.account import generate_account 5 | from algosdk.encoding import decode_address 6 | from algosdk.future import transaction 7 | 8 | from .constants import * 9 | from .core import BaseTestCase 10 | 11 | 12 | class TestSetFeeCollector(BaseTestCase): 13 | @classmethod 14 | def setUpClass(cls): 15 | cls.sp = get_suggested_params() 16 | cls.app_creator_sk, cls.app_creator_address = generate_account() 17 | cls.user_sk, cls.user_addr = generate_account() 18 | cls.asset_1_id = 5 19 | cls.asset_2_id = 2 20 | 21 | def setUp(self): 22 | self.ledger = JigLedger() 23 | self.create_amm_app() 24 | self.ledger.set_account_balance(self.user_addr, 1_000_000) 25 | self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_1_id) 26 | self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_2_id) 27 | 28 | self.pool_address, self.pool_token_asset_id = self.bootstrap_pool(self.asset_1_id, self.asset_2_id) 29 | self.ledger.opt_in_asset(self.user_addr, self.pool_token_asset_id) 30 | 31 | def test_pass(self): 32 | fee_manager_sk, fee_manager = self.app_creator_sk, self.app_creator_address 33 | _, fee_collector_1 = generate_account() 34 | _, fee_collector_2 = generate_account() 35 | self.ledger.set_account_balance(fee_manager, 1_000_000) 36 | self.ledger.set_account_balance(fee_collector_1, 1_000_000) 37 | self.ledger.set_account_balance(fee_collector_2, 1_000_000) 38 | 39 | # Group is not required. 40 | # Creator sets fee_collector to fee_collector_1 41 | # fee_collector_1 sets fee_collector to fee_collector_2 42 | txns = [ 43 | transaction.ApplicationNoOpTxn( 44 | sender=fee_manager, 45 | sp=self.sp, 46 | index=APPLICATION_ID, 47 | app_args=[METHOD_SET_FEE_COLLECTOR], 48 | accounts=[fee_collector_1], 49 | ), 50 | transaction.ApplicationNoOpTxn( 51 | sender=fee_manager, 52 | sp=self.sp, 53 | index=APPLICATION_ID, 54 | app_args=[METHOD_SET_FEE_COLLECTOR], 55 | accounts=[fee_collector_2], 56 | ) 57 | ] 58 | stxns = [ 59 | txns[0].sign(fee_manager_sk), 60 | txns[1].sign(fee_manager_sk) 61 | ] 62 | 63 | block = self.ledger.eval_transactions(stxns) 64 | block_txns = block[b'txns'] 65 | 66 | # outer transactions 67 | self.assertEqual(len(block_txns), 2) 68 | 69 | # outer transactions[0] 70 | txn = block_txns[0] 71 | # there is no inner transaction 72 | self.assertIsNone(txn[b'dt'].get(b'itx')) 73 | self.assertDictEqual( 74 | txn[b'txn'], 75 | { 76 | b'apaa': [b'set_fee_collector'], 77 | b'apat': [decode_address(fee_collector_1)], 78 | b'apid': APPLICATION_ID, 79 | b'fee': self.sp.fee, 80 | b'fv': self.sp.first, 81 | b'lv': self.sp.last, 82 | b'snd': decode_address(fee_manager), 83 | b'type': b'appl' 84 | } 85 | ) 86 | # outer transactions[0] - Global Delta 87 | self.assertDictEqual( 88 | txn[b'dt'][b'gd'], 89 | { 90 | b'fee_collector': {b'at': 1, b'bs': decode_address(fee_collector_1)} 91 | } 92 | ) 93 | 94 | # outer transactions[1] 95 | txn = block_txns[1] 96 | # there is no inner transaction 97 | self.assertIsNone(txn[b'dt'].get(b'itx')) 98 | self.assertDictEqual( 99 | txn[b'txn'], 100 | { 101 | b'apaa': [b'set_fee_collector'], 102 | b'apat': [decode_address(fee_collector_2)], 103 | b'apid': APPLICATION_ID, 104 | b'fee': self.sp.fee, 105 | b'fv': self.sp.first, 106 | b'lv': self.sp.last, 107 | b'snd': decode_address(fee_manager), 108 | b'type': b'appl' 109 | } 110 | ) 111 | 112 | # outer transactions[1] - Global Delta 113 | self.assertDictEqual( 114 | txn[b'dt'][b'gd'], 115 | { 116 | b'fee_collector': {b'at': 1, b'bs': decode_address(fee_collector_2)} 117 | } 118 | ) 119 | 120 | def test_fail_sender_is_not_fee_manager(self): 121 | invalid_account_sk, invalid_account_address = generate_account() 122 | self.ledger.set_account_balance(self.app_creator_address, 1_000_000) 123 | self.ledger.set_account_balance(invalid_account_address, 1_000_000) 124 | 125 | stxn = transaction.ApplicationNoOpTxn( 126 | sender=invalid_account_address, 127 | sp=self.sp, 128 | index=APPLICATION_ID, 129 | app_args=[METHOD_SET_FEE_COLLECTOR], 130 | accounts=[invalid_account_address], 131 | ).sign(invalid_account_sk) 132 | 133 | with self.assertRaises(LogicEvalError) as e: 134 | self.ledger.eval_transactions([stxn]) 135 | self.assertEqual(e.exception.source['line'], 'assert(user_address == app_global_get("fee_manager"))') 136 | -------------------------------------------------------------------------------- /tests/tests_set_fee_manager.py: -------------------------------------------------------------------------------- 1 | from algojig import get_suggested_params 2 | from algojig.exceptions import LogicEvalError 3 | from algojig.ledger import JigLedger 4 | from algosdk.account import generate_account 5 | from algosdk.encoding import decode_address 6 | from algosdk.future import transaction 7 | 8 | from .constants import * 9 | from .core import BaseTestCase 10 | 11 | 12 | class TestSetFeeManager(BaseTestCase): 13 | @classmethod 14 | def setUpClass(cls): 15 | cls.sp = get_suggested_params() 16 | cls.app_creator_sk, cls.app_creator_address = generate_account() 17 | cls.user_sk, cls.user_addr = generate_account() 18 | cls.asset_1_id = 5 19 | cls.asset_2_id = 2 20 | 21 | def setUp(self): 22 | self.ledger = JigLedger() 23 | self.create_amm_app() 24 | self.ledger.set_account_balance(self.user_addr, 1_000_000) 25 | self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_1_id) 26 | self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_2_id) 27 | 28 | self.pool_address, self.pool_token_asset_id = self.bootstrap_pool(self.asset_1_id, self.asset_2_id) 29 | self.ledger.opt_in_asset(self.user_addr, self.pool_token_asset_id) 30 | 31 | def test_pass(self): 32 | fee_manager_1_sk, fee_manager_1 = generate_account() 33 | _, fee_manager_2 = generate_account() 34 | self.ledger.set_account_balance(self.app_creator_address, 1_000_000) 35 | self.ledger.set_account_balance(fee_manager_1, 1_000_000) 36 | self.ledger.set_account_balance(fee_manager_2, 1_000_000) 37 | 38 | # Group is not required. 39 | # Creator sets fee_manager to fee_manager_1 40 | # fee_manager_1 sets fee_manager to fee_manager_2 41 | txns = [ 42 | transaction.ApplicationNoOpTxn( 43 | sender=self.app_creator_address, 44 | sp=self.sp, 45 | index=APPLICATION_ID, 46 | app_args=[METHOD_SET_FEE_MANAGER], 47 | accounts=[fee_manager_1], 48 | ), 49 | transaction.ApplicationNoOpTxn( 50 | sender=fee_manager_1, 51 | sp=self.sp, 52 | index=APPLICATION_ID, 53 | app_args=[METHOD_SET_FEE_MANAGER], 54 | accounts=[fee_manager_2], 55 | ) 56 | ] 57 | stxns = [ 58 | txns[0].sign(self.app_creator_sk), 59 | txns[1].sign(fee_manager_1_sk) 60 | ] 61 | 62 | block = self.ledger.eval_transactions(stxns) 63 | block_txns = block[b'txns'] 64 | 65 | # outer transactions 66 | self.assertEqual(len(block_txns), 2) 67 | 68 | # outer transactions[0] 69 | txn = block_txns[0] 70 | # there is no inner transaction 71 | self.assertIsNone(txn[b'dt'].get(b'itx')) 72 | self.assertDictEqual( 73 | txn[b'txn'], 74 | { 75 | b'apaa': [b'set_fee_manager'], 76 | b'apat': [decode_address(fee_manager_1)], 77 | b'apid': APPLICATION_ID, 78 | b'fee': self.sp.fee, 79 | b'fv': self.sp.first, 80 | b'lv': self.sp.last, 81 | b'snd': decode_address(self.app_creator_address), 82 | b'type': b'appl' 83 | } 84 | ) 85 | # outer transactions[0] - Global Delta 86 | self.assertDictEqual( 87 | txn[b'dt'][b'gd'], 88 | { 89 | b'fee_manager': {b'at': 1, b'bs': decode_address(fee_manager_1)} 90 | } 91 | ) 92 | 93 | # outer transactions[1] 94 | txn = block_txns[1] 95 | # there is no inner transaction 96 | self.assertIsNone(txn[b'dt'].get(b'itx')) 97 | self.assertDictEqual( 98 | txn[b'txn'], 99 | { 100 | b'apaa': [b'set_fee_manager'], 101 | b'apat': [decode_address(fee_manager_2)], 102 | b'apid': APPLICATION_ID, 103 | b'fee': self.sp.fee, 104 | b'fv': self.sp.first, 105 | b'lv': self.sp.last, 106 | b'snd': decode_address(fee_manager_1), 107 | b'type': b'appl' 108 | } 109 | ) 110 | 111 | # outer transactions[1] - Global Delta 112 | self.assertDictEqual( 113 | txn[b'dt'][b'gd'], 114 | { 115 | b'fee_manager': {b'at': 1, b'bs': decode_address(fee_manager_2)} 116 | } 117 | ) 118 | 119 | def test_fail_sender_is_not_fee_manager(self): 120 | invalid_account_sk, invalid_account_address = generate_account() 121 | self.ledger.set_account_balance(self.app_creator_address, 1_000_000) 122 | self.ledger.set_account_balance(invalid_account_address, 1_000_000) 123 | 124 | stxn = transaction.ApplicationNoOpTxn( 125 | sender=invalid_account_address, 126 | sp=self.sp, 127 | index=APPLICATION_ID, 128 | app_args=[METHOD_SET_FEE_MANAGER], 129 | accounts=[invalid_account_address], 130 | ).sign(invalid_account_sk) 131 | 132 | with self.assertRaises(LogicEvalError) as e: 133 | self.ledger.eval_transactions([stxn]) 134 | self.assertEqual(e.exception.source['line'], 'assert(user_address == app_global_get("fee_manager"))') 135 | -------------------------------------------------------------------------------- /tests/tests_set_fee_setter.py: -------------------------------------------------------------------------------- 1 | from algojig import get_suggested_params 2 | from algojig.exceptions import LogicEvalError 3 | from algojig.ledger import JigLedger 4 | from algosdk.account import generate_account 5 | from algosdk.encoding import decode_address 6 | from algosdk.future import transaction 7 | 8 | from .constants import * 9 | from .core import BaseTestCase 10 | 11 | 12 | class TestSetFeeSetter(BaseTestCase): 13 | @classmethod 14 | def setUpClass(cls): 15 | cls.sp = get_suggested_params() 16 | cls.app_creator_sk, cls.app_creator_address = generate_account() 17 | cls.user_sk, cls.user_addr = generate_account() 18 | cls.asset_1_id = 5 19 | cls.asset_2_id = 2 20 | 21 | def setUp(self): 22 | self.ledger = JigLedger() 23 | self.create_amm_app() 24 | self.ledger.set_account_balance(self.user_addr, 1_000_000) 25 | self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_1_id) 26 | self.ledger.set_account_balance(self.user_addr, MAX_ASSET_AMOUNT, asset_id=self.asset_2_id) 27 | 28 | self.pool_address, self.pool_token_asset_id = self.bootstrap_pool(self.asset_1_id, self.asset_2_id) 29 | self.ledger.opt_in_asset(self.user_addr, self.pool_token_asset_id) 30 | 31 | def test_pass(self): 32 | fee_manager_sk, fee_manager = self.app_creator_sk, self.app_creator_address 33 | _, fee_setter_1 = generate_account() 34 | _, fee_setter_2 = generate_account() 35 | self.ledger.set_account_balance(fee_manager, 1_000_000) 36 | self.ledger.set_account_balance(fee_setter_1, 1_000_000) 37 | self.ledger.set_account_balance(fee_setter_2, 1_000_000) 38 | 39 | # Group is not required. 40 | # Creator sets fee_setter to fee_setter_1 41 | # fee_setter_1 sets fee_setter to fee_setter_2 42 | txns = [ 43 | transaction.ApplicationNoOpTxn( 44 | sender=fee_manager, 45 | sp=self.sp, 46 | index=APPLICATION_ID, 47 | app_args=[METHOD_SET_FEE_SETTER], 48 | accounts=[fee_setter_1], 49 | ), 50 | transaction.ApplicationNoOpTxn( 51 | sender=fee_manager, 52 | sp=self.sp, 53 | index=APPLICATION_ID, 54 | app_args=[METHOD_SET_FEE_SETTER], 55 | accounts=[fee_setter_2], 56 | ) 57 | ] 58 | stxns = [ 59 | txns[0].sign(fee_manager_sk), 60 | txns[1].sign(fee_manager_sk) 61 | ] 62 | 63 | block = self.ledger.eval_transactions(stxns) 64 | block_txns = block[b'txns'] 65 | 66 | # outer transactions 67 | self.assertEqual(len(block_txns), 2) 68 | 69 | # outer transactions[0] 70 | txn = block_txns[0] 71 | # there is no inner transaction 72 | self.assertIsNone(txn[b'dt'].get(b'itx')) 73 | self.assertDictEqual( 74 | txn[b'txn'], 75 | { 76 | b'apaa': [b'set_fee_setter'], 77 | b'apat': [decode_address(fee_setter_1)], 78 | b'apid': APPLICATION_ID, 79 | b'fee': self.sp.fee, 80 | b'fv': self.sp.first, 81 | b'lv': self.sp.last, 82 | b'snd': decode_address(fee_manager), 83 | b'type': b'appl' 84 | } 85 | ) 86 | # outer transactions[0] - Global Delta 87 | self.assertDictEqual( 88 | txn[b'dt'][b'gd'], 89 | { 90 | b'fee_setter': {b'at': 1, b'bs': decode_address(fee_setter_1)} 91 | } 92 | ) 93 | 94 | # outer transactions[1] 95 | txn = block_txns[1] 96 | # there is no inner transaction 97 | self.assertIsNone(txn[b'dt'].get(b'itx')) 98 | self.assertDictEqual( 99 | txn[b'txn'], 100 | { 101 | b'apaa': [b'set_fee_setter'], 102 | b'apat': [decode_address(fee_setter_2)], 103 | b'apid': APPLICATION_ID, 104 | b'fee': self.sp.fee, 105 | b'fv': self.sp.first, 106 | b'lv': self.sp.last, 107 | b'snd': decode_address(fee_manager), 108 | b'type': b'appl' 109 | } 110 | ) 111 | 112 | # outer transactions[1] - Global Delta 113 | self.assertDictEqual( 114 | txn[b'dt'][b'gd'], 115 | { 116 | b'fee_setter': {b'at': 1, b'bs': decode_address(fee_setter_2)} 117 | } 118 | ) 119 | 120 | def test_fail_sender_is_not_fee_setter(self): 121 | invalid_account_sk, invalid_account_address = generate_account() 122 | self.ledger.set_account_balance(self.app_creator_address, 1_000_000) 123 | self.ledger.set_account_balance(invalid_account_address, 1_000_000) 124 | 125 | stxn = transaction.ApplicationNoOpTxn( 126 | sender=invalid_account_address, 127 | sp=self.sp, 128 | index=APPLICATION_ID, 129 | app_args=[METHOD_SET_FEE_SETTER], 130 | accounts=[invalid_account_address], 131 | ).sign(invalid_account_sk) 132 | 133 | with self.assertRaises(LogicEvalError) as e: 134 | self.ledger.eval_transactions([stxn]) 135 | self.assertEqual(e.exception.source['line'], 'assert(user_address == app_global_get("fee_manager"))') 136 | -------------------------------------------------------------------------------- /tests/tests_swap.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import ANY 2 | 3 | from algojig import get_suggested_params 4 | from algojig.exceptions import LogicEvalError 5 | from algojig.ledger import JigLedger 6 | from algosdk.account import generate_account 7 | from algosdk.encoding import decode_address 8 | from algosdk.future import transaction 9 | 10 | from .constants import * 11 | from .core import BaseTestCase 12 | from .utils import int_to_bytes_without_zero_padding 13 | 14 | 15 | class TestSwap(BaseTestCase): 16 | 17 | @classmethod 18 | def setUpClass(cls): 19 | cls.sp = get_suggested_params() 20 | cls.app_creator_sk, cls.app_creator_address = generate_account() 21 | cls.user_sk, cls.user_addr = generate_account() 22 | cls.asset_1_id = 5 23 | cls.asset_2_id = 2 24 | 25 | def setUp(self): 26 | self.ledger = JigLedger() 27 | self.create_amm_app() 28 | self.ledger.set_account_balance(self.user_addr, 1_000_000) 29 | self.ledger.set_account_balance(self.user_addr, 1_000_000, asset_id=self.asset_1_id) 30 | self.ledger.set_account_balance(self.user_addr, 0, asset_id=self.asset_2_id) 31 | 32 | self.pool_address, self.pool_token_asset_id = self.bootstrap_pool(self.asset_1_id, self.asset_2_id) 33 | 34 | def test_pass_fixed_input(self): 35 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000) 36 | 37 | min_output = 9000 38 | txn_group = [ 39 | transaction.AssetTransferTxn( 40 | sender=self.user_addr, 41 | sp=self.sp, 42 | receiver=self.pool_address, 43 | index=self.asset_1_id, 44 | amt=10_000, 45 | ), 46 | transaction.ApplicationNoOpTxn( 47 | sender=self.user_addr, 48 | sp=self.sp, 49 | index=APPLICATION_ID, 50 | app_args=[METHOD_SWAP, "fixed-input", min_output], 51 | foreign_assets=[self.asset_1_id, self.asset_2_id], 52 | accounts=[self.pool_address], 53 | ) 54 | ] 55 | txn_group[1].fee = 2000 56 | 57 | txn_group = transaction.assign_group_id(txn_group) 58 | stxns = [ 59 | txn_group[0].sign(self.user_sk), 60 | txn_group[1].sign(self.user_sk) 61 | ] 62 | 63 | block = self.ledger.eval_transactions(stxns) 64 | block_txns = block[b'txns'] 65 | 66 | # outer transactions 67 | self.assertEqual(len(block_txns), 2) 68 | 69 | # outer transactions - [0] 70 | txn = block_txns[0] 71 | self.assertDictEqual( 72 | txn[b'txn'], 73 | { 74 | b'aamt': 10000, 75 | b'arcv': decode_address(self.pool_address), 76 | b'fee': ANY, 77 | b'fv': ANY, 78 | b'lv': ANY, 79 | b'grp': ANY, 80 | b'snd': decode_address(self.user_addr), 81 | b'type': b'axfer', 82 | b'xaid': self.asset_1_id 83 | } 84 | ) 85 | 86 | # outer transactions - [1] 87 | txn = block_txns[1] 88 | self.assertDictEqual( 89 | txn[b'txn'], 90 | { 91 | b'apaa': [ 92 | b'swap', 93 | b'fixed-input', 94 | min_output.to_bytes(8, 'big'), 95 | ], 96 | b'apas': [self.asset_1_id, self.asset_2_id], 97 | b'apat': [decode_address(self.pool_address)], 98 | b'apid': APPLICATION_ID, 99 | b'fee': 2000, 100 | b'fv': ANY, 101 | b'lv': ANY, 102 | b'grp': ANY, 103 | b'snd': decode_address(self.user_addr), 104 | b'type': b'appl' 105 | } 106 | ) 107 | 108 | inner_transactions = txn[b'dt'][b'itx'] 109 | self.assertEqual(len(inner_transactions), 1) 110 | 111 | # inner transactions - [0] 112 | self.assertDictEqual( 113 | inner_transactions[0][b'txn'], 114 | { 115 | b'aamt': 9871, 116 | b'arcv': decode_address(self.user_addr), 117 | b'fv': ANY, 118 | b'lv': ANY, 119 | b'snd': decode_address(self.pool_address), 120 | b'type': b'axfer', 121 | b'xaid': self.asset_2_id 122 | } 123 | ) 124 | 125 | # local state delta 126 | pool_local_state_delta = txn[b'dt'][b'ld'][1] 127 | self.assertDictEqual( 128 | pool_local_state_delta, 129 | { 130 | b'asset_1_reserves': {b'at': 2, b'ui': 1009995}, 131 | b'asset_2_reserves': {b'at': 2, b'ui': 990129}, 132 | b'asset_1_protocol_fees': {b'at': 2, b'ui': 5}, 133 | b'asset_1_cumulative_price': {b'at': 1, b'bs': int_to_bytes_without_zero_padding(PRICE_SCALE_FACTOR * BLOCK_TIME_DELTA)}, 134 | b'asset_2_cumulative_price': {b'at': 1, b'bs': int_to_bytes_without_zero_padding(PRICE_SCALE_FACTOR * BLOCK_TIME_DELTA)}, 135 | b'cumulative_price_update_timestamp': {b'at': 2, b'ui': BLOCK_TIME_DELTA}, 136 | } 137 | ) 138 | 139 | def test_pass_fixed_output(self): 140 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000) 141 | 142 | amount_out = 9871 143 | txn_group = [ 144 | transaction.AssetTransferTxn( 145 | sender=self.user_addr, 146 | sp=self.sp, 147 | receiver=self.pool_address, 148 | index=self.asset_1_id, 149 | amt=10_000, 150 | ), 151 | transaction.ApplicationNoOpTxn( 152 | sender=self.user_addr, 153 | sp=self.sp, 154 | index=APPLICATION_ID, 155 | app_args=[METHOD_SWAP, "fixed-output", amount_out], 156 | foreign_assets=[self.asset_1_id, self.asset_2_id], 157 | accounts=[self.pool_address], 158 | ) 159 | ] 160 | txn_group[1].fee = 3000 161 | 162 | txn_group = transaction.assign_group_id(txn_group) 163 | stxns = [ 164 | txn_group[0].sign(self.user_sk), 165 | txn_group[1].sign(self.user_sk) 166 | ] 167 | block = self.ledger.eval_transactions(stxns) 168 | txns = block[b'txns'] 169 | self.assertEqual(len(txns[1][b'dt'][b'itx']), 1) 170 | 171 | # Check details of output inner transaction 172 | itxn0 = txns[1][b'dt'][b'itx'][0][b'txn'] 173 | self.assertEqual(itxn0[b'aamt'], amount_out) 174 | self.assertEqual(itxn0[b'arcv'], decode_address(self.user_addr)) 175 | self.assertEqual(itxn0[b'xaid'], self.asset_2_id) 176 | self.assertEqual(itxn0[b'snd'], decode_address(self.pool_address)) 177 | 178 | def test_pass_fixed_output_with_change(self): 179 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000) 180 | 181 | amount_out = 9872 182 | txn_group = [ 183 | transaction.AssetTransferTxn( 184 | sender=self.user_addr, 185 | sp=self.sp, 186 | receiver=self.pool_address, 187 | index=self.asset_1_id, 188 | amt=10_100, 189 | ), 190 | transaction.ApplicationNoOpTxn( 191 | sender=self.user_addr, 192 | sp=self.sp, 193 | index=APPLICATION_ID, 194 | app_args=[METHOD_SWAP, "fixed-output", amount_out], 195 | foreign_assets=[self.asset_1_id, self.asset_2_id], 196 | accounts=[self.pool_address], 197 | ) 198 | ] 199 | txn_group[1].fee = 3000 200 | txn_group = transaction.assign_group_id(txn_group) 201 | stxns = [ 202 | txn_group[0].sign(self.user_sk), 203 | txn_group[1].sign(self.user_sk) 204 | ] 205 | block = self.ledger.eval_transactions(stxns) 206 | txns = block[b'txns'] 207 | self.assertEqual(len(txns[1][b'dt'][b'itx']), 2) 208 | 209 | # Check details of input change inner transaction 210 | itxn0 = txns[1][b'dt'][b'itx'][0][b'txn'] 211 | self.assertEqual(itxn0[b'aamt'], 99) 212 | self.assertEqual(itxn0[b'arcv'], decode_address(self.user_addr)) 213 | self.assertEqual(itxn0[b'xaid'], self.asset_1_id) 214 | self.assertEqual(itxn0[b'snd'], decode_address(self.pool_address)) 215 | 216 | # Check details of output inner transaction 217 | itxn1 = txns[1][b'dt'][b'itx'][1][b'txn'] 218 | self.assertEqual(itxn1[b'aamt'], amount_out) 219 | self.assertEqual(itxn1[b'arcv'], decode_address(self.user_addr)) 220 | self.assertEqual(itxn1[b'xaid'], self.asset_2_id) 221 | self.assertEqual(itxn1[b'snd'], decode_address(self.pool_address)) 222 | 223 | def test_fail_zero_input_amount(self): 224 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000) 225 | 226 | txn_group = [ 227 | transaction.AssetTransferTxn( 228 | sender=self.user_addr, 229 | sp=self.sp, 230 | receiver=self.pool_address, 231 | index=self.asset_1_id, 232 | amt=0, 233 | ), 234 | transaction.ApplicationNoOpTxn( 235 | sender=self.user_addr, 236 | sp=self.sp, 237 | index=APPLICATION_ID, 238 | app_args=[METHOD_SWAP, "fixed-input", 0], 239 | foreign_assets=[self.asset_1_id, self.asset_2_id], 240 | accounts=[self.pool_address], 241 | ) 242 | ] 243 | txn_group[1].fee = 2000 244 | 245 | txn_group = transaction.assign_group_id(txn_group) 246 | stxns = [ 247 | txn_group[0].sign(self.user_sk), 248 | txn_group[1].sign(self.user_sk) 249 | ] 250 | 251 | with self.assertRaises(LogicEvalError) as e: 252 | self.ledger.eval_transactions(stxns) 253 | self.assertEqual(e.exception.source['line'], "assert(input_amount)") 254 | 255 | def test_fail_zero_output_amount_fixed_input(self): 256 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=100_000_000, asset_2_reserves=1_000_000) 257 | 258 | txn_group = [ 259 | transaction.AssetTransferTxn( 260 | sender=self.user_addr, 261 | sp=self.sp, 262 | receiver=self.pool_address, 263 | index=self.asset_1_id, 264 | amt=10, 265 | ), 266 | transaction.ApplicationNoOpTxn( 267 | sender=self.user_addr, 268 | sp=self.sp, 269 | index=APPLICATION_ID, 270 | app_args=[METHOD_SWAP, "fixed-input", 1], 271 | foreign_assets=[self.asset_1_id, self.asset_2_id], 272 | accounts=[self.pool_address], 273 | ) 274 | ] 275 | txn_group[1].fee = 2000 276 | 277 | txn_group = transaction.assign_group_id(txn_group) 278 | stxns = [ 279 | txn_group[0].sign(self.user_sk), 280 | txn_group[1].sign(self.user_sk) 281 | ] 282 | 283 | with self.assertRaises(LogicEvalError) as e: 284 | self.ledger.eval_transactions(stxns) 285 | self.assertEqual(e.exception.source['line'], "assert(output_amount)") 286 | 287 | def test_fail_zero_output_amount_fixed_output(self): 288 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000) 289 | 290 | txn_group = [ 291 | transaction.AssetTransferTxn( 292 | sender=self.user_addr, 293 | sp=self.sp, 294 | receiver=self.pool_address, 295 | index=self.asset_1_id, 296 | amt=10_000, 297 | ), 298 | transaction.ApplicationNoOpTxn( 299 | sender=self.user_addr, 300 | sp=self.sp, 301 | index=APPLICATION_ID, 302 | app_args=[METHOD_SWAP, "fixed-output", 0], 303 | foreign_assets=[self.asset_1_id, self.asset_2_id], 304 | accounts=[self.pool_address], 305 | ) 306 | ] 307 | txn_group[1].fee = 2000 308 | 309 | txn_group = transaction.assign_group_id(txn_group) 310 | stxns = [ 311 | txn_group[0].sign(self.user_sk), 312 | txn_group[1].sign(self.user_sk) 313 | ] 314 | 315 | with self.assertRaises(LogicEvalError) as e: 316 | self.ledger.eval_transactions(stxns) 317 | self.assertEqual(e.exception.source['line'], "assert(output_amount)") 318 | 319 | def test_fail_insufficient_fee(self): 320 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000) 321 | 322 | txn_group = [ 323 | transaction.AssetTransferTxn( 324 | sender=self.user_addr, 325 | sp=self.sp, 326 | receiver=self.pool_address, 327 | index=self.asset_1_id, 328 | amt=10_000, 329 | ), 330 | transaction.ApplicationNoOpTxn( 331 | sender=self.user_addr, 332 | sp=self.sp, 333 | index=APPLICATION_ID, 334 | app_args=[METHOD_SWAP, "fixed-input", 9000], 335 | foreign_assets=[self.asset_1_id, self.asset_2_id], 336 | accounts=[self.pool_address], 337 | ) 338 | ] 339 | txn_group = transaction.assign_group_id(txn_group) 340 | stxns = [ 341 | txn_group[0].sign(self.user_sk), 342 | txn_group[1].sign(self.user_sk) 343 | ] 344 | with self.assertRaises(LogicEvalError) as e: 345 | self.ledger.eval_transactions(stxns) 346 | self.assertIn('fee too small', e.exception.error) 347 | 348 | def test_fail_fixed_input_high_min_output(self): 349 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000) 350 | 351 | min_output = 10_000 352 | txn_group = [ 353 | transaction.AssetTransferTxn( 354 | sender=self.user_addr, 355 | sp=self.sp, 356 | receiver=self.pool_address, 357 | index=self.asset_1_id, 358 | amt=10_000, 359 | ), 360 | transaction.ApplicationNoOpTxn( 361 | sender=self.user_addr, 362 | sp=self.sp, 363 | index=APPLICATION_ID, 364 | app_args=[METHOD_SWAP, "fixed-input", min_output], 365 | foreign_assets=[self.asset_1_id, self.asset_2_id], 366 | accounts=[self.pool_address], 367 | ) 368 | ] 369 | txn_group[1].fee = 2000 370 | 371 | txn_group = transaction.assign_group_id(txn_group) 372 | stxns = [ 373 | txn_group[0].sign(self.user_sk), 374 | txn_group[1].sign(self.user_sk) 375 | ] 376 | 377 | with self.assertRaises(LogicEvalError) as e: 378 | self.ledger.eval_transactions(stxns) 379 | self.assertEqual(e.exception.source['line'], "assert(output_amount >= min_output)") 380 | 381 | def test_fail_fixed_output_low_input_amount(self): 382 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000) 383 | 384 | amount_out = 10_000 385 | txn_group = [ 386 | transaction.AssetTransferTxn( 387 | sender=self.user_addr, 388 | sp=self.sp, 389 | receiver=self.pool_address, 390 | index=self.asset_1_id, 391 | amt=10_000, 392 | ), 393 | transaction.ApplicationNoOpTxn( 394 | sender=self.user_addr, 395 | sp=self.sp, 396 | index=APPLICATION_ID, 397 | app_args=[METHOD_SWAP, "fixed-output", amount_out], 398 | foreign_assets=[self.asset_1_id, self.asset_2_id], 399 | accounts=[self.pool_address], 400 | ) 401 | ] 402 | txn_group[1].fee = 3000 403 | 404 | txn_group = transaction.assign_group_id(txn_group) 405 | stxns = [ 406 | txn_group[0].sign(self.user_sk), 407 | txn_group[1].sign(self.user_sk) 408 | ] 409 | 410 | with self.assertRaises(LogicEvalError) as e: 411 | self.ledger.eval_transactions(stxns) 412 | self.assertEqual(e.exception.source['line'], "assert(input_amount >= required_input_amount)") 413 | 414 | def test_fail_fixed_input_total_fee_is_0(self): 415 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000) 416 | 417 | min_output = 300 418 | txn_group = [ 419 | transaction.AssetTransferTxn( 420 | sender=self.user_addr, 421 | sp=self.sp, 422 | receiver=self.pool_address, 423 | index=self.asset_1_id, 424 | amt=330, 425 | ), 426 | transaction.ApplicationNoOpTxn( 427 | sender=self.user_addr, 428 | sp=self.sp, 429 | index=APPLICATION_ID, 430 | app_args=[METHOD_SWAP, "fixed-input", min_output], 431 | foreign_assets=[self.asset_1_id, self.asset_2_id], 432 | accounts=[self.pool_address], 433 | ) 434 | ] 435 | txn_group[1].fee = 2000 436 | 437 | txn_group = transaction.assign_group_id(txn_group) 438 | stxns = [ 439 | txn_group[0].sign(self.user_sk), 440 | txn_group[1].sign(self.user_sk) 441 | ] 442 | 443 | with self.assertRaises(LogicEvalError) as e: 444 | self.ledger.eval_transactions(stxns) 445 | self.assertEqual(e.exception.source['line'], "assert(total_fee_amount)") 446 | 447 | def test_fail_fixed_output_total_fee_is_0(self): 448 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000) 449 | 450 | amount_out = 330 451 | txn_group = [ 452 | transaction.AssetTransferTxn( 453 | sender=self.user_addr, 454 | sp=self.sp, 455 | receiver=self.pool_address, 456 | index=self.asset_1_id, 457 | amt=500, 458 | ), 459 | transaction.ApplicationNoOpTxn( 460 | sender=self.user_addr, 461 | sp=self.sp, 462 | index=APPLICATION_ID, 463 | app_args=[METHOD_SWAP, "fixed-output", amount_out], 464 | foreign_assets=[self.asset_1_id, self.asset_2_id], 465 | accounts=[self.pool_address], 466 | ) 467 | ] 468 | txn_group[1].fee = 3000 469 | 470 | txn_group = transaction.assign_group_id(txn_group) 471 | stxns = [ 472 | txn_group[0].sign(self.user_sk), 473 | txn_group[1].sign(self.user_sk) 474 | ] 475 | 476 | with self.assertRaises(LogicEvalError) as e: 477 | self.ledger.eval_transactions(stxns) 478 | self.assertEqual(e.exception.source['line'], "assert(total_fee_amount)") 479 | 480 | def test_fail_invalid_mode(self): 481 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000) 482 | 483 | amount_out = 330 484 | txn_group = [ 485 | transaction.AssetTransferTxn( 486 | sender=self.user_addr, 487 | sp=self.sp, 488 | receiver=self.pool_address, 489 | index=self.asset_1_id, 490 | amt=500, 491 | ), 492 | transaction.ApplicationNoOpTxn( 493 | sender=self.user_addr, 494 | sp=self.sp, 495 | index=APPLICATION_ID, 496 | app_args=[METHOD_SWAP, "fixed", amount_out], 497 | foreign_assets=[self.asset_1_id, self.asset_2_id], 498 | accounts=[self.pool_address], 499 | ) 500 | ] 501 | txn_group[1].fee = 3000 502 | 503 | txn_group = transaction.assign_group_id(txn_group) 504 | stxns = [ 505 | txn_group[0].sign(self.user_sk), 506 | txn_group[1].sign(self.user_sk) 507 | ] 508 | 509 | with self.assertRaises(LogicEvalError) as e: 510 | self.ledger.eval_transactions(stxns) 511 | self.assertEqual(e.exception.source['line'], "error()") 512 | 513 | def test_fail_invalid_input_asset(self): 514 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000) 515 | self.ledger.set_account_balance(self.user_addr, 1000, asset_id=self.pool_token_asset_id) 516 | 517 | amount_out = 300 518 | txn_group = [ 519 | transaction.AssetTransferTxn( 520 | sender=self.user_addr, 521 | sp=self.sp, 522 | receiver=self.pool_address, 523 | index=self.pool_token_asset_id, 524 | amt=500, 525 | ), 526 | transaction.ApplicationNoOpTxn( 527 | sender=self.user_addr, 528 | sp=self.sp, 529 | index=APPLICATION_ID, 530 | app_args=[METHOD_SWAP, "fixed-input", amount_out], 531 | foreign_assets=[self.asset_1_id, self.asset_2_id], 532 | accounts=[self.pool_address], 533 | ) 534 | ] 535 | txn_group[1].fee = 3000 536 | 537 | txn_group = transaction.assign_group_id(txn_group) 538 | stxns = [ 539 | txn_group[0].sign(self.user_sk), 540 | txn_group[1].sign(self.user_sk) 541 | ] 542 | 543 | with self.assertRaises(LogicEvalError) as e: 544 | self.ledger.eval_transactions(stxns) 545 | self.assertEqual(e.exception.source['line'], "error()") 546 | 547 | def test_fail_invalid_asset_receiver(self): 548 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000) 549 | 550 | amount_out = 300 551 | txn_group = [ 552 | transaction.AssetTransferTxn( 553 | sender=self.user_addr, 554 | sp=self.sp, 555 | receiver=self.user_addr, 556 | index=self.asset_1_id, 557 | amt=500, 558 | ), 559 | transaction.ApplicationNoOpTxn( 560 | sender=self.user_addr, 561 | sp=self.sp, 562 | index=APPLICATION_ID, 563 | app_args=[METHOD_SWAP, "fixed-input", amount_out], 564 | foreign_assets=[self.asset_1_id, self.asset_2_id], 565 | accounts=[self.pool_address], 566 | ) 567 | ] 568 | txn_group[1].fee = 3000 569 | 570 | txn_group = transaction.assign_group_id(txn_group) 571 | stxns = [ 572 | txn_group[0].sign(self.user_sk), 573 | txn_group[1].sign(self.user_sk) 574 | ] 575 | 576 | with self.assertRaises(LogicEvalError) as e: 577 | self.ledger.eval_transactions(stxns) 578 | self.assertEqual(e.exception.source['line'], "assert(Gtxn[input_txn_index].AssetReceiver == pool_address)") 579 | 580 | def test_fail_senders_are_not_same(self): 581 | new_user_sk, new_user_addr = generate_account() 582 | self.ledger.set_account_balance(new_user_addr, 1_000_000) 583 | 584 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000) 585 | 586 | amount_out = 300 587 | txn_group = [ 588 | transaction.AssetTransferTxn( 589 | sender=self.user_addr, 590 | sp=self.sp, 591 | receiver=self.user_addr, 592 | index=self.asset_1_id, 593 | amt=500, 594 | ), 595 | transaction.ApplicationNoOpTxn( 596 | sender=new_user_addr, 597 | sp=self.sp, 598 | index=APPLICATION_ID, 599 | app_args=[METHOD_SWAP, "fixed-input", amount_out], 600 | foreign_assets=[self.asset_1_id, self.asset_2_id], 601 | accounts=[self.pool_address], 602 | ) 603 | ] 604 | txn_group[1].fee = 3000 605 | 606 | txn_group = transaction.assign_group_id(txn_group) 607 | stxns = [ 608 | txn_group[0].sign(self.user_sk), 609 | txn_group[1].sign(new_user_sk) 610 | ] 611 | 612 | with self.assertRaises(LogicEvalError) as e: 613 | self.ledger.eval_transactions(stxns) 614 | self.assertEqual(e.exception.source['line'], "assert(Gtxn[input_txn_index].AssetReceiver == pool_address)") 615 | -------------------------------------------------------------------------------- /tests/tests_swap_groupped.py: -------------------------------------------------------------------------------- 1 | 2 | from algojig import get_suggested_params 3 | from algojig.ledger import JigLedger 4 | from algosdk.account import generate_account 5 | from algosdk.encoding import decode_address 6 | from algosdk.future import transaction 7 | 8 | from .constants import * 9 | from .core import BaseTestCase 10 | from .utils import get_pool_logicsig_bytecode 11 | 12 | 13 | class TestGroupedSwap(BaseTestCase): 14 | 15 | @classmethod 16 | def setUpClass(cls): 17 | cls.sp = get_suggested_params() 18 | cls.app_creator_sk, cls.app_creator_address = generate_account() 19 | cls.user_sk, cls.user_addr = generate_account() 20 | cls.asset_1_id = 5 21 | cls.asset_2_id = 2 22 | cls.asset_3_id = 7 23 | 24 | def setUp(self): 25 | self.ledger = JigLedger() 26 | self.create_amm_app() 27 | self.ledger.set_account_balance(self.user_addr, 1_000_000) 28 | self.ledger.set_account_balance(self.user_addr, 1_000_000, asset_id=self.asset_1_id) 29 | self.ledger.set_account_balance(self.user_addr, 0, asset_id=self.asset_2_id) 30 | self.ledger.set_account_balance(self.user_addr, 0, asset_id=self.asset_3_id) 31 | 32 | lsig1 = get_pool_logicsig_bytecode(amm_pool_template, APPLICATION_ID, self.asset_1_id, self.asset_2_id) 33 | self.pool_address1 = lsig1.address() 34 | self.ledger.set_account_balance(self.pool_address1, 1_000_000) 35 | self.ledger.set_auth_addr(self.pool_address1, APPLICATION_ADDRESS) 36 | 37 | lsig2 = get_pool_logicsig_bytecode(amm_pool_template, APPLICATION_ID, self.asset_2_id, self.asset_3_id) 38 | self.pool_address2 = lsig2.address() 39 | self.ledger.set_account_balance(self.pool_address2, 1_000_000) 40 | self.ledger.set_auth_addr(self.pool_address2, APPLICATION_ADDRESS) 41 | 42 | def test_pass(self): 43 | self.ledger.set_account_balance(self.pool_address1, 1_000_000, asset_id=self.asset_1_id) 44 | self.ledger.set_account_balance(self.pool_address1, 1_000_000, asset_id=self.asset_2_id) 45 | self.ledger.set_local_state( 46 | address=self.pool_address1, 47 | app_id=APPLICATION_ID, 48 | state={ 49 | b'asset_1_id': self.asset_1_id, 50 | b'asset_2_id': self.asset_2_id, 51 | b'asset_1_reserves': 1_000_000, 52 | b'asset_2_reserves': 1_000_000, 53 | b'total_fee_share': TOTAL_FEE_SHARE, 54 | b'protocol_fee_ratio': PROTOCOL_FEE_RATIO, 55 | } 56 | ) 57 | 58 | self.ledger.set_account_balance(self.pool_address2, 1_000_000, asset_id=self.asset_2_id) 59 | self.ledger.set_account_balance(self.pool_address2, 1_000_000, asset_id=self.asset_3_id) 60 | self.ledger.set_local_state( 61 | address=self.pool_address2, 62 | app_id=APPLICATION_ID, 63 | state={ 64 | b'asset_1_id': self.asset_2_id, 65 | b'asset_2_id': self.asset_3_id, 66 | b'asset_1_reserves': 1_000_000, 67 | b'asset_2_reserves': 1_000_000, 68 | b'total_fee_share': TOTAL_FEE_SHARE, 69 | b'protocol_fee_ratio': PROTOCOL_FEE_RATIO, 70 | } 71 | ) 72 | 73 | swap_1_amount_out = 9871 74 | swap_2_amount_out = 9746 75 | txn_group = [ 76 | # Swap 1 77 | transaction.AssetTransferTxn( 78 | sender=self.user_addr, 79 | sp=self.sp, 80 | receiver=self.pool_address1, 81 | index=self.asset_1_id, 82 | amt=10_000, 83 | ), 84 | transaction.ApplicationNoOpTxn( 85 | sender=self.user_addr, 86 | sp=self.sp, 87 | index=APPLICATION_ID, 88 | app_args=[METHOD_SWAP, "fixed-input", swap_1_amount_out], 89 | foreign_assets=[self.asset_1_id, self.asset_2_id], 90 | accounts=[self.pool_address1], 91 | ), 92 | 93 | # Swap 2 94 | transaction.AssetTransferTxn( 95 | sender=self.user_addr, 96 | sp=self.sp, 97 | receiver=self.pool_address2, 98 | index=self.asset_2_id, 99 | amt=swap_1_amount_out, 100 | ), 101 | transaction.ApplicationNoOpTxn( 102 | sender=self.user_addr, 103 | sp=self.sp, 104 | index=APPLICATION_ID, 105 | app_args=[METHOD_SWAP, "fixed-input", swap_2_amount_out], 106 | foreign_assets=[self.asset_2_id, self.asset_3_id], 107 | accounts=[self.pool_address2], 108 | ) 109 | ] 110 | txn_group[1].fee = 5000 111 | txn_group[3].fee = 5000 112 | 113 | txn_group = transaction.assign_group_id(txn_group) 114 | stxns = self.sign_txns(txn_group, self.user_sk) 115 | block = self.ledger.eval_transactions(stxns) 116 | txns = block[b'txns'] 117 | 118 | itxn = txns[1][b'dt'][b'itx'][0][b'txn'] 119 | self.assertEqual(itxn[b'aamt'], swap_1_amount_out) 120 | self.assertEqual(itxn[b'arcv'], decode_address(self.user_addr)) 121 | self.assertEqual(itxn[b'xaid'], self.asset_2_id) 122 | self.assertEqual(itxn[b'snd'], decode_address(self.pool_address1)) 123 | 124 | itxn = txns[3][b'dt'][b'itx'][0][b'txn'] 125 | self.assertEqual(itxn[b'aamt'], swap_2_amount_out) 126 | self.assertEqual(itxn[b'arcv'], decode_address(self.user_addr)) 127 | self.assertEqual(itxn[b'xaid'], self.asset_3_id) 128 | self.assertEqual(itxn[b'snd'], decode_address(self.pool_address2)) 129 | 130 | self.assertEqual(self.ledger.get_account_balance(self.user_addr, self.asset_2_id)[0], 0) 131 | self.assertEqual(self.ledger.get_account_balance(self.user_addr, self.asset_3_id)[0], swap_2_amount_out) 132 | -------------------------------------------------------------------------------- /tests/tests_swap_proxy.py: -------------------------------------------------------------------------------- 1 | from algojig import get_suggested_params 2 | from algojig.ledger import JigLedger 3 | from algosdk.account import generate_account 4 | from algosdk.encoding import decode_address 5 | from algosdk.future import transaction 6 | 7 | from .constants import * 8 | from .core import BaseTestCase 9 | 10 | proxy_approval_program = TealishProgram('tests/proxy_approval_program.tl') 11 | PROXY_APP_ID = 10 12 | PROXY_ADDRESS = get_application_address(PROXY_APP_ID) 13 | 14 | 15 | class TestProxySwap(BaseTestCase): 16 | 17 | @classmethod 18 | def setUpClass(cls): 19 | cls.sp = get_suggested_params() 20 | cls.app_creator_sk, cls.app_creator_address = generate_account() 21 | cls.user_sk, cls.user_addr = generate_account() 22 | cls.asset_1_id = 5 23 | cls.asset_2_id = 2 24 | 25 | def setUp(self): 26 | self.ledger = JigLedger() 27 | self.create_amm_app() 28 | self.ledger.set_account_balance(self.user_addr, 1_000_000) 29 | self.ledger.set_account_balance(self.user_addr, 1_000_000, asset_id=self.asset_1_id) 30 | self.ledger.set_account_balance(self.user_addr, 0, asset_id=self.asset_2_id) 31 | 32 | self.ledger.create_app(app_id=PROXY_APP_ID, approval_program=proxy_approval_program, creator=self.app_creator_address) 33 | self.ledger.set_account_balance(PROXY_ADDRESS, 1_000_000) 34 | self.ledger.set_account_balance(PROXY_ADDRESS, 0, asset_id=self.asset_1_id) 35 | self.ledger.set_account_balance(PROXY_ADDRESS, 0, asset_id=self.asset_2_id) 36 | 37 | self.pool_address, self.pool_token_asset_id = self.bootstrap_pool(self.asset_1_id, self.asset_2_id) 38 | 39 | def test_pass(self): 40 | self.set_initial_pool_liquidity(self.pool_address, self.asset_1_id, self.asset_2_id, self.pool_token_asset_id, asset_1_reserves=1_000_000, asset_2_reserves=1_000_000) 41 | 42 | txn_group = [ 43 | transaction.AssetTransferTxn( 44 | sender=self.user_addr, 45 | sp=self.sp, 46 | receiver=PROXY_ADDRESS, 47 | index=self.asset_1_id, 48 | amt=10_000, 49 | ), 50 | transaction.ApplicationNoOpTxn( 51 | sender=self.user_addr, 52 | sp=self.sp, 53 | index=PROXY_APP_ID, 54 | app_args=[METHOD_SWAP, 9000], 55 | foreign_assets=[self.asset_1_id, self.asset_2_id], 56 | foreign_apps=[APPLICATION_ID], 57 | accounts=[self.pool_address], 58 | ) 59 | ] 60 | txn_group[1].fee = 5000 61 | 62 | txn_group = transaction.assign_group_id(txn_group) 63 | stxns = [ 64 | txn_group[0].sign(self.user_sk), 65 | txn_group[1].sign(self.user_sk), 66 | ] 67 | 68 | block = self.ledger.eval_transactions(stxns) 69 | txns = block[b'txns'] 70 | itxn = txns[1][b'dt'][b'itx'][-1][b'txn'] 71 | self.assertEqual(itxn[b'aamt'], 9774) 72 | self.assertEqual(itxn[b'arcv'], decode_address(self.user_addr)) 73 | self.assertEqual(itxn[b'xaid'], self.asset_2_id) 74 | self.assertEqual(itxn[b'snd'], decode_address(PROXY_ADDRESS)) 75 | 76 | self.assertEqual(self.ledger.get_account_balance(PROXY_ADDRESS, self.asset_1_id)[0], 100) 77 | 78 | # do the same swap again and watch the fees accumulate 79 | self.ledger.eval_transactions(stxns) 80 | self.assertEqual(self.ledger.get_account_balance(PROXY_ADDRESS, self.asset_1_id)[0], 200) 81 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from decimal import ROUND_UP, Decimal 2 | 3 | from algosdk.future import transaction 4 | 5 | 6 | def int_to_bytes_without_zero_padding(value): 7 | length = int((Decimal(value.bit_length()) / 8).quantize(Decimal('1.'), rounding=ROUND_UP)) 8 | return value.to_bytes(length, "big") 9 | 10 | 11 | def itob(value): 12 | """ The same as teal itob - int to 8 bytes """ 13 | return value.to_bytes(8, 'big') 14 | 15 | 16 | def get_pool_logicsig_bytecode(pool_template, app_id, asset_1_id, asset_2_id): 17 | # These are the bytes of the logicsig template. This needs to be updated if the logicsig is updated. 18 | program = bytearray(pool_template.bytecode) 19 | 20 | template = b'\x06\x80\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x81\x00[5\x004\x001\x18\x12D1\x19\x81\x01\x12D\x81\x01C' 21 | assert program == bytearray(template) 22 | 23 | program[3:11] = app_id.to_bytes(8, 'big') 24 | program[11:19] = asset_1_id.to_bytes(8, 'big') 25 | program[19:27] = asset_2_id.to_bytes(8, 'big') 26 | return transaction.LogicSigAccount(program) 27 | 28 | 29 | def print_logs(txn): 30 | logs = txn[b'dt'].get(b'lg') 31 | if logs: 32 | for log in logs: 33 | if b'%i' in log: 34 | i = log.index(b'%i') 35 | s = log[0:i].decode() 36 | value = int.from_bytes(log[i + 2:], 'big') 37 | print(f'{s}: {value}') 38 | --------------------------------------------------------------------------------