├── examples ├── __init__.py ├── v2 │ ├── __init__.py │ ├── tutorial │ │ ├── __init__.py │ │ ├── 01_generate_account.py │ │ ├── 03_bootstrap_pool.py │ │ ├── 09_fixed_input_swap.py │ │ ├── 10_fixed_output_swap.py │ │ ├── 02_create_assets.py │ │ ├── 07_remove_liquidity.py │ │ ├── 08_single_asset_remove_liquidity.py │ │ ├── 04_add_initial_liquidity.py │ │ ├── common.py │ │ ├── 11_flash_loan_1_single_asset.py │ │ ├── 06_add_single_asset_liquidity.py │ │ ├── 05_add_flexible_liquidity.py │ │ ├── 12_flash_loan_2_multiple_assets.py │ │ ├── 13_flash_swap_1_pay_in_other_currency.py │ │ ├── 14_flash_swap_2_pay_in_same_currency.py │ │ └── 15_flash_swap_3_pay_in_multiple_currencies.py │ ├── utils.py │ └── sneak_preview.py ├── governance │ ├── __init__.py │ ├── 00_opt_in_to_tiny.py │ ├── 31_cast_vote.py │ ├── 20_staking_proposal_cast_vote.py │ ├── 05_withdraw.py │ ├── 99_create_checkpoints.py │ ├── 03_increase_lock_amount.py │ ├── 02_extend_lock_end_time.py │ ├── 04_increase_lock_amount_and_extend_lock_end_time.py │ ├── 01_create_lock.py │ ├── 10_claim_reward.py │ └── 30_create_proposal.py ├── swap_router │ └── __init__.py ├── staking │ ├── commitments.py │ └── make_commitment.py ├── v1 │ ├── pooling1.py │ ├── add_liquidity1.py │ ├── swapping1.py │ └── swapping1_less_convenience.py └── folks_lending │ ├── remove_liquidity.py │ ├── add_liquidity.py │ └── create_new_pool.py ├── tinyman ├── __init__.py ├── v1 │ ├── __init__.py │ ├── constants.py │ ├── optout.py │ ├── utils.py │ ├── fees.py │ ├── redeem.py │ ├── contracts.py │ ├── bootstrap.py │ ├── burn.py │ ├── mint.py │ ├── swap.py │ └── client.py ├── v2 │ ├── __init__.py │ ├── exceptions.py │ ├── contracts.py │ ├── bootstrap.py │ ├── constants.py │ ├── management.py │ ├── flash_swap.py │ ├── swap.py │ ├── fees.py │ ├── client.py │ ├── utils.py │ ├── flash_loan.py │ ├── remove_liquidity.py │ └── quotes.py ├── ordering │ ├── __init__.py │ ├── structs.py │ ├── registry_structs.json │ ├── utils.py │ ├── constants.py │ ├── order_structs.json │ └── event.py ├── folks_lending │ ├── __init__.py │ ├── constants.py │ └── utils.py ├── governance │ ├── __init__.py │ ├── rewards │ │ ├── __init__.py │ │ ├── utils.py │ │ ├── constants.py │ │ └── events.py │ ├── vault │ │ ├── __init__.py │ │ ├── exceptions.py │ │ ├── utils.py │ │ ├── constants.py │ │ └── events.py │ ├── proposal_voting │ │ ├── __init__.py │ │ ├── exceptions.py │ │ └── constants.py │ ├── staking_voting │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── events.py │ │ └── storage.py │ ├── constants.py │ ├── event.py │ └── utils.py ├── swap_router │ ├── __init__.py │ ├── constants.py │ ├── utils.py │ └── management.py ├── liquid_staking │ ├── __init__.py │ ├── structs.json │ ├── constants.py │ └── utils.py ├── staking │ └── constants.py ├── exceptions.py ├── constants.py ├── errors.py ├── optin.py ├── compat.py ├── tealishmap.py ├── assets.py └── client.py ├── tests ├── swap_router │ └── __init__.py ├── __init__.py ├── v2 │ ├── __init__.py │ └── test_flash_swap.py └── test_app_call_note.py ├── .flake8 ├── .pre-commit-config.yaml ├── setup.py ├── LICENSE ├── .github └── workflows │ └── tests.yml ├── asc.json ├── CHANGELOG.md └── .gitignore /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/v2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tinyman/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tinyman/v1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tinyman/v2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tinyman/ordering/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/governance/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/swap_router/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/v2/tutorial/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/swap_router/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tinyman/folks_lending/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tinyman/governance/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tinyman/swap_router/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tinyman/governance/rewards/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tinyman/governance/vault/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tinyman/liquid_staking/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tinyman/governance/proposal_voting/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tinyman/governance/staking_voting/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501,F403,F405,E126,E121,W503,E203 -------------------------------------------------------------------------------- /tinyman/governance/proposal_voting/exceptions.py: -------------------------------------------------------------------------------- 1 | class InsufficientTinyPower(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /tinyman/staking/constants.py: -------------------------------------------------------------------------------- 1 | DATE_FORMAT = "%Y%m%d" 2 | 3 | TESTNET_STAKING_APP_ID = 51948952 4 | MAINNET_STAKING_APP_ID = 649588853 5 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from tinyman.compat import SuggestedParams 2 | 3 | 4 | def get_suggested_params(): 5 | sp = SuggestedParams( 6 | fee=1000, first=1, last=1000, min_fee=1000, flat_fee=True, gh="test" 7 | ) 8 | return sp 9 | -------------------------------------------------------------------------------- /tinyman/folks_lending/constants.py: -------------------------------------------------------------------------------- 1 | MAINNET_FOLKS_POOL_MANAGER_APP_ID = 971350278 2 | TESTNET_FOLKS_POOL_MANAGER_APP_ID = 147157634 3 | 4 | TESTNET_FOLKS_WRAPPER_LENDING_POOL_APP_ID = 548587153 5 | MAINNET_FOLKS_WRAPPER_LENDING_POOL_APP_ID = 1385499515 6 | -------------------------------------------------------------------------------- /tinyman/v2/exceptions.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa, for backward compatibility. 2 | 3 | from tinyman.exceptions import ( 4 | PoolIsNotBootstrapped, 5 | PoolAlreadyBootstrapped, 6 | PoolHasNoLiquidity, 7 | PoolAlreadyInitialized, 8 | InsufficientReserves, 9 | ) 10 | -------------------------------------------------------------------------------- /examples/v2/utils.py: -------------------------------------------------------------------------------- 1 | from algosdk.v2client.algod import AlgodClient 2 | 3 | 4 | def get_algod(): 5 | # return AlgodClient( 6 | # "", "http://localhost:8080", headers={"User-Agent": "algosdk"} 7 | # ) 8 | return AlgodClient("", "https://testnet-api.algonode.network") 9 | -------------------------------------------------------------------------------- /tinyman/governance/vault/exceptions.py: -------------------------------------------------------------------------------- 1 | class InsufficientLockAmount(Exception): 2 | pass 3 | 4 | 5 | class InvalidLockEndTime(Exception): 6 | pass 7 | 8 | 9 | class ShortLockEndTime(Exception): 10 | pass 11 | 12 | 13 | class TooLongLockEndTime(Exception): 14 | pass 15 | -------------------------------------------------------------------------------- /tinyman/exceptions.py: -------------------------------------------------------------------------------- 1 | class PoolIsNotBootstrapped(Exception): 2 | pass 3 | 4 | 5 | class PoolAlreadyBootstrapped(Exception): 6 | pass 7 | 8 | 9 | class PoolHasNoLiquidity(Exception): 10 | pass 11 | 12 | 13 | class PoolAlreadyInitialized(Exception): 14 | pass 15 | 16 | 17 | class InsufficientReserves(Exception): 18 | pass 19 | 20 | 21 | class LowSwapAmountError(Exception): 22 | pass 23 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pycqa/flake8 3 | rev: '3.9.2' # pick a git hash / tag to point to 4 | hooks: 5 | - id: flake8 6 | args: ['--ignore=E501,F403,F405,E126,E121,W503,E203', '.'] 7 | exclude: ^(env|venv) 8 | 9 | - repo: https://github.com/psf/black 10 | rev: 23.3.0 11 | hooks: 12 | - id: black 13 | args: ['.', '--check'] 14 | exclude: ^(env|venv) 15 | -------------------------------------------------------------------------------- /tinyman/ordering/structs.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from .struct import register_struct_file, get_struct 3 | 4 | SDK_DIR = Path(__file__).parent 5 | 6 | 7 | register_struct_file(filepath=SDK_DIR / "order_structs.json") 8 | register_struct_file(filepath=SDK_DIR / "registry_structs.json") 9 | 10 | AppVersion = get_struct("AppVersion") 11 | Entry = get_struct("Entry") 12 | TriggerOrder = get_struct("TriggerOrder") 13 | RecurringOrder = get_struct("RecurringOrder") 14 | -------------------------------------------------------------------------------- /tinyman/constants.py: -------------------------------------------------------------------------------- 1 | # https://developer.algorand.org/docs/get-details/parameter_tables/ 2 | # MaxAppTotalTxnReferences 3 | MAX_APP_TOTAL_TXN_REFERENCES = 8 4 | 5 | # MaxAppProgramCost 6 | MAX_APP_PROGRAM_COST = 700 7 | 8 | MINIMUM_BALANCE_REQUIREMENT_PER_BOX = 2_500 9 | MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE = 400 10 | 11 | # Folks Lending Integration 12 | MINUTE = 60 13 | HOUR = 60 * MINUTE 14 | DAY = 24 * HOUR 15 | WEEK = 7 * DAY 16 | YEAR = 365 * DAY 17 | HOURS_PER_YEAR = 365 * 24 18 | -------------------------------------------------------------------------------- /examples/staking/commitments.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from tinyman.staking import parse_commit_transaction 3 | from tinyman.staking.constants import TESTNET_STAKING_APP_ID 4 | 5 | app_id = TESTNET_STAKING_APP_ID 6 | result = requests.get( 7 | f"https://indexer.testnet.algoexplorerapi.io/v2/transactions?application-id={app_id}&latest=50" 8 | ).json() 9 | for txn in result["transactions"]: 10 | commit = parse_commit_transaction(txn, app_id) 11 | if commit: 12 | print(commit) 13 | print() 14 | -------------------------------------------------------------------------------- /tinyman/v1/constants.py: -------------------------------------------------------------------------------- 1 | BOOTSTRAP_APP_ARGUMENT = "Ym9vdHN0cmFw" 2 | BURN_APP_ARGUMENT = "YnVybg==" 3 | MINT_APP_ARGUMENT = "bWludA==" 4 | REDEEM_APP_ARGUMENT = "cmVkZWVt" 5 | SWAP_APP_ARGUMENT = "c3dhcA==" 6 | 7 | TESTNET_VALIDATOR_APP_ID_V1_0 = 21580889 8 | TESTNET_VALIDATOR_APP_ID_V1_1 = 62368684 9 | 10 | MAINNET_VALIDATOR_APP_ID_V1_0 = 350338509 11 | MAINNET_VALIDATOR_APP_ID_V1_1 = 552635992 12 | 13 | TESTNET_VALIDATOR_APP_ID = TESTNET_VALIDATOR_APP_ID_V1_1 14 | MAINNET_VALIDATOR_APP_ID = MAINNET_VALIDATOR_APP_ID_V1_1 15 | -------------------------------------------------------------------------------- /tinyman/v1/optout.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from tinyman.compat import ApplicationClearStateTxn 4 | from algosdk.v2client.algod import AlgodClient 5 | 6 | 7 | def get_optout_transactions( 8 | client: AlgodClient, 9 | sender, 10 | validator_app_id, 11 | app_call_note: Optional[str] = None, 12 | ): 13 | suggested_params = client.suggested_params() 14 | 15 | txn = ApplicationClearStateTxn( 16 | sender=sender, 17 | sp=suggested_params, 18 | index=validator_app_id, 19 | note=app_call_note, 20 | ) 21 | 22 | return [txn], [None] 23 | -------------------------------------------------------------------------------- /tinyman/ordering/registry_structs.json: -------------------------------------------------------------------------------- 1 | { 2 | "structs": { 3 | "Entry": { 4 | "size": 8, 5 | "fields": { 6 | "app_id": { 7 | "type": "int", 8 | "size": 8, 9 | "offset": 0 10 | } 11 | } 12 | }, 13 | "AppVersion": { 14 | "size": 982, 15 | "fields": { 16 | "approval_hash": { 17 | "type": "bytes[32]", 18 | "size": 32, 19 | "offset": 0 20 | }, 21 | "unused": { 22 | "type": "bytes[950]", 23 | "size": 950, 24 | "offset": 32 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tinyman/swap_router/constants.py: -------------------------------------------------------------------------------- 1 | TESTNET_SWAP_ROUTER_APP_ID_V1 = 184778019 2 | MAINNET_SWAP_ROUTER_APP_ID_V1 = 1083651166 3 | 4 | FIXED_INPUT_SWAP_TYPE = "fixed-input" 5 | FIXED_OUTPUT_SWAP_TYPE = "fixed-output" 6 | 7 | SWAP_APP_ARGUMENT = b"swap" 8 | FIXED_INPUT_APP_ARGUMENT = b"fixed-input" 9 | FIXED_OUTPUT_APP_ARGUMENT = b"fixed-output" 10 | ASSET_OPT_IN_APP_ARGUMENT = b"asset_opt_in" 11 | CLAIM_EXTRA_APP_ARGUMENT = b"claim_extra" 12 | SET_MANAGER_APP_ARGUMENT = b"set_manager" 13 | SET_EXTRA_COLLECTOR_APP_ARGUMENT = b"set_extra_collector" 14 | 15 | # Event Log Selectors 16 | SWAP_EVENT_LOG_SELECTOR = b"\x81b\xda\x9e" # "swap(uint64,uint64,uint64,uint64)" 17 | -------------------------------------------------------------------------------- /tinyman/v2/contracts.py: -------------------------------------------------------------------------------- 1 | from base64 import b64decode 2 | 3 | from tinyman.compat import LogicSigAccount 4 | 5 | from tinyman.v2.constants import POOL_LOGICSIG_TEMPLATE 6 | 7 | 8 | def get_pool_logicsig( 9 | validator_app_id: int, asset_a_id: int, asset_b_id: int 10 | ) -> LogicSigAccount: 11 | assets = [asset_a_id, asset_b_id] 12 | asset_1_id = max(assets) 13 | asset_2_id = min(assets) 14 | 15 | program = bytearray(b64decode(POOL_LOGICSIG_TEMPLATE)) 16 | program[3:11] = validator_app_id.to_bytes(8, "big") 17 | program[11:19] = asset_1_id.to_bytes(8, "big") 18 | program[19:27] = asset_2_id.to_bytes(8, "big") 19 | return LogicSigAccount(program) 20 | -------------------------------------------------------------------------------- /tinyman/v1/utils.py: -------------------------------------------------------------------------------- 1 | from base64 import b64decode 2 | 3 | 4 | def get_state_from_account_info(account_info, app_id): 5 | try: 6 | app = [a for a in account_info["apps-local-state"] if a["id"] == app_id][0] 7 | except IndexError: 8 | return {} 9 | try: 10 | app_state = {} 11 | for x in app["key-value"]: 12 | key = b64decode(x["key"]) 13 | if x["value"]["type"] == 1: 14 | value = b64decode(x["value"].get("bytes", "")) 15 | else: 16 | value = x["value"].get("uint", 0) 17 | app_state[key] = value 18 | except KeyError: 19 | return {} 20 | return app_state 21 | -------------------------------------------------------------------------------- /tinyman/liquid_staking/structs.json: -------------------------------------------------------------------------------- 1 | { 2 | "structs": { 3 | "UserState": { 4 | "size": 32, 5 | "fields": { 6 | "staked_amount": { 7 | "type": "int", 8 | "size": 8, 9 | "offset": 0 10 | }, 11 | "accumulated_rewards_per_unit_at_last_update": { 12 | "type": "int", 13 | "size": 8, 14 | "offset": 8 15 | }, 16 | "accumulated_rewards": { 17 | "type": "int", 18 | "size": 8, 19 | "offset": 16 20 | }, 21 | "timestamp": { 22 | "type": "int", 23 | "size": 8, 24 | "offset": 24 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tinyman/errors.py: -------------------------------------------------------------------------------- 1 | class AlgodError(Exception): 2 | def __init__(self, message: str) -> None: 3 | super().__init__(message) 4 | self.message = message 5 | 6 | def __str__(self): 7 | return self.message 8 | 9 | 10 | class LogicError(AlgodError): 11 | def __init__(self, message, txn_id=None, pc=None, app_id=None) -> None: 12 | super().__init__(message) 13 | self.txn_id = txn_id 14 | self.pc = pc 15 | self.app_id = app_id 16 | 17 | 18 | class OverspendError(AlgodError): 19 | def __init__(self, txn_id, address, amount) -> None: 20 | super().__init__(f"Overspend by {address}. Tried to spend {amount}") 21 | self.txn_id = txn_id 22 | self.address = address 23 | self.amount = amount 24 | -------------------------------------------------------------------------------- /tinyman/swap_router/utils.py: -------------------------------------------------------------------------------- 1 | from base64 import b64decode 2 | from typing import Union, Optional 3 | 4 | 5 | def parse_swap_router_event_log(log: Union[bytes, str]) -> Optional[dict]: 6 | # Signature is "swap(uint64,uint64,uint64,uint64)" 7 | swap_event_selector = b"\x81b\xda\x9e" 8 | 9 | if isinstance(log, str): 10 | # Indexer returns logs as b64 encoded. 11 | log = b64decode(log) 12 | 13 | if log[:4] == swap_event_selector and len(log) >= 36: 14 | return dict( 15 | input_asset_id=int.from_bytes(log[4:12], "big"), 16 | output_asset_id=int.from_bytes(log[12:20], "big"), 17 | input_amount=int.from_bytes(log[20:28], "big"), 18 | output_amount=int.from_bytes(log[28:36], "big"), 19 | ) 20 | 21 | return None 22 | -------------------------------------------------------------------------------- /tinyman/optin.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from tinyman.compat import ApplicationOptInTxn, AssetOptInTxn 4 | 5 | from tinyman.utils import TransactionGroup 6 | 7 | 8 | def prepare_app_optin_transactions( 9 | validator_app_id, 10 | sender, 11 | suggested_params, 12 | app_call_note: Optional[str] = None, 13 | ): 14 | txn = ApplicationOptInTxn( 15 | sender=sender, sp=suggested_params, index=validator_app_id, note=app_call_note 16 | ) 17 | txn_group = TransactionGroup([txn]) 18 | return txn_group 19 | 20 | 21 | def prepare_asset_optin_transactions(asset_id, sender, suggested_params): 22 | assert asset_id != 0, "Cannot opt into ALGO" 23 | 24 | txn = AssetOptInTxn( 25 | sender=sender, 26 | sp=suggested_params, 27 | index=asset_id, 28 | ) 29 | txn_group = TransactionGroup([txn]) 30 | return txn_group 31 | -------------------------------------------------------------------------------- /examples/v2/sneak_preview.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | from examples.v2.utils import get_algod 5 | from tinyman.v2.client import TinymanV2TestnetClient 6 | 7 | algod = get_algod() 8 | client = TinymanV2TestnetClient(algod_client=algod) 9 | 10 | # Fetch our two assets of interest 11 | USDC = client.fetch_asset(10458941) 12 | ALGO = client.fetch_asset(0) 13 | 14 | # Fetch the pool we will work with 15 | pool = client.fetch_pool(USDC, ALGO) 16 | print(f"Pool Info: {pool.info()}") 17 | 18 | # Get a quote for a swap of 1 ALGO to USDC with 1% slippage tolerance 19 | quote = pool.fetch_fixed_input_swap_quote(amount_in=ALGO(1_000_000), slippage=0.01) 20 | print(quote) 21 | print(f"USDC per ALGO: {quote.price}") 22 | print(f"USDC per ALGO (worst case): {quote.price_with_slippage}") 23 | -------------------------------------------------------------------------------- /tinyman/ordering/utils.py: -------------------------------------------------------------------------------- 1 | from math import ceil 2 | from tinyman.utils import int_to_bytes 3 | from hashlib import sha256 4 | 5 | 6 | def int_array(elements, size, default=0): 7 | array = [default] * size 8 | 9 | for i in range(len(elements)): 10 | array[i] = elements[i] 11 | bytes = b"".join(map(int_to_bytes, array)) 12 | return bytes 13 | 14 | 15 | def calculate_approval_hash(bytecode): 16 | approval_hash = bytes(32) 17 | # the AVM gives access to approval programs in chunks of up to 4096 bytes 18 | chunk_size = 4096 19 | num_chunks = ceil(len(bytecode) / chunk_size) 20 | chunk_hashes = b"" 21 | for i in range(num_chunks): 22 | offset = (i * chunk_size) 23 | chunk = bytecode[offset: offset + chunk_size] 24 | chunk_hashes += sha256(chunk).digest() 25 | approval_hash = sha256(chunk_hashes).digest() 26 | return approval_hash 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | 4 | with open("README.md", "r") as f: 5 | long_description = f.read() 6 | 7 | setuptools.setup( 8 | name="tinyman-py-sdk", 9 | description="Tinyman Python SDK", 10 | author="Tinyman", 11 | author_email="hello@tinyman.org", 12 | version="2.1.2", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | license="MIT", 16 | project_urls={ 17 | "Source": "https://github.com/tinyman/tinyman-py-sdk", 18 | }, 19 | install_requires=["py-algorand-sdk >= 1.10.0", "requests >= 2.0.0"], 20 | packages=setuptools.find_packages(), 21 | python_requires=">=3.8", 22 | package_data={ 23 | "tinyman.v1": ["asc.json"], 24 | "tinyman.v2": ["amm_approval.map.json", "swap_router_approval.map.json"], 25 | "tinyman.liquid_staking": ["structs.json"], 26 | }, 27 | include_package_data=True, 28 | ) 29 | -------------------------------------------------------------------------------- /tinyman/governance/rewards/utils.py: -------------------------------------------------------------------------------- 1 | from tinyman.governance.rewards.storage import RewardPeriod 2 | 3 | 4 | def calculate_reward_amount(account_cumulative_power_delta: int, reward_period: RewardPeriod): 5 | return reward_period.total_reward_amount * account_cumulative_power_delta // reward_period.total_cumulative_power_delta 6 | 7 | 8 | def group_adjacent_period_indexes(indexes: list[int]) -> list[list[int]]: 9 | if not indexes: # Handle empty list 10 | return [] 11 | 12 | grouped = [] 13 | current_group = [indexes[0]] 14 | 15 | for i in range(1, len(indexes)): 16 | # Check if the current number is adjacent to the previous number 17 | if indexes[i] - indexes[i - 1] == 1: 18 | current_group.append(indexes[i]) 19 | else: 20 | grouped.append(current_group) 21 | current_group = [indexes[i]] 22 | 23 | # Append the last group after exiting the loop 24 | grouped.append(current_group) 25 | 26 | return grouped 27 | -------------------------------------------------------------------------------- /tinyman/compat.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | try: 4 | from algosdk.transaction import ( 5 | ApplicationClearStateTxn, 6 | ApplicationOptInTxn, 7 | ApplicationNoOpTxn, 8 | AssetTransferTxn, 9 | AssetCreateTxn, 10 | AssetOptInTxn, 11 | assign_group_id, 12 | LogicSigAccount, 13 | LogicSigTransaction, 14 | PaymentTxn, 15 | SuggestedParams, 16 | Transaction, 17 | OnComplete, 18 | wait_for_confirmation, 19 | ) 20 | except ImportError: 21 | from algosdk.future.transaction import ( 22 | ApplicationClearStateTxn, 23 | ApplicationOptInTxn, 24 | ApplicationNoOpTxn, 25 | AssetTransferTxn, 26 | AssetCreateTxn, 27 | AssetOptInTxn, 28 | assign_group_id, 29 | LogicSigAccount, 30 | LogicSigTransaction, 31 | PaymentTxn, 32 | SuggestedParams, 33 | Transaction, 34 | OnComplete, 35 | wait_for_confirmation, 36 | ) 37 | -------------------------------------------------------------------------------- /examples/v2/tutorial/01_generate_account.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from algosdk.account import generate_account 5 | from algosdk.mnemonic import from_private_key 6 | 7 | from examples.v2.tutorial.common import get_account_file_path 8 | 9 | account_file_path = get_account_file_path() 10 | 11 | try: 12 | size = os.path.getsize(account_file_path) 13 | except FileNotFoundError: 14 | size = 0 15 | else: 16 | if size > 0: 17 | raise Exception(f"The file({account_file_path}) is not empty") 18 | 19 | private_key, address = generate_account() 20 | mnemonic = from_private_key(private_key) 21 | 22 | account_data = { 23 | "address": address, 24 | "private_key": private_key, 25 | "mnemonic": mnemonic, 26 | } 27 | 28 | with open(account_file_path, "w", encoding="utf-8") as f: 29 | json.dump(account_data, f, ensure_ascii=False, indent=4) 30 | 31 | print(f"Generated Account: {address}") 32 | # Fund the account 33 | print( 34 | f"Go to https://bank.testnet.algorand.network/?account={address} and fund your account." 35 | ) 36 | -------------------------------------------------------------------------------- /examples/governance/00_opt_in_to_tiny.py: -------------------------------------------------------------------------------- 1 | from examples.v2.utils import get_algod 2 | from tinyman.governance.client import TinymanGovernanceTestnetClient 3 | 4 | from tinyman.governance.constants import TESTNET_TINY_ASSET_ID 5 | 6 | # Hardcoding account keys is not a great practice. This is for demonstration purposes only. 7 | # See the README & Docs for alternative signing methods. 8 | account = { 9 | "address": "ALGORAND_ADDRESS_HERE", 10 | "private_key": "base64_private_key_here", 11 | } 12 | 13 | algod = get_algod() 14 | 15 | # Client 16 | governance_client = TinymanGovernanceTestnetClient( 17 | algod_client=algod, 18 | user_address=account["address"] 19 | ) 20 | 21 | if not governance_client.asset_is_opted_in(TESTNET_TINY_ASSET_ID): 22 | txn_group = governance_client.prepare_asset_optin_transactions(TESTNET_TINY_ASSET_ID) 23 | txn_group.sign_with_private_key(address=account["address"], private_key=account["private_key"]) 24 | result = txn_group.submit(algod, wait=True) 25 | print("TXN:", result) 26 | 27 | print("Get some TINY token.") 28 | -------------------------------------------------------------------------------- /examples/governance/31_cast_vote.py: -------------------------------------------------------------------------------- 1 | from examples.v2.utils import get_algod 2 | from tinyman.governance.client import TinymanGovernanceTestnetClient 3 | 4 | # Hardcoding account keys is not a great practice. This is for demonstration purposes only. 5 | # See the README & Docs for alternative signing methods. 6 | account = { 7 | "address": "ALGORAND_ADDRESS_HERE", 8 | "private_key": "base64_private_key_here", 9 | } 10 | algod = get_algod() 11 | 12 | # Client 13 | governance_client = TinymanGovernanceTestnetClient( 14 | algod_client=algod, 15 | user_address=account["address"], 16 | ) 17 | 18 | account_state = governance_client.fetch_account_state() 19 | print("Account State before TXN:", account_state) 20 | 21 | proposal_id = "bafkreicgbzr64gmjl642tazzzuomrbzn2uimhhig2wq2ch7tjcyee5cxh4" 22 | 23 | # Upload metadata 24 | txn_group = governance_client.prepare_cast_vote_transactions(proposal_id=proposal_id, vote=0) 25 | txn_group.sign_with_private_key(address=account["address"], private_key=account["private_key"]) 26 | result = txn_group.submit(algod=algod, wait=True) 27 | print(result) 28 | -------------------------------------------------------------------------------- /tinyman/tealishmap.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional, Any 2 | 3 | 4 | class TealishMap: 5 | def __init__(self, map: Dict[str, Any]) -> None: 6 | self.pc_teal = map.get("pc_teal", []) 7 | self.teal_tealish = map.get("teal_tealish", []) 8 | self.errors: Dict[int, str] = { 9 | int(k): v for k, v in map.get("errors", {}).items() 10 | } 11 | 12 | def get_tealish_line_for_pc(self, pc: int) -> Optional[int]: 13 | teal_line = self.get_teal_line_for_pc(pc) 14 | if teal_line is not None: 15 | return self.get_tealish_line_for_teal(teal_line) 16 | return None 17 | 18 | def get_teal_line_for_pc(self, pc: int) -> Optional[int]: 19 | return self.pc_teal[pc] 20 | 21 | def get_tealish_line_for_teal(self, teal_line: int) -> int: 22 | return self.teal_tealish[teal_line] 23 | 24 | def get_error_for_pc(self, pc: int) -> Optional[str]: 25 | tealish_line = self.get_tealish_line_for_pc(pc) 26 | if tealish_line is not None: 27 | return self.errors.get(tealish_line, None) 28 | return None 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tinyman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Lint & Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ["3.11", "3.10", "3.9", "3.8"] 12 | py-algorand-sdk-version: [ 13 | "1.10.0", "1.11.0", "1.12.0", "1.13.0", "1.13.1", "1.14.0", "1.15.0", "1.16.0", 14 | "1.16.1","1.17.0", "1.18.0","1.19.0", "1.20.0", "1.20.1", "1.20.2", 15 | "2.0.0", "2.1.0", "2.1.1", "2.1.2" 16 | ] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install flake8 black==23.3.0 py-algorand-sdk==${{ matrix.py-algorand-sdk-version }} requests 30 | 31 | - name: Run flake8 32 | run: flake8 ${{ github.workspace }} --ignore=E501,F403,F405,E126,E121,W503,E203 33 | 34 | - name: Run Unit tests 35 | run: python -m unittest 36 | -------------------------------------------------------------------------------- /examples/governance/20_staking_proposal_cast_vote.py: -------------------------------------------------------------------------------- 1 | from examples.v2.utils import get_algod 2 | from tinyman.governance.client import TinymanGovernanceTestnetClient 3 | 4 | # Hardcoding account keys is not a great practice. This is for demonstration purposes only. 5 | # See the README & Docs for alternative signing methods. 6 | account = { 7 | "address": "ALGORAND_ADDRESS_HERE", 8 | "private_key": "base64_private_key_here", 9 | } 10 | 11 | algod = get_algod() 12 | 13 | # Client 14 | governance_client = TinymanGovernanceTestnetClient( 15 | algod_client=algod, 16 | user_address=account["address"], 17 | ) 18 | 19 | account_state = governance_client.fetch_account_state() 20 | print("Account State before TXN:", account_state) 21 | 22 | proposal_id = "bafkreiamwsbjwf5ithiq67cfwecuke5k262ktxd6vacy6sz5a52nzwrc24" 23 | 24 | # Upload metadata 25 | txn_group = governance_client.prepare_cast_vote_for_staking_distribution_proposal_transactions( 26 | proposal_id=proposal_id, 27 | votes=[20, 80], 28 | asset_ids=[149571310, 148620458], 29 | ) 30 | txn_group.sign_with_private_key(address=account["address"], private_key=account["private_key"]) 31 | result = txn_group.submit(algod=algod, wait=True) 32 | print(result) 33 | -------------------------------------------------------------------------------- /examples/governance/05_withdraw.py: -------------------------------------------------------------------------------- 1 | from examples.v2.utils import get_algod 2 | from tinyman.governance.client import TinymanGovernanceTestnetClient 3 | 4 | # Hardcoding account keys is not a great practice. This is for demonstration purposes only. 5 | # See the README & Docs for alternative signing methods. 6 | account = { 7 | "address": "ALGORAND_ADDRESS_HERE", 8 | "private_key": "base64_private_key_here", 9 | } 10 | 11 | algod = get_algod() 12 | 13 | # Client 14 | governance_client = TinymanGovernanceTestnetClient( 15 | algod_client=algod, 16 | user_address=account["address"] 17 | ) 18 | 19 | account_state = governance_client.fetch_account_state() 20 | print("Account State before TXN:", account_state) 21 | 22 | txn_group = governance_client.prepare_withdraw_transactions() 23 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 24 | txn_group.submit(algod, wait=True) 25 | 26 | account_state = governance_client.fetch_account_state() 27 | print("Account State after TXN:", account_state) 28 | 29 | tiny_power = governance_client.get_tiny_power() 30 | print("TINY POWER:", tiny_power) 31 | 32 | total_tiny_power = governance_client.get_total_tiny_power() 33 | print("Total TINY POWER:", total_tiny_power) 34 | # print(f"User TINY Power %{(tiny_power / total_tiny_power) * 100}") 35 | -------------------------------------------------------------------------------- /examples/governance/99_create_checkpoints.py: -------------------------------------------------------------------------------- 1 | from examples.v2.utils import get_algod 2 | from tinyman.governance.client import TinymanGovernanceTestnetClient 3 | 4 | # Hardcoding account keys is not a great practice. This is for demonstration purposes only. 5 | # See the README & Docs for alternative signing methods. 6 | account = { 7 | "address": "ALGORAND_ADDRESS_HERE", 8 | "private_key": "base64_private_key_here", 9 | } 10 | algod = get_algod() 11 | 12 | # Client 13 | governance_client = TinymanGovernanceTestnetClient( 14 | algod_client=algod, 15 | user_address=account["address"] 16 | ) 17 | 18 | account_state = governance_client.fetch_account_state() 19 | print("Account State before TXN:", account_state) 20 | 21 | txn_group = governance_client.prepare_create_checkpoints_transactions() 22 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 23 | txn_group.submit(algod, wait=True) 24 | 25 | account_state = governance_client.fetch_account_state() 26 | print("Account State after TXN:", account_state) 27 | 28 | tiny_power = governance_client.get_tiny_power() 29 | print("TINY POWER:", tiny_power) 30 | 31 | total_tiny_power = governance_client.get_total_tiny_power() 32 | print("Total TINY POWER:", total_tiny_power) 33 | print(f"User TINY Power %{(tiny_power / total_tiny_power) * 100}") 34 | -------------------------------------------------------------------------------- /examples/v1/pooling1.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | 5 | from tinyman.v1.client import TinymanTestnetClient 6 | from algosdk.v2client.algod import AlgodClient 7 | 8 | 9 | # Hardcoding account keys is not a great practice. This is for demonstration purposes only. 10 | # See the README & Docs for alternative signing methods. 11 | account = { 12 | "address": "ALGORAND_ADDRESS_HERE", 13 | "private_key": "base64_private_key_here", # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary 14 | } 15 | 16 | algod = AlgodClient( 17 | "", "http://localhost:8080", headers={"User-Agent": "algosdk"} 18 | ) 19 | client = TinymanTestnetClient(algod_client=algod, user_address=account["address"]) 20 | # By default all subsequent operations are on behalf of user_address 21 | 22 | # Fetch our two assets of interest 23 | TINYUSDC = client.fetch_asset(21582668) 24 | ALGO = client.fetch_asset(0) 25 | 26 | # Fetch the pool we will work with 27 | pool = client.fetch_pool(TINYUSDC, ALGO) 28 | 29 | info = pool.fetch_pool_position() 30 | share = info["share"] * 100 31 | print(f"Pool Tokens: {info[pool.liquidity_asset]}") 32 | print(f"Assets: {info[TINYUSDC]}, {info[ALGO]}") 33 | print(f"Share of pool: {share:.3f}%") 34 | -------------------------------------------------------------------------------- /examples/v2/tutorial/03_bootstrap_pool.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | from pprint import pprint 5 | from urllib.parse import quote_plus 6 | 7 | from examples.v2.tutorial.common import get_account, get_assets 8 | from examples.v2.utils import get_algod 9 | from tinyman.v2.client import TinymanV2TestnetClient 10 | 11 | 12 | account = get_account() 13 | algod = get_algod() 14 | client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) 15 | 16 | # Fetch assets 17 | ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] 18 | ASSET_A = client.fetch_asset(ASSET_A_ID) 19 | ASSET_B = client.fetch_asset(ASSET_B_ID) 20 | 21 | # Fetch the pool 22 | pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) 23 | print(pool) 24 | 25 | # Get transaction group 26 | txn_group = pool.prepare_bootstrap_transactions() 27 | 28 | # Sign 29 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 30 | 31 | # Submit transactions to the network and wait for confirmation 32 | txn_info = client.submit(txn_group, wait=True) 33 | 34 | print("Transaction Info") 35 | pprint(txn_info) 36 | 37 | print( 38 | f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" 39 | ) 40 | -------------------------------------------------------------------------------- /examples/governance/03_increase_lock_amount.py: -------------------------------------------------------------------------------- 1 | from examples.v2.utils import get_algod 2 | from tinyman.governance.client import TinymanGovernanceTestnetClient 3 | 4 | # Hardcoding account keys is not a great practice. This is for demonstration purposes only. 5 | # See the README & Docs for alternative signing methods. 6 | account = { 7 | "address": "ALGORAND_ADDRESS_HERE", 8 | "private_key": "base64_private_key_here", 9 | } 10 | 11 | algod = get_algod() 12 | 13 | # Client 14 | governance_client = TinymanGovernanceTestnetClient( 15 | algod_client=algod, 16 | user_address=account["address"] 17 | ) 18 | 19 | account_state = governance_client.fetch_account_state() 20 | print("Account State before TXN:", account_state) 21 | 22 | txn_group = governance_client.prepare_increase_lock_amount_transactions( 23 | locked_amount=4000000000, 24 | ) 25 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 26 | txn_group.submit(algod, wait=True) 27 | 28 | account_state = governance_client.fetch_account_state() 29 | print("Account State after TXN:", account_state) 30 | 31 | tiny_power = governance_client.get_tiny_power() 32 | print("TINY POWER:", tiny_power) 33 | 34 | total_tiny_power = governance_client.get_total_tiny_power() 35 | print("Total TINY POWER:", total_tiny_power) 36 | print(f"User TINY Power %{(tiny_power / total_tiny_power) * 100}") 37 | -------------------------------------------------------------------------------- /tinyman/governance/constants.py: -------------------------------------------------------------------------------- 1 | HOUR = 60 * 60 2 | DAY = 24 * HOUR 3 | WEEK = 7 * DAY 4 | 5 | BYTES_ZERO = b'\x00' 6 | BYTES_ONE = b'\x01' 7 | 8 | TESTNET_TINY_ASSET_ID = 258703304 9 | MAINNET_TINY_ASSET_ID = 2200000000 10 | 11 | TESTNET_VAULT_APP_ID = 480164661 12 | MAINNET_VAULT_APP_ID = 2200606875 13 | 14 | TESTNET_REWARDS_APP_ID = 336189106 15 | MAINNET_REWARDS_APP_ID = 2200608153 16 | 17 | TESTNET_STAKING_VOTING_APP_ID = 360907790 18 | MAINNET_STAKING_VOTING_APP_ID = 2200609638 19 | 20 | TESTNET_PROPOSAL_VOTING_APP_ID = 383416252 21 | MAINNET_PROPOSAL_VOTING_APP_ID = 2200608887 22 | 23 | TESTNET_ARBITRARY_EXECUTOR_APP_ID = 0 # Temporary 24 | MAINNET_ARBITRARY_EXECUTOR_APP_ID = 0 # Temporary 25 | 26 | TESTNET_FEE_MANAGEMENT_EXECUTOR_APP_ID = 0 # Temporary 27 | MAINNET_FEE_MANAGEMENT_EXECUTOR_APP_ID = 0 # Temporary 28 | 29 | TESTNET_TREASURY_MANAGEMENT_EXECUTOR_APP_ID = 0 # Temporary 30 | MAINNET_TREASURY_MANAGEMENT_EXECUTOR_APP_ID = 0 # Temporary 31 | 32 | TINY_ASSET_ID_KEY = b'tiny_asset_id' 33 | VAULT_APP_ID_KEY = b'vault_app_id' 34 | 35 | INCREASE_BUDGET_APP_ARGUMENT = b"increase_budget" 36 | GET_BOX_APP_ARGUMENT = b"get_box" 37 | SET_MANAGER_APP_ARGUMENT = b"set_manager" 38 | SET_PROPOSAL_MANAGER_APP_ARGUMENT = b"set_proposal_manager" 39 | SET_VOTING_DELAY_APP_ARGUMENT = b"set_voting_delay" 40 | SET_VOTING_DURATION_APP_ARGUMENT = b"set_voting_duration" 41 | -------------------------------------------------------------------------------- /tinyman/governance/staking_voting/constants.py: -------------------------------------------------------------------------------- 1 | from tinyman.constants import MINIMUM_BALANCE_REQUIREMENT_PER_BOX, MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE 2 | 3 | MAX_OPTION_COUNT = 16 4 | 5 | # Global States 6 | PROPOSAL_INDEX_COUNTER_KEY = b'proposal_index_counter' 7 | VOTING_DELAY_KEY = b'voting_delay' 8 | VOTING_DURATION_KEY = b'voting_duration' 9 | MANAGER_KEY = b'manager' 10 | PROPOSAL_MANAGER_KEY = b'proposal_manager' 11 | 12 | # Box 13 | PROPOSAL_BOX_PREFIX = b'p' 14 | STAKING_VOTE_BOX_PREFIX = b'v' 15 | STAKING_ATTENDANCE_BOX_PREFIX = b'a' 16 | 17 | STAKING_PROPOSAL_BOX_SIZE = 49 18 | STAKING_VOTE_BOX_SIZE = 8 19 | STAKING_ACCOUNT_ATTENDANCE_SHEET_BOX_SIZE = 24 20 | 21 | STAKING_VOTING_APP_MINIMUM_BALANCE_REQUIREMENT = 100_000 22 | 23 | STAKING_PROPOSAL_BOX_COST = MINIMUM_BALANCE_REQUIREMENT_PER_BOX + MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE * (60 + STAKING_PROPOSAL_BOX_SIZE) 24 | STAKING_VOTE_BOX_COST = MINIMUM_BALANCE_REQUIREMENT_PER_BOX + MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE * (17 + STAKING_VOTE_BOX_SIZE) 25 | STAKING_ATTENDANCE_BOX_COST = MINIMUM_BALANCE_REQUIREMENT_PER_BOX + MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE * (41 + STAKING_ACCOUNT_ATTENDANCE_SHEET_BOX_SIZE) 26 | 27 | STAKING_PROPOSAL_CATEGORY = "farming-rewards-distribution" 28 | 29 | CREATE_PROPOSAL_APP_ARGUMENT = b"create_proposal" 30 | CANCEL_PROPOSAL_APP_ARGUMENT = b"cancel_proposal" 31 | CAST_VOTE_APP_ARGUMENT = b"cast_vote" 32 | -------------------------------------------------------------------------------- /tinyman/liquid_staking/constants.py: -------------------------------------------------------------------------------- 1 | TESTNET_TALGO_APP_ID = 724519988 2 | MAINNET_TALGO_APP_ID = 2537013674 3 | 4 | TESTNET_TALGO_STAKING_APP_ID = 724676904 5 | MAINNET_TALGO_STAKING_APP_ID = 2537022861 6 | 7 | TESTNET_TALGO_ASSET_ID = 724519992 8 | MAINNET_TALGO_ASSET_ID = 2537013734 9 | 10 | TESTNET_STALGO_ASSET_ID = 724676936 11 | MAINNET_STALGO_ASSET_ID = 2537023208 12 | 13 | # App Constants 14 | 15 | APP_LOCAL_INTS = 3 16 | APP_LOCAL_BYTES = 1 17 | APP_GLOBAL_INTS = 16 18 | APP_GLOBAL_BYTES = 16 19 | EXTRA_PAGES = 3 20 | 21 | VAULT_APP_ID_KEY = b"vault_app_id" 22 | TINY_ASSET_ID_KEY = b"tiny_asset_id" 23 | TALGO_ASSET_ID_KEY = b"talgo_asset_id" 24 | STALGO_ASSET_ID_KEY = b"stalgo_asset_id" 25 | 26 | TOTAL_REWARD_AMOUNT_SUM_KEY = b"total_reward_amount_sum" 27 | TOTAL_CLAIMED_REWARD_AMOUNT_KEY = b"total_claimed_reward_amount" 28 | CURRENT_REWARD_RATE_PER_TIME_KEY = b"current_reward_rate_per_time" 29 | CURRENT_REWARD_RATE_PER_TIME_END_TIMESTAMP_KEY = b"current_reward_rate_per_time_end_timestamp" 30 | 31 | TINY_POWER_THRESHOLD_KEY = b"tiny_power_threshold" 32 | LAST_UPDATE_TIMESTAMP_KEY = b"last_update_timestamp" 33 | ACCUMULATED_REWARDS_PER_UNIT = b"accumulated_rewards_per_unit" 34 | TOTAL_STAKED_AMOUNT_KEY = b"total_staked_amount" 35 | TOTAL_STAKER_COUNT_KEY = b"total_staker_count" 36 | 37 | PROPOSED_MANAGER_KEY = b"proposed_manager" 38 | MANAGER_KEY = b"manager" 39 | 40 | MAX_UINT64 = 18446744073709551615 41 | RATE_SCALER = 1_000_000_000_000 42 | -------------------------------------------------------------------------------- /examples/staking/make_commitment.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | 5 | from tinyman.v1.client import TinymanTestnetClient 6 | 7 | from tinyman.staking import prepare_commit_transaction 8 | 9 | # Hardcoding account keys is not a great practice. This is for demonstration purposes only. 10 | # See the README & Docs for alternative signing methods. 11 | account = { 12 | "address": "ALGORAND_ADDRESS_HERE", 13 | "private_key": "base64_private_key_here", # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary 14 | } 15 | 16 | client = TinymanTestnetClient(user_address=account["address"]) 17 | 18 | # Fetch our two assets of interest 19 | TINYUSDC = client.fetch_asset(21582668) 20 | ALGO = client.fetch_asset(0) 21 | 22 | # Fetch the pool we will work with 23 | pool = client.fetch_pool(TINYUSDC, ALGO) 24 | 25 | 26 | sp = client.algod.suggested_params() 27 | 28 | txn_group = prepare_commit_transaction( 29 | app_id=client.staking_app_id, 30 | program_id=1, 31 | program_account="B4XVZ226UPFEIQBPIY6H454YA4B7HYXGEM7UDQR2RJP66HVLOARZTUTS6Q", 32 | pool_asset_id=pool.liquidity_asset.id, 33 | amount=600_000_000, 34 | sender=account["address"], 35 | suggested_params=sp, 36 | ) 37 | 38 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 39 | result = client.submit(txn_group, wait=True) 40 | print(result) 41 | -------------------------------------------------------------------------------- /examples/governance/02_extend_lock_end_time.py: -------------------------------------------------------------------------------- 1 | from examples.v2.utils import get_algod 2 | from tinyman.governance.client import TinymanGovernanceTestnetClient 3 | 4 | from tinyman.governance.constants import WEEK 5 | 6 | # Hardcoding account keys is not a great practice. This is for demonstration purposes only. 7 | # See the README & Docs for alternative signing methods. 8 | account = { 9 | "address": "ALGORAND_ADDRESS_HERE", 10 | "private_key": "base64_private_key_here", 11 | } 12 | 13 | algod = get_algod() 14 | 15 | # Client 16 | governance_client = TinymanGovernanceTestnetClient( 17 | algod_client=algod, 18 | user_address=account["address"] 19 | ) 20 | 21 | account_state = governance_client.fetch_account_state() 22 | print("Account State before TXN:", account_state) 23 | 24 | new_lock_end_time = account_state.lock_end_time + 4 * WEEK 25 | txn_group = governance_client.prepare_extend_lock_end_time_transactions( 26 | new_lock_end_time=new_lock_end_time, 27 | ) 28 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 29 | txn_group.submit(algod, wait=True) 30 | 31 | account_state = governance_client.fetch_account_state() 32 | print("Account State after TXN:", account_state) 33 | 34 | tiny_power = governance_client.get_tiny_power() 35 | print("TINY POWER:", tiny_power) 36 | 37 | total_tiny_power = governance_client.get_total_tiny_power() 38 | print("Total TINY POWER:", total_tiny_power) 39 | print(f"User TINY Power %{(tiny_power / total_tiny_power) * 100}") 40 | -------------------------------------------------------------------------------- /examples/governance/04_increase_lock_amount_and_extend_lock_end_time.py: -------------------------------------------------------------------------------- 1 | from examples.v2.utils import get_algod 2 | from tinyman.governance.client import TinymanGovernanceTestnetClient 3 | 4 | from tinyman.governance.constants import WEEK 5 | 6 | # Hardcoding account keys is not a great practice. This is for demonstration purposes only. 7 | # See the README & Docs for alternative signing methods. 8 | account = { 9 | "address": "ALGORAND_ADDRESS_HERE", 10 | "private_key": "base64_private_key_here", 11 | } 12 | 13 | algod = get_algod() 14 | 15 | # Client 16 | governance_client = TinymanGovernanceTestnetClient( 17 | algod_client=algod, 18 | user_address=account["address"] 19 | ) 20 | 21 | account_state = governance_client.fetch_account_state() 22 | print("Account State before TXN:", account_state) 23 | 24 | txn_group = governance_client.prepare_increase_lock_amount_and_extend_lock_end_time_transactions( 25 | locked_amount=5_000_000_000, 26 | new_lock_end_time=account_state.lock_end_time + 4 * WEEK 27 | ) 28 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 29 | txn_group.submit(algod, wait=True) 30 | 31 | account_state = governance_client.fetch_account_state() 32 | print("Account State after TXN:", account_state) 33 | 34 | tiny_power = governance_client.get_tiny_power() 35 | print("TINY POWER:", tiny_power) 36 | 37 | total_tiny_power = governance_client.get_total_tiny_power() 38 | print("Total TINY POWER:", total_tiny_power) 39 | print(f"User TINY Power %{(tiny_power / total_tiny_power) * 100}") 40 | -------------------------------------------------------------------------------- /tinyman/ordering/constants.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from algosdk import transaction 3 | 4 | # Order App Globals & Commons with Registry 5 | REGISTRY_APP_ID_KEY = b"registry_app_id" 6 | REGISTRY_APP_ACCOUNT_ADDRESS_KEY = b"registry_app_account_address" 7 | VAULT_APP_ID_KEY = b"vault_app_id" 8 | ROUTER_APP_ID_KEY = b"router_app_id" 9 | ORDER_FEE_RATE_KEY = b"order_fee_rate" 10 | GOVERNOR_ORDER_FEE_RATE_KEY = b"governor_order_fee_rate" 11 | GOVERNOR_FEE_RATE_POWER_THRESHOLD = b"governor_fee_rate_power_threshold" 12 | 13 | USER_ADDRESS_KEY = b"user_address" 14 | TOTAL_ORDER_COUNT_KEY = b"order_count" 15 | PROPOSED_MANAGER_KEY = b"proposed_manager" 16 | MANAGER_KEY = b"manager" 17 | VERSION_KEY = b"version" 18 | 19 | # Registry App Globals 20 | ENTRY_COUNT_KEY = b"entry_count" 21 | 22 | # Registry App Locals 23 | IS_ENDORSED_KEY = b"is_endorsed" 24 | 25 | # App Creation Config 26 | order_approval_program = requests.get("https://raw.githubusercontent.com/tinymanorg/tinyman-order-protocol/refs/tags/v4/contracts/order/build/order_approval.teal.tok").content 27 | order_clear_state_program = requests.get("https://raw.githubusercontent.com/tinymanorg/tinyman-order-protocol/refs/tags/v4/contracts/order/build/order_clear_state.teal.tok").content 28 | order_app_global_schema = transaction.StateSchema(num_uints=16, num_byte_slices=16) 29 | order_app_local_schema = transaction.StateSchema(num_uints=0, num_byte_slices=0) 30 | order_app_extra_pages = 3 31 | 32 | # App Ids 33 | TESTNET_ORDERING_REGISTRY_APP_ID = 739800082 34 | MAINNET_ORDERING_REGISTRY_APP_ID = 3019195131 35 | -------------------------------------------------------------------------------- /tinyman/liquid_staking/utils.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | from tinyman.liquid_staking.constants import * 4 | from tinyman.utils import get_global_state 5 | 6 | 7 | def calculate_talgo_to_algo_ratio(algod): 8 | global_state = get_global_state(algod, MAINNET_TALGO_APP_ID) 9 | 10 | LIQUID_STAKING_NODE_ADDRESSES = [ 11 | "EP2YRTCL3SAA7HYG7KKWUC6ZH36SLYIKOX4FORKXZLUUQASP5JDJP4UU5A", 12 | "D6CCE7DL3GSVOCQDPWMNR5V7JEKGXOJACCU4A4K76DLJHZ4H47WRVBPUNY", 13 | "UTTJ2JOAXXAZEMKFSRNKFW4OIPMETRORCHNCDEDAHBJ5THNZTLWS6ZLUYU", 14 | "3X3CIVGQGHVVGMJ627NQUXPN3EVLOR6ZPDXJ4XZGFW5DQVXFBGUKEKOEEI", 15 | "F66MBWKUEG5GXZB4HFIZJRSMNYOATH2URKQBTBKKI7ZJAA2IFUFKXLHTOA", 16 | ] 17 | app_account = LIQUID_STAKING_NODE_ADDRESSES[0] 18 | account_info = algod.account_info(app_account) 19 | 20 | algo_balance = account_info["amount"] - account_info["min-balance"] 21 | talgo_balance = account_info["assets"][0]["amount"] - global_state["protocol_talgo"] 22 | 23 | for address in LIQUID_STAKING_NODE_ADDRESSES[1:]: 24 | account_info = algod.account_info(address) 25 | algo_balance += (account_info["amount"] - account_info["min-balance"]) 26 | 27 | TALGO_TOTAL_SUPPLY = 10_000_000_000_000_000 28 | minted_talgo = TALGO_TOTAL_SUPPLY - talgo_balance 29 | new_rewards = algo_balance - global_state["algo_balance"] 30 | protocol_rewards = (new_rewards * global_state["protocol_fee"]) / 100 31 | rate = Decimal(algo_balance - protocol_rewards) / Decimal(minted_talgo) 32 | 33 | return rate 34 | -------------------------------------------------------------------------------- /examples/v2/tutorial/09_fixed_input_swap.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | from pprint import pprint 5 | from urllib.parse import quote_plus 6 | 7 | from tinyman.assets import AssetAmount 8 | 9 | from examples.v2.tutorial.common import get_account, get_assets 10 | from examples.v2.utils import get_algod 11 | from tinyman.v2.client import TinymanV2TestnetClient 12 | 13 | 14 | account = get_account() 15 | algod = get_algod() 16 | client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) 17 | 18 | ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] 19 | ASSET_A = client.fetch_asset(ASSET_A_ID) 20 | ASSET_B = client.fetch_asset(ASSET_B_ID) 21 | pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) 22 | 23 | position = pool.fetch_pool_position() 24 | amount_in = AssetAmount(pool.asset_1, 1_000_000) 25 | 26 | quote = pool.fetch_fixed_input_swap_quote( 27 | amount_in=amount_in, 28 | ) 29 | 30 | print("\nSwap Quote:") 31 | print(quote) 32 | 33 | txn_group = pool.prepare_swap_transactions_from_quote(quote=quote) 34 | 35 | # Sign 36 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 37 | 38 | # Submit transactions to the network and wait for confirmation 39 | txn_info = client.submit(txn_group, wait=True) 40 | print("Transaction Info") 41 | pprint(txn_info) 42 | 43 | print( 44 | f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" 45 | ) 46 | -------------------------------------------------------------------------------- /examples/v2/tutorial/10_fixed_output_swap.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | from pprint import pprint 5 | from urllib.parse import quote_plus 6 | 7 | from tinyman.assets import AssetAmount 8 | 9 | from examples.v2.tutorial.common import get_account, get_assets 10 | from examples.v2.utils import get_algod 11 | from tinyman.v2.client import TinymanV2TestnetClient 12 | 13 | 14 | account = get_account() 15 | algod = get_algod() 16 | client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) 17 | 18 | ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] 19 | ASSET_A = client.fetch_asset(ASSET_A_ID) 20 | ASSET_B = client.fetch_asset(ASSET_B_ID) 21 | pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) 22 | 23 | position = pool.fetch_pool_position() 24 | amount_out = AssetAmount(pool.asset_1, 1_000_000) 25 | 26 | quote = pool.fetch_fixed_output_swap_quote( 27 | amount_out=amount_out, 28 | ) 29 | 30 | print("\nSwap Quote:") 31 | print(quote) 32 | 33 | txn_group = pool.prepare_swap_transactions_from_quote(quote=quote) 34 | 35 | # Sign 36 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 37 | 38 | # Submit transactions to the network and wait for confirmation 39 | txn_info = client.submit(txn_group, wait=True) 40 | print("Transaction Info") 41 | pprint(txn_info) 42 | 43 | print( 44 | f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" 45 | ) 46 | -------------------------------------------------------------------------------- /tinyman/v1/fees.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from tinyman.compat import ApplicationNoOpTxn, PaymentTxn, AssetTransferTxn 4 | 5 | from tinyman.utils import TransactionGroup 6 | from .contracts import get_pool_logicsig 7 | 8 | 9 | def prepare_redeem_fees_transactions( 10 | validator_app_id, 11 | asset1_id, 12 | asset2_id, 13 | liquidity_asset_id, 14 | amount, 15 | creator, 16 | sender, 17 | suggested_params, 18 | app_call_note: Optional[str] = None, 19 | ): 20 | pool_logicsig = get_pool_logicsig(validator_app_id, asset1_id, asset2_id) 21 | pool_address = pool_logicsig.address() 22 | 23 | txns = [ 24 | PaymentTxn( 25 | sender=sender, 26 | sp=suggested_params, 27 | receiver=pool_address, 28 | amt=2000, 29 | note="fee", 30 | ), 31 | ApplicationNoOpTxn( 32 | sender=pool_address, 33 | sp=suggested_params, 34 | index=validator_app_id, 35 | app_args=["fees"], 36 | foreign_assets=[asset1_id, liquidity_asset_id] 37 | if asset2_id == 0 38 | else [asset1_id, asset2_id, liquidity_asset_id], 39 | note=app_call_note, 40 | ), 41 | AssetTransferTxn( 42 | sender=pool_address, 43 | sp=suggested_params, 44 | receiver=creator, 45 | amt=int(amount), 46 | index=liquidity_asset_id, 47 | ), 48 | ] 49 | txn_group = TransactionGroup(txns) 50 | txn_group.sign_with_logicsig(pool_logicsig) 51 | return txn_group 52 | -------------------------------------------------------------------------------- /tinyman/governance/rewards/constants.py: -------------------------------------------------------------------------------- 1 | # Global states 2 | from tinyman.constants import MINIMUM_BALANCE_REQUIREMENT_PER_BOX, MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE 3 | 4 | FIRST_PERIOD_TIMESTAMP = b'first_period_timestamp' 5 | REWARD_HISTORY_COUNT_KEY = b'reward_history_count' 6 | REWARD_PERIOD_COUNT_KEY = b'reward_period_count' 7 | MANAGER_KEY = b'manager' 8 | REWARDS_MANAGER_KEY = b'rewards_manager' 9 | 10 | # Boxes 11 | REWARD_PERIOD_BOX_PREFIX = b'rp' 12 | REWARD_HISTORY_BOX_PREFIX = b'rh' 13 | REWARD_CLAIM_SHEET_BOX_PREFIX = b'c' 14 | 15 | REWARD_CLAIM_SHEET_BOX_SIZE = 1012 16 | 17 | REWARD_HISTORY_SIZE = 16 18 | REWARD_HISTORY_BOX_SIZE = 256 19 | REWARD_HISTORY_BOX_ARRAY_LEN = 16 20 | 21 | REWARD_PERIOD_SIZE = 24 22 | REWARD_PERIOD_BOX_SIZE = 1008 23 | REWARD_PERIOD_BOX_ARRAY_LEN = 42 24 | 25 | REWARD_CLAIM_SHEET_BOX_COST = MINIMUM_BALANCE_REQUIREMENT_PER_BOX + MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE * (41 + REWARD_CLAIM_SHEET_BOX_SIZE) 26 | REWARD_PERIOD_BOX_COST = MINIMUM_BALANCE_REQUIREMENT_PER_BOX + MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE * (10 + REWARD_PERIOD_BOX_SIZE) 27 | REWARD_HISTORY_BOX_COST = MINIMUM_BALANCE_REQUIREMENT_PER_BOX + MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE * (10 + REWARD_HISTORY_BOX_SIZE) 28 | 29 | # 100_000 Default 30 | # 100_000 Opt-in 31 | # Box 32 | REWARDS_APP_MINIMUM_BALANCE_REQUIREMENT = 200_000 + REWARD_HISTORY_BOX_COST 33 | 34 | INIT_APP_ARGUMENT = b"init" 35 | SET_REWARD_AMOUNT_APP_ARGUMENT = b"set_reward_amount" 36 | CREATE_REWARD_PERIOD_APP_ARGUMENT = b"create_reward_period" 37 | CLAIM_REWARDS_APP_ARGUMENT = b"claim_rewards" 38 | SET_REWARDS_MANAGER_APP_ARGUMENT = b"set_rewards_manager" 39 | -------------------------------------------------------------------------------- /examples/v2/tutorial/02_create_assets.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | import json 5 | import os 6 | 7 | from examples.v2.tutorial.common import ( 8 | get_account, 9 | get_assets_file_path, 10 | create_asset, 11 | ) 12 | from examples.v2.utils import get_algod 13 | from tinyman.v2.client import TinymanV2TestnetClient 14 | 15 | 16 | assets_file_path = get_assets_file_path() 17 | 18 | try: 19 | size = os.path.getsize(assets_file_path) 20 | except FileNotFoundError: 21 | size = 0 22 | else: 23 | if size > 0: 24 | raise Exception(f"The file({assets_file_path}) is not empty") 25 | 26 | account = get_account() 27 | algod = get_algod() 28 | client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) 29 | 30 | account_info = algod.account_info(account["address"]) 31 | if not account_info["amount"]: 32 | print( 33 | f"Go to https://bank.testnet.algorand.network/?account={account['address']} and fund your account." 34 | ) 35 | exit(1) 36 | 37 | ASSET_A_ID = create_asset(algod, account["address"], account["private_key"]) 38 | ASSET_B_ID = create_asset(algod, account["address"], account["private_key"]) 39 | 40 | assets_data = {"ids": [ASSET_A_ID, ASSET_B_ID]} 41 | 42 | with open(assets_file_path, "w", encoding="utf-8") as f: 43 | json.dump(assets_data, f, ensure_ascii=False, indent=4) 44 | 45 | print(f"Generated Assets: {[ASSET_A_ID, ASSET_B_ID]}") 46 | print("View on Algoexplorer:") 47 | print(f"https://testnet.algoexplorer.io/asset/{ASSET_A_ID}") 48 | print(f"https://testnet.algoexplorer.io/asset/{ASSET_B_ID}") 49 | -------------------------------------------------------------------------------- /examples/governance/01_create_lock.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from examples.v2.utils import get_algod 4 | from tinyman.governance.client import TinymanGovernanceTestnetClient 5 | from tinyman.governance.constants import WEEK 6 | from tinyman.governance.vault.constants import MIN_LOCK_TIME 7 | 8 | # Hardcoding account keys is not a great practice. This is for demonstration purposes only. 9 | # See the README & Docs for alternative signing methods. 10 | account = { 11 | "address": "ALGORAND_ADDRESS_HERE", 12 | "private_key": "base64_private_key_here", 13 | } 14 | 15 | algod = get_algod() 16 | 17 | # Client 18 | governance_client = TinymanGovernanceTestnetClient( 19 | algod_client=algod, 20 | user_address=account["address"] 21 | ) 22 | 23 | account_state = governance_client.fetch_account_state() 24 | print("Account State before TXN:", account_state) 25 | 26 | end_timestamp_of_current_week = (int(time.time()) // WEEK + 1) * WEEK 27 | lock_end_timestamp = end_timestamp_of_current_week + MIN_LOCK_TIME 28 | 29 | # lock_end_timestamp = int(time.time()) + 100 30 | 31 | txn_group = governance_client.prepare_create_lock_transactions( 32 | locked_amount=10_000_000, 33 | lock_end_time=lock_end_timestamp, 34 | ) 35 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 36 | txn_group.submit(algod, wait=True) 37 | 38 | account_state = governance_client.fetch_account_state() 39 | print("Account State after TXN:", account_state) 40 | 41 | tiny_power = governance_client.get_tiny_power() 42 | print("TINY POWER:", tiny_power) 43 | 44 | total_tiny_power = governance_client.get_total_tiny_power() 45 | print("Total TINY POWER:", total_tiny_power) 46 | print(f"User TINY Power %{(tiny_power / total_tiny_power) * 100}") 47 | -------------------------------------------------------------------------------- /tests/v2/__init__.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from algosdk.v2client.algod import AlgodClient 4 | 5 | from tinyman.v2.client import TinymanV2Client 6 | from tests import get_suggested_params 7 | 8 | 9 | class BaseTestCase(TestCase): 10 | maxDiff = None 11 | 12 | @classmethod 13 | def get_tinyman_client(cls, user_address=None): 14 | return TinymanV2Client( 15 | algod_client=AlgodClient("TEST", "https://test.test.network"), 16 | validator_app_id=cls.VALIDATOR_APP_ID, 17 | user_address=user_address or cls.user_address, 18 | staking_app_id=None, 19 | ) 20 | 21 | @classmethod 22 | def get_suggested_params(cls): 23 | return get_suggested_params() 24 | 25 | @classmethod 26 | def get_pool_state( 27 | cls, asset_1_id=None, asset_2_id=None, pool_token_asset_id=None, **kwargs 28 | ): 29 | state = { 30 | "asset_1_cumulative_price": 0, 31 | "lock": 0, 32 | "cumulative_price_update_timestamp": 0, 33 | "asset_2_cumulative_price": 0, 34 | "asset_2_protocol_fees": 0, 35 | "asset_1_reserves": 0, 36 | "pool_token_asset_id": pool_token_asset_id or cls.pool_token_asset_id, 37 | "asset_1_protocol_fees": 0, 38 | "asset_1_id": asset_1_id or cls.asset_1_id, 39 | "asset_2_id": asset_2_id or cls.asset_2_id, 40 | "issued_pool_tokens": 0, 41 | "asset_2_reserves": 0, 42 | "protocol_fee_ratio": 6, 43 | "total_fee_share": 30, 44 | } 45 | state.update(**kwargs) 46 | return state 47 | 48 | @classmethod 49 | def app_call_note(cls): 50 | return b'tinyman/v2:j{"origin":"tinyman-py-sdk"}' 51 | -------------------------------------------------------------------------------- /examples/governance/10_claim_reward.py: -------------------------------------------------------------------------------- 1 | from examples.v2.utils import get_algod 2 | from tinyman.governance.client import TinymanGovernanceTestnetClient 3 | 4 | from tinyman.governance.constants import TESTNET_TINY_ASSET_ID 5 | from tinyman.governance.rewards.utils import group_adjacent_period_indexes 6 | 7 | # Hardcoding account keys is not a great practice. This is for demonstration purposes only. 8 | # See the README & Docs for alternative signing methods. 9 | account = { 10 | "address": "ALGORAND_ADDRESS_HERE", 11 | "private_key": "base64_private_key_here", 12 | } 13 | 14 | algod = get_algod() 15 | 16 | 17 | def get_tiny_balance(address): 18 | account_info = algod.account_info(account["address"]) 19 | assets = {a["asset-id"]: a for a in account_info["assets"]} 20 | return assets.get(TESTNET_TINY_ASSET_ID, {}).get("amount", 0) 21 | 22 | 23 | # Client 24 | governance_client = TinymanGovernanceTestnetClient( 25 | algod_client=algod, 26 | user_address=account["address"] 27 | ) 28 | 29 | print("TINY balance before TXN:", get_tiny_balance(account["address"])) 30 | 31 | pending_reward_period_indexes = governance_client.get_pending_reward_period_indexes() 32 | print(pending_reward_period_indexes) 33 | index_groups = group_adjacent_period_indexes(pending_reward_period_indexes) 34 | 35 | for index_group in index_groups: 36 | print("Index Group:", index_group) 37 | txn_group = governance_client.prepare_claim_reward_transactions( 38 | period_index_start=index_group[0], 39 | period_count=len(index_group), 40 | ) 41 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 42 | txn_group.submit(algod, wait=True) 43 | 44 | account_state = governance_client.fetch_account_state() 45 | print("TINY balance after TXN:", get_tiny_balance(account["address"])) 46 | -------------------------------------------------------------------------------- /tinyman/v2/bootstrap.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from tinyman.compat import ( 4 | ApplicationOptInTxn, 5 | PaymentTxn, 6 | SuggestedParams, 7 | ) 8 | from algosdk.logic import get_application_address 9 | 10 | from tinyman.utils import TransactionGroup 11 | from .constants import BOOTSTRAP_APP_ARGUMENT 12 | from .contracts import get_pool_logicsig 13 | 14 | 15 | def prepare_bootstrap_transactions( 16 | validator_app_id: int, 17 | asset_1_id: int, 18 | asset_2_id: int, 19 | sender: str, 20 | app_call_fee: int, 21 | required_algo: int, 22 | suggested_params: SuggestedParams, 23 | app_call_note: Optional[str] = None, 24 | ) -> TransactionGroup: 25 | pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) 26 | pool_address = pool_logicsig.address() 27 | assert asset_1_id > asset_2_id 28 | 29 | txns = list() 30 | 31 | # Fund pool account to cover minimum balance and fee requirements 32 | if required_algo: 33 | txns.append( 34 | PaymentTxn( 35 | sender=sender, 36 | sp=suggested_params, 37 | receiver=pool_address, 38 | amt=required_algo, 39 | ) 40 | ) 41 | 42 | # Bootstrap (Opt-in) App Call 43 | bootstrap_app_call = ApplicationOptInTxn( 44 | sender=pool_address, 45 | sp=suggested_params, 46 | index=validator_app_id, 47 | app_args=[BOOTSTRAP_APP_ARGUMENT], 48 | foreign_assets=[asset_1_id, asset_2_id], 49 | rekey_to=get_application_address(validator_app_id), 50 | note=app_call_note, 51 | ) 52 | bootstrap_app_call.fee = app_call_fee 53 | txns.append(bootstrap_app_call) 54 | 55 | txn_group = TransactionGroup(txns) 56 | txn_group.sign_with_logicsig(pool_logicsig) 57 | return txn_group 58 | -------------------------------------------------------------------------------- /tinyman/v1/redeem.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from tinyman.compat import ApplicationNoOpTxn, PaymentTxn, AssetTransferTxn 4 | 5 | from tinyman.utils import TransactionGroup 6 | from .contracts import get_pool_logicsig 7 | 8 | 9 | def prepare_redeem_transactions( 10 | validator_app_id, 11 | asset1_id, 12 | asset2_id, 13 | liquidity_asset_id, 14 | asset_id, 15 | asset_amount, 16 | sender, 17 | suggested_params, 18 | app_call_note: Optional[str] = None, 19 | ): 20 | pool_logicsig = get_pool_logicsig(validator_app_id, asset1_id, asset2_id) 21 | pool_address = pool_logicsig.address() 22 | 23 | txns = [ 24 | PaymentTxn( 25 | sender=sender, 26 | sp=suggested_params, 27 | receiver=pool_address, 28 | amt=2000, 29 | note="fee", 30 | ), 31 | ApplicationNoOpTxn( 32 | sender=pool_address, 33 | sp=suggested_params, 34 | index=validator_app_id, 35 | app_args=["redeem"], 36 | accounts=[sender], 37 | foreign_assets=[asset1_id, liquidity_asset_id] 38 | if asset2_id == 0 39 | else [asset1_id, asset2_id, liquidity_asset_id], 40 | note=app_call_note, 41 | ), 42 | AssetTransferTxn( 43 | sender=pool_address, 44 | sp=suggested_params, 45 | receiver=sender, 46 | amt=int(asset_amount), 47 | index=asset_id, 48 | ) 49 | if asset_id != 0 50 | else PaymentTxn( 51 | sender=pool_address, 52 | sp=suggested_params, 53 | receiver=sender, 54 | amt=int(asset_amount), 55 | ), 56 | ] 57 | txn_group = TransactionGroup(txns) 58 | txn_group.sign_with_logicsig(pool_logicsig) 59 | return txn_group 60 | -------------------------------------------------------------------------------- /examples/v2/tutorial/07_remove_liquidity.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | from pprint import pprint 5 | from urllib.parse import quote_plus 6 | 7 | from examples.v2.tutorial.common import get_account, get_assets 8 | from examples.v2.utils import get_algod 9 | from tinyman.v2.client import TinymanV2TestnetClient 10 | 11 | 12 | account = get_account() 13 | algod = get_algod() 14 | client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) 15 | 16 | ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] 17 | ASSET_A = client.fetch_asset(ASSET_A_ID) 18 | ASSET_B = client.fetch_asset(ASSET_B_ID) 19 | pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) 20 | 21 | position = pool.fetch_pool_position() 22 | pool_token_asset_in = position[pool.pool_token_asset].amount // 4 23 | 24 | quote = pool.fetch_remove_liquidity_quote( 25 | pool_token_asset_in=pool_token_asset_in, 26 | ) 27 | 28 | print("\nRemove Liquidity Quote:") 29 | print(quote) 30 | 31 | txn_group = pool.prepare_remove_liquidity_transactions_from_quote(quote=quote) 32 | 33 | # Sign 34 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 35 | 36 | # Submit transactions to the network and wait for confirmation 37 | txn_info = client.submit(txn_group, wait=True) 38 | print("Transaction Info") 39 | pprint(txn_info) 40 | 41 | print( 42 | f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" 43 | ) 44 | 45 | pool.refresh() 46 | 47 | pool_position = pool.fetch_pool_position() 48 | share = pool_position["share"] * 100 49 | print(f"Pool Tokens: {pool_position[pool.pool_token_asset]}") 50 | print(f"Assets: {pool_position[ASSET_A]}, {pool_position[ASSET_B]}") 51 | print(f"Share of pool: {share:.3f}%") 52 | -------------------------------------------------------------------------------- /tinyman/v1/contracts.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | import importlib.resources 4 | from tinyman.compat import LogicSigAccount 5 | import tinyman.v1 6 | from base64 import b64decode 7 | from tinyman.utils import encode_value 8 | 9 | if sys.version_info >= (3, 9): 10 | _contracts = json.loads( 11 | importlib.resources.files(tinyman.v1).joinpath("asc.json").read_text() 12 | ) 13 | else: 14 | _contracts = json.loads(importlib.resources.read_text(tinyman.v1, "asc.json")) 15 | 16 | pool_logicsig_def = _contracts["contracts"]["pool_logicsig"]["logic"] 17 | 18 | validator_app_def = _contracts["contracts"]["validator_app"] 19 | 20 | 21 | def get_program(definition, variables=None): 22 | """ 23 | Return a byte array to be used in LogicSig. 24 | """ 25 | template = definition["bytecode"] 26 | template_bytes = list(b64decode(template)) 27 | 28 | offset = 0 29 | for v in sorted(definition["variables"], key=lambda v: v["index"]): 30 | name = v["name"].split("TMPL_")[-1].lower() 31 | value = variables[name] 32 | start = v["index"] - offset 33 | end = start + v["length"] 34 | value_encoded = encode_value(value, v["type"]) 35 | value_encoded_len = len(value_encoded) 36 | diff = v["length"] - value_encoded_len 37 | offset += diff 38 | template_bytes[start:end] = list(value_encoded) 39 | 40 | return bytes(template_bytes) 41 | 42 | 43 | def get_pool_logicsig(validator_app_id, asset1_id, asset2_id): 44 | assets = [asset1_id, asset2_id] 45 | asset_id_1 = max(assets) 46 | asset_id_2 = min(assets) 47 | program_bytes = get_program( 48 | pool_logicsig_def, 49 | variables=dict( 50 | validator_app_id=validator_app_id, 51 | asset_id_1=asset_id_1, 52 | asset_id_2=asset_id_2, 53 | ), 54 | ) 55 | return LogicSigAccount(program=program_bytes) 56 | -------------------------------------------------------------------------------- /asc.json: -------------------------------------------------------------------------------- 1 | { 2 | "repo": "https://github.com/tinymanorg/tinyman-staking", 3 | "ref": "main", 4 | "contracts": { 5 | "staking_app": { 6 | "type": "app", 7 | "approval_program": { 8 | "bytecode": "BSADAAEIJgcIZW5kX3RpbWUGYXNzZXRzBG1pbnMMdmVyaWZpY2F0aW9uCGJhbGFuY2UgAmlkD3Byb2dyYW1fY291bnRlcjEZIhJAAC8xGSMSQAAZMRmBAhJAAicxGYEEEkACITEZgQUSQAIfADYaAIAFc2V0dXASQAGZADYaAIAGY3JlYXRlEkAAijYaAIAGY29tbWl0EkAAfTYaAIAFY2xhaW0SQAEDNhoAgAZ1cGRhdGUSQAE8NhoAgA51cGRhdGVfcmV3YXJkcxJAAN82GgCADXVwZGF0ZV9hc3NldHMSQAD4NhoAgAtlbmRfcHJvZ3JhbRJAAPI2GgArEkAA9TYaAIALbG9nX2JhbGFuY2USQADvACNDIyhiMgcNRCMpYiJKJAtbNjAAEkAACiMISYEODERC/+tMSDUBIypiNAEkC1s1AjYaARdBAAg2GgEXNAIPRCI2MABwAERJFicETFCwNhoBFw9EgBN0aW55bWFuU3Rha2luZy92MTpiMQVRABMSRDEFVxMANQE0ASJbIycFYhJENAEkWzYwABJENAGBEFs2GgEXEkQjQyNDIoACcjE2GgFmIoACcjI2GgJmIoACcjM2GgNmIoACcjQ2GgRmIoACcjU2GgVmI0MiKTYaAWYiKjYaAmYjQyIoNhoBF2YjQyNDMgkxABJEIis2GgFmI0MiNjAAcABESUQWJwRMULAjQycGZCMINQEnBjQBZyInBTQBZiKAA3VybDYaAWYigA9yZXdhcmRfYXNzZXRfaWQ2GgIXZiKADXJld2FyZF9wZXJpb2Q2GgMXZiKACnN0YXJ0X3RpbWU2GgQXZiIoNhoFF2YiKTYaBmYiKjYaB2YjQyNDMgkxABJDMgkxABJDAA==", 9 | "address": "KJ3W4IB66Q4ZITCNVABJXAV4I4HKWSZIJMD6BTFATQAWO5AKOV5VMZ6OFI", 10 | "size": 658, 11 | "variables": [], 12 | "source": "https://github.com/tinymanorg/tinyman-staking/tree/main/contracts/staking.teal" 13 | }, 14 | "clear_program": { 15 | "bytecode": "BIEB", 16 | "address": "P7GEWDXXW5IONRW6XRIRVPJCT2XXEQGOBGG65VJPBUOYZEJCBZWTPHS3VQ", 17 | "size": 3, 18 | "variables": [], 19 | "source": "https://github.com/tinymanorg/tinyman-staking/tree/main/contracts/clear_state.teal" 20 | }, 21 | "global_state_schema": { 22 | "num_uints": 2, 23 | "num_byte_slices": 2 24 | }, 25 | "local_state_schema": { 26 | "num_uints": 5, 27 | "num_byte_slices": 11 28 | }, 29 | "name": "staking_app" 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /examples/governance/30_create_proposal.py: -------------------------------------------------------------------------------- 1 | from examples.v2.utils import get_algod 2 | from tinyman.governance.client import TinymanGovernanceTestnetClient 3 | from tinyman.governance.proposal_voting.transactions import generate_proposal_metadata 4 | from tinyman.governance.utils import generate_cid_from_proposal_metadata 5 | 6 | # Hardcoding account keys is not a great practice. This is for demonstration purposes only. 7 | # See the README & Docs for alternative signing methods. 8 | account = { 9 | "address": "ALGORAND_ADDRESS_HERE", 10 | "private_key": "base64_private_key_here", 11 | } 12 | algod = get_algod() 13 | 14 | # Client 15 | governance_client = TinymanGovernanceTestnetClient( 16 | algod_client=algod, 17 | user_address=account["address"], 18 | ) 19 | 20 | account_state = governance_client.fetch_account_state() 21 | print("Account State before TXN:", account_state) 22 | 23 | tiny_power = governance_client.get_tiny_power() 24 | print("TINY POWER:", tiny_power) 25 | total_tiny_power = governance_client.get_total_tiny_power() 26 | print("Total TINY POWER:", total_tiny_power) 27 | print(f"User TINY Power %{(tiny_power / total_tiny_power) * 100}") 28 | 29 | # Generate metadata and proposal ID 30 | metadata = generate_proposal_metadata( 31 | title="Proposal #3", 32 | description="Description #3", 33 | category="governance", 34 | discussion_url="http://www.discussion-url.com", 35 | poll_url="http://www.poll-url.com", 36 | ) 37 | print(metadata) 38 | proposal_id = generate_cid_from_proposal_metadata(metadata) 39 | 40 | # Upload metadata 41 | governance_client.upload_proposal_metadata(proposal_id, metadata) 42 | 43 | # Submit transactions 44 | txn_group = governance_client.prepare_create_proposal_transactions(proposal_id=proposal_id) 45 | txn_group.sign_with_private_key(address=account["address"], private_key=account["private_key"]) 46 | result = txn_group.submit(algod=algod, wait=True) 47 | print(result) 48 | -------------------------------------------------------------------------------- /examples/v2/tutorial/08_single_asset_remove_liquidity.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | from pprint import pprint 5 | from urllib.parse import quote_plus 6 | 7 | from examples.v2.tutorial.common import get_account, get_assets 8 | from examples.v2.utils import get_algod 9 | from tinyman.v2.client import TinymanV2TestnetClient 10 | 11 | 12 | account = get_account() 13 | algod = get_algod() 14 | client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) 15 | 16 | ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] 17 | ASSET_A = client.fetch_asset(ASSET_A_ID) 18 | ASSET_B = client.fetch_asset(ASSET_B_ID) 19 | pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) 20 | 21 | position = pool.fetch_pool_position() 22 | pool_token_asset_in = position[pool.pool_token_asset].amount // 8 23 | 24 | quote = pool.fetch_single_asset_remove_liquidity_quote( 25 | pool_token_asset_in=pool_token_asset_in, 26 | output_asset=pool.asset_1, 27 | ) 28 | 29 | print("\nSingle Asset Remove Liquidity Quote:") 30 | print(quote) 31 | 32 | txn_group = pool.prepare_remove_liquidity_transactions_from_quote(quote=quote) 33 | 34 | # Sign 35 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 36 | 37 | # Submit transactions to the network and wait for confirmation 38 | txn_info = client.submit(txn_group, wait=True) 39 | print("Transaction Info") 40 | pprint(txn_info) 41 | 42 | print( 43 | f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" 44 | ) 45 | 46 | pool.refresh() 47 | pool_position = pool.fetch_pool_position() 48 | share = pool_position["share"] * 100 49 | print(f"Pool Tokens: {pool_position[pool.pool_token_asset]}") 50 | print(f"Assets: {pool_position[ASSET_A]}, {pool_position[ASSET_B]}") 51 | print(f"Share of pool: {share:.3f}%") 52 | -------------------------------------------------------------------------------- /examples/v2/tutorial/04_add_initial_liquidity.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | from pprint import pprint 5 | from urllib.parse import quote_plus 6 | 7 | from tinyman.assets import AssetAmount 8 | 9 | from examples.v2.tutorial.common import get_account, get_assets 10 | from examples.v2.utils import get_algod 11 | from tinyman.v2.client import TinymanV2TestnetClient 12 | 13 | 14 | account = get_account() 15 | algod = get_algod() 16 | client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) 17 | 18 | ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] 19 | ASSET_A = client.fetch_asset(ASSET_A_ID) 20 | ASSET_B = client.fetch_asset(ASSET_B_ID) 21 | pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) 22 | 23 | # Opt-in to the pool token 24 | txn_group_1 = pool.prepare_pool_token_asset_optin_transactions() 25 | 26 | # Get initial add liquidity quote 27 | quote = pool.fetch_initial_add_liquidity_quote( 28 | amount_a=AssetAmount(pool.asset_1, 10_000_000), 29 | amount_b=AssetAmount(pool.asset_2, 10_000_000), 30 | ) 31 | print("Quote:") 32 | print(quote) 33 | 34 | txn_group_2 = pool.prepare_add_liquidity_transactions_from_quote(quote) 35 | 36 | # You can merge the transaction groups 37 | txn_group = txn_group_1 + txn_group_2 38 | 39 | # Sign 40 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 41 | 42 | # Submit transactions to the network and wait for confirmation 43 | txn_info = client.submit(txn_group, wait=True) 44 | print("Transaction Info") 45 | pprint(txn_info) 46 | 47 | print( 48 | f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" 49 | ) 50 | 51 | pool.refresh() 52 | pool_position = pool.fetch_pool_position() 53 | share = pool_position["share"] * 100 54 | print(f"Pool Tokens: {pool_position[pool.pool_token_asset]}") 55 | print(f"Assets: {pool_position[ASSET_A]}, {pool_position[ASSET_B]}") 56 | print(f"Share of pool: {share:.3f}%") 57 | -------------------------------------------------------------------------------- /tinyman/governance/vault/utils.py: -------------------------------------------------------------------------------- 1 | from tinyman.governance.constants import WEEK 2 | from tinyman.governance.vault.constants import TWO_TO_THE_64, MAX_LOCK_TIME 3 | 4 | 5 | def get_slope(locked_amount): 6 | return locked_amount * TWO_TO_THE_64 // MAX_LOCK_TIME 7 | 8 | 9 | def get_bias(slope, time_delta): 10 | assert time_delta >= 0 11 | return (slope * time_delta) // TWO_TO_THE_64 12 | 13 | 14 | def get_start_timestamp_of_week(value): 15 | return (value // WEEK) * WEEK 16 | 17 | 18 | def get_cumulative_power_delta(bias: int, slope: int, time_delta: int) -> int: 19 | bias_delta = get_bias(slope, time_delta) 20 | 21 | if bias_delta > bias: 22 | if slope: 23 | cumulative_power_delta = ((bias * bias) * TWO_TO_THE_64) // (slope * 2) 24 | else: 25 | cumulative_power_delta = 0 26 | else: 27 | new_bias = bias - bias_delta 28 | cumulative_power_delta = (bias + new_bias) * time_delta // 2 29 | return cumulative_power_delta 30 | 31 | 32 | def get_cumulative_power(old_bias: int, new_bias: int, time_delta: int) -> int: 33 | """Calculate the cumulative power between two biases over a time period. Reference: vault_approval.get_cumulative_power_1()""" 34 | return (old_bias + new_bias) * time_delta // 2 35 | 36 | 37 | def get_cumulative_power_2(bias: int, slope: int): 38 | """Calculate the cumulative power through the end of lock. Reference: vault_approval.get_cumulative_power_2()""" 39 | return ((bias * bias) * TWO_TO_THE_64) // (slope * 2) 40 | 41 | 42 | def get_new_total_power_timestamps(old_timestamp, new_timestamp): 43 | assert old_timestamp <= new_timestamp 44 | 45 | timestamps = [] 46 | week_timestamp = get_start_timestamp_of_week(old_timestamp) + WEEK 47 | while week_timestamp < new_timestamp: 48 | timestamps.append(week_timestamp) 49 | week_timestamp += WEEK 50 | timestamps.append(new_timestamp) 51 | 52 | return timestamps 53 | 54 | 55 | def get_new_total_power_count(old_timestamp, new_timestamp): 56 | return len(get_new_total_power_timestamps(old_timestamp, new_timestamp)) 57 | -------------------------------------------------------------------------------- /tinyman/v2/constants.py: -------------------------------------------------------------------------------- 1 | from algosdk.logic import get_application_address 2 | 3 | POOL_LOGICSIG_TEMPLATE = ( 4 | "BoAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgQBbNQA0ADEYEkQxGYEBEkSBAUM=" 5 | ) 6 | 7 | BOOTSTRAP_APP_ARGUMENT = b"bootstrap" 8 | ADD_LIQUIDITY_APP_ARGUMENT = b"add_liquidity" 9 | ADD_INITIAL_LIQUIDITY_APP_ARGUMENT = b"add_initial_liquidity" 10 | REMOVE_LIQUIDITY_APP_ARGUMENT = b"remove_liquidity" 11 | SWAP_APP_ARGUMENT = b"swap" 12 | FLASH_LOAN_APP_ARGUMENT = b"flash_loan" 13 | VERIFY_FLASH_LOAN_APP_ARGUMENT = b"verify_flash_loan" 14 | FLASH_SWAP_APP_ARGUMENT = b"flash_swap" 15 | VERIFY_FLASH_SWAP_APP_ARGUMENT = b"verify_flash_swap" 16 | CLAIM_FEES_APP_ARGUMENT = b"claim_fees" 17 | CLAIM_EXTRA_APP_ARGUMENT = b"claim_extra" 18 | SET_FEE_APP_ARGUMENT = b"set_fee" 19 | SET_FEE_COLLECTOR_APP_ARGUMENT = b"set_fee_collector" 20 | SET_FEE_SETTER_APP_ARGUMENT = b"set_fee_setter" 21 | SET_FEE_MANAGER_APP_ARGUMENT = b"set_fee_manager" 22 | 23 | FIXED_INPUT_APP_ARGUMENT = b"fixed-input" 24 | FIXED_OUTPUT_APP_ARGUMENT = b"fixed-output" 25 | 26 | ADD_LIQUIDITY_FLEXIBLE_MODE_APP_ARGUMENT = b"flexible" 27 | ADD_LIQUIDITY_SINGLE_MODE_APP_ARGUMENT = b"single" 28 | 29 | 30 | TESTNET_VALIDATOR_APP_ID_V2 = 148607000 31 | MAINNET_VALIDATOR_APP_ID_V2 = 1002541853 32 | 33 | TESTNET_VALIDATOR_APP_ID = TESTNET_VALIDATOR_APP_ID_V2 34 | MAINNET_VALIDATOR_APP_ID = MAINNET_VALIDATOR_APP_ID_V2 35 | 36 | TESTNET_VALIDATOR_APP_ADDRESS = get_application_address(TESTNET_VALIDATOR_APP_ID) 37 | # MAINNET_VALIDATOR_APP_ADDRESS = get_application_address(MAINNET_VALIDATOR_APP_ID) 38 | 39 | LOCKED_POOL_TOKENS = 1000 40 | ASSET_MIN_TOTAL = 1000000 41 | 42 | # State 43 | APP_LOCAL_INTS = 12 44 | APP_LOCAL_BYTES = 2 45 | APP_GLOBAL_INTS = 0 46 | APP_GLOBAL_BYTES = 3 47 | 48 | # 100,000 Algo 49 | # + 100,000 ASA 1 50 | # + 100,000 ASA 2 51 | # + 100,000 Pool Token 52 | # + 542,500 App Optin (100000 + (25000+3500)*12 + (25000+25000)*2) 53 | MIN_POOL_BALANCE_ASA_ALGO_PAIR = 300_000 + ( 54 | 100_000 + (25_000 + 3_500) * APP_LOCAL_INTS + (25_000 + 25_000) * APP_LOCAL_BYTES 55 | ) 56 | MIN_POOL_BALANCE_ASA_ASA_PAIR = MIN_POOL_BALANCE_ASA_ALGO_PAIR + 100_000 57 | -------------------------------------------------------------------------------- /examples/folks_lending/remove_liquidity.py: -------------------------------------------------------------------------------- 1 | from algosdk.v2client.algod import AlgodClient 2 | 3 | from tinyman.folks_lending.constants import ( 4 | TESTNET_FOLKS_POOL_MANAGER_APP_ID, 5 | TESTNET_FOLKS_WRAPPER_LENDING_POOL_APP_ID) 6 | from tinyman.folks_lending.transactions import \ 7 | prepare_remove_liquidity_transaction_group 8 | from tinyman.folks_lending.utils import get_lending_pools 9 | from tinyman.v2.client import TinymanV2TestnetClient 10 | from tinyman.v2.constants import TESTNET_VALIDATOR_APP_ID_V2 11 | 12 | algod = AlgodClient("", "https://testnet-api.algonode.network") 13 | account_sk, account_address = ('YOUR PRIVATE KEY HERE', 'YOUR ADDRESS HERE') 14 | client = TinymanV2TestnetClient(algod_client=algod, user_address=account_address) 15 | 16 | asset_1_id = 67395862 # USDC 17 | asset_2_id = 0 # Algo 18 | 19 | # Get f_asset ids 20 | 21 | folks_pools = get_lending_pools(algod, TESTNET_FOLKS_POOL_MANAGER_APP_ID) 22 | temp = dict() 23 | for folks_pool in folks_pools: 24 | temp[folks_pool['asset_id']] = folks_pool 25 | folks_pools = temp 26 | 27 | f_asset_1_id = folks_pools[asset_1_id]['f_asset_id'] 28 | f_asset_2_id = folks_pools[asset_2_id]['f_asset_id'] 29 | 30 | pool = client.fetch_pool(f_asset_1_id, f_asset_2_id, fetch=True) 31 | 32 | # Remove liquidity 33 | 34 | txn_group = prepare_remove_liquidity_transaction_group( 35 | sender=account_address, 36 | suggested_params=algod.suggested_params(), 37 | wrapper_app_id=TESTNET_FOLKS_WRAPPER_LENDING_POOL_APP_ID, 38 | tinyman_amm_app_id=TESTNET_VALIDATOR_APP_ID_V2, 39 | lending_app_1_id=folks_pools[asset_1_id]['pool_app_id'], 40 | lending_app_2_id=folks_pools[asset_2_id]['pool_app_id'], 41 | lending_manager_app_id=TESTNET_FOLKS_POOL_MANAGER_APP_ID, 42 | tinyman_pool_address=pool.address, 43 | asset_1_id=asset_1_id, 44 | asset_2_id=asset_2_id, 45 | f_asset_1_id=f_asset_1_id, 46 | f_asset_2_id=f_asset_2_id, 47 | liquidity_token_id=pool.pool_token_asset.id, 48 | liquidity_token_amount=1_000_000 49 | ) 50 | 51 | txn_group.sign_with_private_key(account_address, account_sk) 52 | txn_group.submit(algod, True) 53 | -------------------------------------------------------------------------------- /examples/folks_lending/add_liquidity.py: -------------------------------------------------------------------------------- 1 | from algosdk.v2client.algod import AlgodClient 2 | 3 | from tinyman.folks_lending.constants import ( 4 | TESTNET_FOLKS_POOL_MANAGER_APP_ID, 5 | TESTNET_FOLKS_WRAPPER_LENDING_POOL_APP_ID) 6 | from tinyman.folks_lending.transactions import \ 7 | prepare_add_liquidity_transaction_group 8 | from tinyman.folks_lending.utils import get_lending_pools 9 | from tinyman.v2.client import TinymanV2TestnetClient 10 | from tinyman.v2.constants import TESTNET_VALIDATOR_APP_ID_V2 11 | 12 | algod = AlgodClient("", "https://testnet-api.algonode.network") 13 | account_sk, account_address = ('YOUR PRIVATE KEY HERE', 'YOUR ADDRESS HERE') 14 | client = TinymanV2TestnetClient(algod_client=algod, user_address=account_address) 15 | 16 | asset_1_id = 67395862 # USDC 17 | asset_2_id = 0 # Algo 18 | 19 | # Get f_asset ids 20 | 21 | folks_pools = get_lending_pools(algod, TESTNET_FOLKS_POOL_MANAGER_APP_ID) 22 | temp = dict() 23 | for folks_pool in folks_pools: 24 | temp[folks_pool['asset_id']] = folks_pool 25 | folks_pools = temp 26 | 27 | f_asset_1_id = folks_pools[asset_1_id]['f_asset_id'] 28 | f_asset_2_id = folks_pools[asset_2_id]['f_asset_id'] 29 | 30 | pool = client.fetch_pool(f_asset_1_id, f_asset_2_id, fetch=True) 31 | 32 | # Add liquidity 33 | 34 | txn_group = prepare_add_liquidity_transaction_group( 35 | sender=account_address, 36 | suggested_params=algod.suggested_params(), 37 | wrapper_app_id=TESTNET_FOLKS_WRAPPER_LENDING_POOL_APP_ID, 38 | tinyman_amm_app_id=TESTNET_VALIDATOR_APP_ID_V2, 39 | lending_app_1_id=folks_pools[asset_1_id]['pool_app_id'], 40 | lending_app_2_id=folks_pools[asset_2_id]['pool_app_id'], 41 | lending_manager_app_id=TESTNET_FOLKS_POOL_MANAGER_APP_ID, 42 | tinyman_pool_address=pool.address, 43 | asset_1_id=asset_1_id, 44 | asset_2_id=asset_2_id, 45 | f_asset_1_id=f_asset_1_id, 46 | f_asset_2_id=f_asset_2_id, 47 | liquidity_token_id=pool.pool_token_asset.id, 48 | asset_1_amount=10000, 49 | asset_2_amount=10000 50 | ) 51 | 52 | txn_group.sign_with_private_key(account_address, account_sk) 53 | txn_group.submit(algod, True) 54 | -------------------------------------------------------------------------------- /examples/v2/tutorial/common.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import string 4 | import random 5 | from pprint import pprint 6 | 7 | from tinyman.compat import AssetCreateTxn, wait_for_confirmation 8 | 9 | 10 | def get_account_file_path(filename="account.json"): 11 | dir_path = os.path.dirname(os.path.realpath(__file__)) 12 | file_path = os.path.join(dir_path, filename) 13 | return file_path 14 | 15 | 16 | def get_account(filename="account.json"): 17 | file_path = get_account_file_path(filename) 18 | try: 19 | with open(file_path, "r", encoding="utf-8") as f: 20 | account = json.loads(f.read()) 21 | except FileNotFoundError: 22 | raise Exception("Please run generate_account.py to generate a test account.") 23 | 24 | return account 25 | 26 | 27 | def get_assets_file_path(filename="assets.json"): 28 | dir_path = os.path.dirname(os.path.realpath(__file__)) 29 | file_path = os.path.join(dir_path, filename) 30 | return file_path 31 | 32 | 33 | def get_assets(filename="assets.json"): 34 | file_path = get_account_file_path(filename) 35 | try: 36 | with open(file_path, "r", encoding="utf-8") as f: 37 | assets = json.loads(f.read()) 38 | except FileNotFoundError: 39 | raise Exception("Please run generate_account.py to generate a test account.") 40 | 41 | return assets 42 | 43 | 44 | def create_asset(algod, sender, private_key): 45 | sp = algod.suggested_params() 46 | asset_name = "".join( 47 | random.choice(string.ascii_uppercase + string.digits) for _ in range(8) 48 | ) 49 | max_total = 2**64 - 1 50 | txn = AssetCreateTxn( 51 | sender=sender, 52 | sp=sp, 53 | total=max_total, 54 | decimals=6, 55 | default_frozen=False, 56 | unit_name=asset_name, 57 | asset_name=asset_name, 58 | ) 59 | signed_txn = txn.sign(private_key) 60 | transaction_id = algod.send_transaction(signed_txn) 61 | print(f"Asset Creation Transaction ID: {transaction_id}") 62 | result = wait_for_confirmation(algod, transaction_id) 63 | print("Asset Creation Result:") 64 | pprint(result) 65 | asset_id = result["asset-index"] 66 | print(f"Created Asset ID: {asset_id}") 67 | return asset_id 68 | -------------------------------------------------------------------------------- /tinyman/v2/management.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from tinyman.compat import ( 4 | ApplicationNoOpTxn, 5 | SuggestedParams, 6 | ) 7 | 8 | from tinyman.utils import TransactionGroup 9 | from tinyman.v2.constants import ( 10 | SET_FEE_COLLECTOR_APP_ARGUMENT, 11 | SET_FEE_SETTER_APP_ARGUMENT, 12 | SET_FEE_MANAGER_APP_ARGUMENT, 13 | ) 14 | 15 | 16 | def prepare_set_fee_collector_transactions( 17 | validator_app_id: int, 18 | fee_manager: str, 19 | new_fee_collector: str, 20 | suggested_params: SuggestedParams, 21 | app_call_note: Optional[str] = None, 22 | ) -> TransactionGroup: 23 | txns = [ 24 | ApplicationNoOpTxn( 25 | sender=fee_manager, 26 | sp=suggested_params, 27 | index=validator_app_id, 28 | app_args=[SET_FEE_COLLECTOR_APP_ARGUMENT], 29 | accounts=[new_fee_collector], 30 | note=app_call_note, 31 | ), 32 | ] 33 | txn_group = TransactionGroup(txns) 34 | return txn_group 35 | 36 | 37 | def prepare_set_fee_setter_transactions( 38 | validator_app_id: int, 39 | fee_manager: str, 40 | new_fee_setter: str, 41 | suggested_params: SuggestedParams, 42 | app_call_note: Optional[str] = None, 43 | ) -> TransactionGroup: 44 | txns = [ 45 | ApplicationNoOpTxn( 46 | sender=fee_manager, 47 | sp=suggested_params, 48 | index=validator_app_id, 49 | app_args=[SET_FEE_SETTER_APP_ARGUMENT], 50 | accounts=[new_fee_setter], 51 | note=app_call_note, 52 | ), 53 | ] 54 | txn_group = TransactionGroup(txns) 55 | return txn_group 56 | 57 | 58 | def prepare_set_fee_manager_transactions( 59 | validator_app_id: int, 60 | fee_manager: str, 61 | new_fee_manager: str, 62 | suggested_params: SuggestedParams, 63 | app_call_note: Optional[str] = None, 64 | ) -> TransactionGroup: 65 | txns = [ 66 | ApplicationNoOpTxn( 67 | sender=fee_manager, 68 | sp=suggested_params, 69 | index=validator_app_id, 70 | app_args=[SET_FEE_MANAGER_APP_ARGUMENT], 71 | accounts=[new_fee_manager], 72 | note=app_call_note, 73 | ), 74 | ] 75 | txn_group = TransactionGroup(txns) 76 | return txn_group 77 | -------------------------------------------------------------------------------- /examples/v2/tutorial/11_flash_loan_1_single_asset.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | from pprint import pprint 5 | from urllib.parse import quote_plus 6 | 7 | from tinyman.assets import AssetAmount 8 | 9 | from examples.v2.tutorial.common import get_account, get_assets 10 | from examples.v2.utils import get_algod 11 | from tinyman.v2.client import TinymanV2TestnetClient 12 | from tinyman.compat import AssetTransferTxn 13 | 14 | account = get_account() 15 | algod = get_algod() 16 | client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) 17 | 18 | ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] 19 | ASSET_A = client.fetch_asset(ASSET_A_ID) 20 | ASSET_B = client.fetch_asset(ASSET_B_ID) 21 | pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) 22 | 23 | position = pool.fetch_pool_position() 24 | 25 | quote = pool.fetch_flash_loan_quote( 26 | loan_amount_a=AssetAmount(pool.asset_1, 1_000_000), 27 | loan_amount_b=AssetAmount(pool.asset_2, 0), 28 | ) 29 | 30 | print("\nQuote:") 31 | print(quote) 32 | 33 | account_info = algod.account_info(account["address"]) 34 | 35 | for asset in account_info["assets"]: 36 | if asset["asset-id"] == pool.asset_1.id: 37 | asset_1_balance = asset["amount"] 38 | 39 | # Transfer amount is equal to sum of initial account balance and loan amount 40 | # this transaction demonstrates that you can use the total amount 41 | transactions = [ 42 | AssetTransferTxn( 43 | sender=account["address"], 44 | sp=algod.suggested_params(), 45 | receiver=account["address"], 46 | amt=asset_1_balance + quote.amounts_out[pool.asset_1].amount, 47 | index=pool.asset_1.id, 48 | ) 49 | ] 50 | 51 | txn_group = pool.prepare_flash_loan_transactions_from_quote( 52 | quote=quote, transactions=transactions 53 | ) 54 | 55 | # Sign 56 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 57 | 58 | # Submit transactions to the network and wait for confirmation 59 | txn_info = client.submit(txn_group, wait=True) 60 | print("Transaction Info") 61 | pprint(txn_info) 62 | 63 | print( 64 | f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" 65 | ) 66 | -------------------------------------------------------------------------------- /tinyman/v1/bootstrap.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from tinyman.compat import ( 4 | ApplicationOptInTxn, 5 | PaymentTxn, 6 | AssetCreateTxn, 7 | AssetOptInTxn, 8 | ) 9 | 10 | from tinyman.utils import int_to_bytes, TransactionGroup 11 | from .contracts import get_pool_logicsig 12 | 13 | 14 | def prepare_bootstrap_transactions( 15 | validator_app_id, 16 | asset1_id, 17 | asset2_id, 18 | asset1_unit_name, 19 | asset2_unit_name, 20 | sender, 21 | suggested_params, 22 | app_call_note: Optional[str] = None, 23 | ): 24 | pool_logicsig = get_pool_logicsig(validator_app_id, asset1_id, asset2_id) 25 | pool_address = pool_logicsig.address() 26 | 27 | assert asset1_id > asset2_id 28 | 29 | if asset2_id == 0: 30 | asset2_unit_name = "ALGO" 31 | 32 | txns = [ 33 | PaymentTxn( 34 | sender=sender, 35 | sp=suggested_params, 36 | receiver=pool_address, 37 | amt=961000 if asset2_id > 0 else 860000, 38 | note="fee", 39 | ), 40 | ApplicationOptInTxn( 41 | sender=pool_address, 42 | sp=suggested_params, 43 | index=validator_app_id, 44 | app_args=["bootstrap", int_to_bytes(asset1_id), int_to_bytes(asset2_id)], 45 | foreign_assets=[asset1_id] if asset2_id == 0 else [asset1_id, asset2_id], 46 | note=app_call_note, 47 | ), 48 | AssetCreateTxn( 49 | sender=pool_address, 50 | sp=suggested_params, 51 | total=0xFFFFFFFFFFFFFFFF, 52 | decimals=6, 53 | unit_name="TMPOOL11", 54 | asset_name=f"TinymanPool1.1 {asset1_unit_name}-{asset2_unit_name}", 55 | url="https://tinyman.org", 56 | default_frozen=False, 57 | ), 58 | AssetOptInTxn( 59 | sender=pool_address, 60 | sp=suggested_params, 61 | index=asset1_id, 62 | ), 63 | ] 64 | if asset2_id > 0: 65 | txns += [ 66 | AssetOptInTxn( 67 | sender=pool_address, 68 | sp=suggested_params, 69 | index=asset2_id, 70 | ) 71 | ] 72 | txn_group = TransactionGroup(txns) 73 | txn_group.sign_with_logicsig(pool_logicsig) 74 | return txn_group 75 | -------------------------------------------------------------------------------- /tinyman/v1/burn.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from tinyman.compat import ApplicationNoOpTxn, PaymentTxn, AssetTransferTxn 4 | 5 | from tinyman.utils import TransactionGroup 6 | from .contracts import get_pool_logicsig 7 | 8 | 9 | def prepare_burn_transactions( 10 | validator_app_id, 11 | asset1_id, 12 | asset2_id, 13 | liquidity_asset_id, 14 | asset1_amount, 15 | asset2_amount, 16 | liquidity_asset_amount, 17 | sender, 18 | suggested_params, 19 | app_call_note: Optional[str] = None, 20 | ): 21 | pool_logicsig = get_pool_logicsig(validator_app_id, asset1_id, asset2_id) 22 | pool_address = pool_logicsig.address() 23 | 24 | txns = [ 25 | PaymentTxn( 26 | sender=sender, 27 | sp=suggested_params, 28 | receiver=pool_address, 29 | amt=3000, 30 | note="fee", 31 | ), 32 | ApplicationNoOpTxn( 33 | sender=pool_address, 34 | sp=suggested_params, 35 | index=validator_app_id, 36 | app_args=["burn"], 37 | accounts=[sender], 38 | foreign_assets=[asset1_id, liquidity_asset_id] 39 | if asset2_id == 0 40 | else [asset1_id, asset2_id, liquidity_asset_id], 41 | note=app_call_note, 42 | ), 43 | AssetTransferTxn( 44 | sender=pool_address, 45 | sp=suggested_params, 46 | receiver=sender, 47 | amt=int(asset1_amount), 48 | index=asset1_id, 49 | ), 50 | AssetTransferTxn( 51 | sender=pool_address, 52 | sp=suggested_params, 53 | receiver=sender, 54 | amt=int(asset2_amount), 55 | index=asset2_id, 56 | ) 57 | if asset2_id != 0 58 | else PaymentTxn( 59 | sender=pool_address, 60 | sp=suggested_params, 61 | receiver=sender, 62 | amt=int(asset2_amount), 63 | ), 64 | AssetTransferTxn( 65 | sender=sender, 66 | sp=suggested_params, 67 | receiver=pool_address, 68 | amt=int(liquidity_asset_amount), 69 | index=liquidity_asset_id, 70 | ), 71 | ] 72 | txn_group = TransactionGroup(txns) 73 | txn_group.sign_with_logicsig(pool_logicsig) 74 | return txn_group 75 | -------------------------------------------------------------------------------- /tinyman/v1/mint.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from tinyman.compat import ApplicationNoOpTxn, PaymentTxn, AssetTransferTxn 4 | 5 | from tinyman.utils import TransactionGroup 6 | from .contracts import get_pool_logicsig 7 | 8 | 9 | def prepare_mint_transactions( 10 | validator_app_id, 11 | asset1_id, 12 | asset2_id, 13 | liquidity_asset_id, 14 | asset1_amount, 15 | asset2_amount, 16 | liquidity_asset_amount, 17 | sender, 18 | suggested_params, 19 | app_call_note: Optional[str] = None, 20 | ): 21 | pool_logicsig = get_pool_logicsig(validator_app_id, asset1_id, asset2_id) 22 | pool_address = pool_logicsig.address() 23 | 24 | txns = [ 25 | PaymentTxn( 26 | sender=sender, 27 | sp=suggested_params, 28 | receiver=pool_address, 29 | amt=2000, 30 | note="fee", 31 | ), 32 | ApplicationNoOpTxn( 33 | sender=pool_address, 34 | sp=suggested_params, 35 | index=validator_app_id, 36 | app_args=["mint"], 37 | accounts=[sender], 38 | foreign_assets=[asset1_id, liquidity_asset_id] 39 | if asset2_id == 0 40 | else [asset1_id, asset2_id, liquidity_asset_id], 41 | note=app_call_note, 42 | ), 43 | AssetTransferTxn( 44 | sender=sender, 45 | sp=suggested_params, 46 | receiver=pool_address, 47 | amt=int(asset1_amount), 48 | index=asset1_id, 49 | ), 50 | AssetTransferTxn( 51 | sender=sender, 52 | sp=suggested_params, 53 | receiver=pool_address, 54 | amt=int(asset2_amount), 55 | index=asset2_id, 56 | ) 57 | if asset2_id != 0 58 | else PaymentTxn( 59 | sender=sender, 60 | sp=suggested_params, 61 | receiver=pool_address, 62 | amt=int(asset2_amount), 63 | ), 64 | AssetTransferTxn( 65 | sender=pool_address, 66 | sp=suggested_params, 67 | receiver=sender, 68 | amt=int(liquidity_asset_amount), 69 | index=liquidity_asset_id, 70 | ), 71 | ] 72 | txn_group = TransactionGroup(txns) 73 | txn_group.sign_with_logicsig(pool_logicsig) 74 | return txn_group 75 | -------------------------------------------------------------------------------- /tinyman/swap_router/management.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from tinyman.compat import ( 4 | ApplicationNoOpTxn, 5 | SuggestedParams, 6 | ) 7 | from tinyman.swap_router.constants import ( 8 | CLAIM_EXTRA_APP_ARGUMENT, 9 | SET_MANAGER_APP_ARGUMENT, 10 | SET_EXTRA_COLLECTOR_APP_ARGUMENT, 11 | ) 12 | from tinyman.utils import TransactionGroup 13 | 14 | 15 | def prepare_claim_extra_transactions( 16 | router_app_id: int, 17 | asset_ids: [int], 18 | sender: str, 19 | suggested_params: SuggestedParams, 20 | app_call_note: Optional[str] = None, 21 | ) -> TransactionGroup: 22 | claim_extra_app_call = ApplicationNoOpTxn( 23 | sender=sender, 24 | sp=suggested_params, 25 | index=router_app_id, 26 | app_args=[CLAIM_EXTRA_APP_ARGUMENT], 27 | foreign_assets=asset_ids, 28 | note=app_call_note, 29 | ) 30 | min_fee = suggested_params.min_fee 31 | inner_transaction_count = len(asset_ids) 32 | claim_extra_app_call.fee = min_fee * (1 + inner_transaction_count) 33 | 34 | txn_group = TransactionGroup([claim_extra_app_call]) 35 | return txn_group 36 | 37 | 38 | def prepare_set_set_manager_transactions( 39 | router_app_id: int, 40 | manager: str, 41 | new_manager: str, 42 | suggested_params: SuggestedParams, 43 | app_call_note: Optional[str] = None, 44 | ) -> TransactionGroup: 45 | txns = [ 46 | ApplicationNoOpTxn( 47 | sender=manager, 48 | sp=suggested_params, 49 | index=router_app_id, 50 | app_args=[SET_MANAGER_APP_ARGUMENT], 51 | accounts=[new_manager], 52 | note=app_call_note, 53 | ), 54 | ] 55 | txn_group = TransactionGroup(txns) 56 | return txn_group 57 | 58 | 59 | def prepare_set_extra_collector_transactions( 60 | router_app_id: int, 61 | manager: str, 62 | new_extra_collector: str, 63 | suggested_params: SuggestedParams, 64 | app_call_note: Optional[str] = None, 65 | ) -> TransactionGroup: 66 | txns = [ 67 | ApplicationNoOpTxn( 68 | sender=manager, 69 | sp=suggested_params, 70 | index=router_app_id, 71 | app_args=[SET_EXTRA_COLLECTOR_APP_ARGUMENT], 72 | accounts=[new_extra_collector], 73 | note=app_call_note, 74 | ), 75 | ] 76 | txn_group = TransactionGroup(txns) 77 | return txn_group 78 | -------------------------------------------------------------------------------- /tinyman/v2/flash_swap.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from tinyman.compat import ( 4 | Transaction, 5 | ApplicationNoOpTxn, 6 | SuggestedParams, 7 | ) 8 | 9 | from tinyman.utils import TransactionGroup 10 | from .constants import ( 11 | FLASH_SWAP_APP_ARGUMENT, 12 | VERIFY_FLASH_SWAP_APP_ARGUMENT, 13 | ) 14 | from .contracts import get_pool_logicsig 15 | 16 | 17 | def prepare_flash_swap_transactions( 18 | validator_app_id: int, 19 | asset_1_id: int, 20 | asset_2_id: int, 21 | asset_1_loan_amount: int, 22 | asset_2_loan_amount: int, 23 | transactions: "list[Transaction]", 24 | sender: str, 25 | suggested_params: SuggestedParams, 26 | app_call_note: Optional[str] = None, 27 | ) -> TransactionGroup: 28 | assert asset_1_loan_amount or asset_2_loan_amount 29 | 30 | pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) 31 | pool_address = pool_logicsig.address() 32 | min_fee = suggested_params.min_fee 33 | 34 | if asset_1_loan_amount and asset_2_loan_amount: 35 | inner_transaction_count = 2 36 | else: 37 | inner_transaction_count = 1 38 | 39 | index_diff = len(transactions) + 1 40 | txns = [ 41 | # Flash Swap 42 | ApplicationNoOpTxn( 43 | sender=sender, 44 | sp=suggested_params, 45 | index=validator_app_id, 46 | app_args=[ 47 | FLASH_SWAP_APP_ARGUMENT, 48 | index_diff, 49 | asset_1_loan_amount, 50 | asset_2_loan_amount, 51 | ], 52 | foreign_assets=[asset_1_id, asset_2_id], 53 | accounts=[pool_address], 54 | note=app_call_note, 55 | ) 56 | ] 57 | # This app call contains inner transactions 58 | txns[0].fee = min_fee * (inner_transaction_count + 1) 59 | 60 | if transactions: 61 | txns.extend(transactions) 62 | 63 | # Verify Flash Swap 64 | txns.append( 65 | ApplicationNoOpTxn( 66 | sender=sender, 67 | sp=suggested_params, 68 | index=validator_app_id, 69 | app_args=[VERIFY_FLASH_SWAP_APP_ARGUMENT, index_diff], 70 | foreign_assets=[asset_1_id, asset_2_id], 71 | accounts=[pool_address], 72 | note=app_call_note, 73 | ) 74 | ) 75 | 76 | txn_group = TransactionGroup(txns) 77 | return txn_group 78 | -------------------------------------------------------------------------------- /examples/v1/add_liquidity1.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | 5 | from tinyman.v1.client import TinymanTestnetClient 6 | from algosdk.v2client.algod import AlgodClient 7 | 8 | 9 | # Hardcoding account keys is not a great practice. This is for demonstration purposes only. 10 | # See the README & Docs for alternative signing methods. 11 | account = { 12 | "address": "ALGORAND_ADDRESS_HERE", 13 | "private_key": "base64_private_key_here", # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary 14 | } 15 | 16 | algod = AlgodClient( 17 | "", "http://localhost:8080", headers={"User-Agent": "algosdk"} 18 | ) 19 | client = TinymanTestnetClient(algod_client=algod, user_address=account["address"]) 20 | # By default all subsequent operations are on behalf of user_address 21 | 22 | # Fetch our two assets of interest 23 | TINYUSDC = client.fetch_asset(21582668) 24 | ALGO = client.fetch_asset(0) 25 | 26 | # Fetch the pool we will work with 27 | pool = client.fetch_pool(TINYUSDC, ALGO) 28 | 29 | # Get a quote for supplying 1000.0 TinyUSDC 30 | quote = pool.fetch_mint_quote(TINYUSDC(1000_000_000), slippage=0.01) 31 | 32 | print(quote) 33 | 34 | # Check if we are happy with the quote.. 35 | if quote.amounts_in[ALGO] < 5_000_000: 36 | # Prepare the mint transactions from the quote and sign them 37 | transaction_group = pool.prepare_mint_transactions_from_quote(quote) 38 | transaction_group.sign_with_private_key(account["address"], account["private_key"]) 39 | result = client.submit(transaction_group, wait=True) 40 | 41 | # Check if any excess liquidity asset remaining after the mint 42 | excess = pool.fetch_excess_amounts() 43 | if pool.liquidity_asset in excess: 44 | amount = excess[pool.liquidity_asset] 45 | print(f"Excess: {amount}") 46 | if amount > 1_000_000: 47 | transaction_group = pool.prepare_redeem_transactions(amount) 48 | transaction_group.sign_with_private_key( 49 | account["address"], account["private_key"] 50 | ) 51 | result = client.submit(transaction_group, wait=True) 52 | 53 | info = pool.fetch_pool_position() 54 | share = info["share"] * 100 55 | print(f"Pool Tokens: {info[pool.liquidity_asset]}") 56 | print(f"Assets: {info[TINYUSDC]}, {info[ALGO]}") 57 | print(f"Share of pool: {share:.3f}%") 58 | -------------------------------------------------------------------------------- /examples/v2/tutorial/06_add_single_asset_liquidity.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | from pprint import pprint 5 | from urllib.parse import quote_plus 6 | 7 | from tinyman.assets import AssetAmount 8 | 9 | from examples.v2.tutorial.common import get_account, get_assets 10 | from examples.v2.utils import get_algod 11 | from tinyman.v2.client import TinymanV2TestnetClient 12 | 13 | 14 | account = get_account() 15 | algod = get_algod() 16 | client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) 17 | 18 | ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] 19 | ASSET_A = client.fetch_asset(ASSET_A_ID) 20 | ASSET_B = client.fetch_asset(ASSET_B_ID) 21 | pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) 22 | 23 | # Add flexible liquidity (advanced) 24 | # txn_group = pool.prepare_add_liquidity_transactions( 25 | # amounts_in={ 26 | # pool.asset_1: AssetAmount(pool.asset_1, 5_000_000), 27 | # pool.asset_2: AssetAmount(pool.asset_2, 7_000_000) 28 | # }, 29 | # min_pool_token_asset_amount="Do your own calculation" 30 | # ) 31 | 32 | quote = pool.fetch_single_asset_add_liquidity_quote( 33 | amount_a=AssetAmount(pool.asset_1, 10_000_000), 34 | ) 35 | 36 | print("\nAdd Liquidity Quote:") 37 | print(quote) 38 | 39 | txn_group = pool.prepare_add_liquidity_transactions_from_quote(quote=quote) 40 | 41 | if not client.asset_is_opted_in(asset_id=pool.pool_token_asset.id): 42 | # Opt-in to the pool token 43 | opt_in_txn_group = pool.prepare_pool_token_asset_optin_transactions() 44 | # You can merge the transaction groups 45 | txn_group = txn_group + opt_in_txn_group 46 | 47 | # Sign 48 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 49 | 50 | # Submit transactions to the network and wait for confirmation 51 | txn_info = client.submit(txn_group, wait=True) 52 | print("Transaction Info") 53 | pprint(txn_info) 54 | 55 | print( 56 | f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" 57 | ) 58 | 59 | pool.refresh() 60 | pool_position = pool.fetch_pool_position() 61 | share = pool_position["share"] * 100 62 | print(f"Pool Tokens: {pool_position[pool.pool_token_asset]}") 63 | print(f"Assets: {pool_position[ASSET_A]}, {pool_position[ASSET_B]}") 64 | print(f"Share of pool: {share:.3f}%") 65 | -------------------------------------------------------------------------------- /tinyman/v2/swap.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from tinyman.compat import ( 4 | ApplicationNoOpTxn, 5 | PaymentTxn, 6 | AssetTransferTxn, 7 | SuggestedParams, 8 | ) 9 | 10 | from tinyman.utils import TransactionGroup 11 | from .constants import ( 12 | SWAP_APP_ARGUMENT, 13 | FIXED_INPUT_APP_ARGUMENT, 14 | FIXED_OUTPUT_APP_ARGUMENT, 15 | ) 16 | from .contracts import get_pool_logicsig 17 | 18 | 19 | def prepare_swap_transactions( 20 | validator_app_id: int, 21 | asset_1_id: int, 22 | asset_2_id: int, 23 | asset_in_id: int, 24 | asset_in_amount: int, 25 | asset_out_amount: int, 26 | swap_type: [str, bytes], 27 | sender: str, 28 | suggested_params: SuggestedParams, 29 | app_call_note: Optional[str] = None, 30 | ) -> TransactionGroup: 31 | pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) 32 | pool_address = pool_logicsig.address() 33 | 34 | txns = [ 35 | AssetTransferTxn( 36 | sender=sender, 37 | sp=suggested_params, 38 | receiver=pool_address, 39 | index=asset_in_id, 40 | amt=asset_in_amount, 41 | ) 42 | if asset_in_id != 0 43 | else PaymentTxn( 44 | sender=sender, 45 | sp=suggested_params, 46 | receiver=pool_address, 47 | amt=asset_in_amount, 48 | ), 49 | ApplicationNoOpTxn( 50 | sender=sender, 51 | sp=suggested_params, 52 | index=validator_app_id, 53 | app_args=[SWAP_APP_ARGUMENT, swap_type, asset_out_amount], 54 | foreign_assets=[asset_1_id, asset_2_id], 55 | accounts=[pool_address], 56 | note=app_call_note, 57 | ), 58 | ] 59 | 60 | if isinstance(swap_type, bytes): 61 | pass 62 | elif isinstance(swap_type, str): 63 | swap_type = swap_type.encode() 64 | else: 65 | raise NotImplementedError() 66 | 67 | min_fee = suggested_params.min_fee 68 | if swap_type == FIXED_INPUT_APP_ARGUMENT: 69 | # App call contains 1 inner transaction 70 | app_call_fee = min_fee * 2 71 | elif swap_type == FIXED_OUTPUT_APP_ARGUMENT: 72 | # App call contains 2 inner transactions 73 | app_call_fee = min_fee * 3 74 | else: 75 | raise NotImplementedError() 76 | 77 | txns[-1].fee = app_call_fee 78 | txn_group = TransactionGroup(txns) 79 | return txn_group 80 | -------------------------------------------------------------------------------- /examples/v2/tutorial/05_add_flexible_liquidity.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | from pprint import pprint 5 | from urllib.parse import quote_plus 6 | 7 | from tinyman.assets import AssetAmount 8 | 9 | from examples.v2.tutorial.common import get_account, get_assets 10 | from examples.v2.utils import get_algod 11 | from tinyman.v2.client import TinymanV2TestnetClient 12 | 13 | 14 | account = get_account() 15 | algod = get_algod() 16 | client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) 17 | 18 | ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] 19 | ASSET_A = client.fetch_asset(ASSET_A_ID) 20 | ASSET_B = client.fetch_asset(ASSET_B_ID) 21 | pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) 22 | 23 | # Add flexible liquidity (advanced) 24 | # txn_group = pool.prepare_add_liquidity_transactions( 25 | # amounts_in={ 26 | # pool.asset_1: AssetAmount(pool.asset_1, 5_000_000), 27 | # pool.asset_2: AssetAmount(pool.asset_2, 7_000_000) 28 | # }, 29 | # min_pool_token_asset_amount="Do your own calculation" 30 | # ) 31 | 32 | quote = pool.fetch_flexible_add_liquidity_quote( 33 | amount_a=AssetAmount(pool.asset_1, 10_000_000), 34 | amount_b=AssetAmount(pool.asset_2, 5_000_000), 35 | ) 36 | 37 | print("\nQuote:") 38 | print(quote) 39 | 40 | txn_group = pool.prepare_add_liquidity_transactions_from_quote(quote=quote) 41 | 42 | if not client.asset_is_opted_in(asset_id=pool.pool_token_asset.id): 43 | # Opt-in to the pool token 44 | opt_in_txn_group = pool.prepare_pool_token_asset_optin_transactions() 45 | # You can merge the transaction groups 46 | txn_group = txn_group + opt_in_txn_group 47 | 48 | # Sign 49 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 50 | 51 | # Submit transactions to the network and wait for confirmation 52 | txn_info = client.submit(txn_group, wait=True) 53 | print("Transaction Info") 54 | pprint(txn_info) 55 | 56 | print( 57 | f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" 58 | ) 59 | 60 | pool.refresh() 61 | pool_position = pool.fetch_pool_position() 62 | share = pool_position["share"] * 100 63 | print(f"Pool Tokens: {pool_position[pool.pool_token_asset]}") 64 | print(f"Assets: {pool_position[ASSET_A]}, {pool_position[ASSET_B]}") 65 | print(f"Share of pool: {share:.3f}%") 66 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 2.1.1 4 | 5 | ### Changed 6 | 7 | * Added `py-algorand-sdk==2.0.0` support. [#58](https://github.com/tinymanorg/tinyman-py-sdk/pull/58) 8 | * Added `refresh` parameter to Tinyman V1 fetch functions (`fetch_mint_quote`, `fetch_burn_quote`, `fetch_fixed_input_swap_quote`, `fetch_fixed_output_swap_quote`). [#59](https://github.com/tinymanorg/tinyman-py-sdk/pull/59) 9 | 10 | 11 | ## 2.1.0 12 | 13 | ### Added 14 | 15 | * Added Tinyman V2 (Mainnet) support. 16 | * Added `client_name` attribute to `TinymanClient` classes. [#51](https://github.com/tinymanorg/tinyman-py-sdk/pull/51) 17 | * Added note to application call transactions. The note (`tinyman/:j{"origin":""}`) follows [Algorand Transaction Note Field Conventions ARC-2](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md). [#51](https://github.com/tinymanorg/tinyman-py-sdk/pull/51) 18 | * Added `version` property and `generate_app_call_note` method to `TinymanClient` classes. [#51](https://github.com/tinymanorg/tinyman-py-sdk/pull/51) 19 | * Added `get_version` and `generate_app_call_note` to `tinyman.utils`. [#51](https://github.com/tinymanorg/tinyman-py-sdk/pull/51) 20 | * Improved error handling [#49](https://github.com/tinymanorg/tinyman-py-sdk/pull/49/files). 21 | - Added `TealishMap`. 22 | - Added `AlgodError`, `LogicError`, `OverspendError` exception classes. 23 | - Added `amm_approval.map.json` for V2. 24 | 25 | ## 2.0.0 26 | 27 | ### Added 28 | 29 | * Added Tinyman V2 (Testnet) support (`tinyman.v2`). 30 | * Added Staking support (`tinyman.staking`). 31 | - It allows creating commitment transaction by `prepare_commit_transaction` and tracking commitments by `parse_commit_transaction`. 32 | * Added `calculate_price_impact` function to `tinyman.utils`. 33 | * Improved `TransactionGroup` class. 34 | - Added `+` operator support for composability, it allows creating a new transaction group (`txn_group_1 + txn_group_2`). 35 | - Added `id` property, it returns the transactions group id. 36 | - Added `TransactionGroup.sign_with_logicsig` function and deprecated `TransactionGroup.sign_with_logicisg` because of the typo. 37 | 38 | ### Changed 39 | 40 | * `get_program` (V1) is moved from `tinyman.utils` to `tinyman.v1.contracts`. 41 | * `get_state_from_account_info` (V1) is moved from `tinyman.utils` to `tinyman.v1.utils`. 42 | 43 | ### Removed 44 | 45 | * Deprecated `wait_for_confirmation` function is removed. `wait_for_confirmation` is added to [Algorand SDK](https://github.com/algorand/py-algorand-sdk). 46 | * Drop Python 3.7 support. 47 | 48 | -------------------------------------------------------------------------------- /tinyman/v1/swap.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from tinyman.compat import ApplicationNoOpTxn, PaymentTxn, AssetTransferTxn 4 | 5 | from tinyman.utils import TransactionGroup 6 | from .contracts import get_pool_logicsig 7 | 8 | 9 | def prepare_swap_transactions( 10 | validator_app_id, 11 | asset1_id, 12 | asset2_id, 13 | liquidity_asset_id, 14 | asset_in_id, 15 | asset_in_amount, 16 | asset_out_amount, 17 | swap_type, 18 | sender, 19 | suggested_params, 20 | app_call_note: Optional[str] = None, 21 | ): 22 | pool_logicsig = get_pool_logicsig(validator_app_id, asset1_id, asset2_id) 23 | pool_address = pool_logicsig.address() 24 | 25 | swap_types = { 26 | "fixed-input": "fi", 27 | "fixed-output": "fo", 28 | } 29 | 30 | asset_out_id = asset2_id if asset_in_id == asset1_id else asset1_id 31 | 32 | txns = [ 33 | PaymentTxn( 34 | sender=sender, 35 | sp=suggested_params, 36 | receiver=pool_address, 37 | amt=2000, 38 | note="fee", 39 | ), 40 | ApplicationNoOpTxn( 41 | sender=pool_address, 42 | sp=suggested_params, 43 | index=validator_app_id, 44 | app_args=["swap", swap_types[swap_type]], 45 | accounts=[sender], 46 | foreign_assets=[asset1_id, liquidity_asset_id] 47 | if asset2_id == 0 48 | else [asset1_id, asset2_id, liquidity_asset_id], 49 | note=app_call_note, 50 | ), 51 | AssetTransferTxn( 52 | sender=sender, 53 | sp=suggested_params, 54 | receiver=pool_address, 55 | amt=int(asset_in_amount), 56 | index=asset_in_id, 57 | ) 58 | if asset_in_id != 0 59 | else PaymentTxn( 60 | sender=sender, 61 | sp=suggested_params, 62 | receiver=pool_address, 63 | amt=int(asset_in_amount), 64 | ), 65 | AssetTransferTxn( 66 | sender=pool_address, 67 | sp=suggested_params, 68 | receiver=sender, 69 | amt=int(asset_out_amount), 70 | index=asset_out_id, 71 | ) 72 | if asset_out_id != 0 73 | else PaymentTxn( 74 | sender=pool_address, 75 | sp=suggested_params, 76 | receiver=sender, 77 | amt=int(asset_out_amount), 78 | ), 79 | ] 80 | 81 | txn_group = TransactionGroup(txns) 82 | txn_group.sign_with_logicsig(pool_logicsig) 83 | return txn_group 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # Tutorials 135 | account*.json 136 | assets*.json 137 | -------------------------------------------------------------------------------- /tinyman/governance/rewards/events.py: -------------------------------------------------------------------------------- 1 | from algosdk import abi 2 | 3 | from tinyman.governance.event import Event 4 | 5 | 6 | event_init = Event( 7 | name="init", 8 | args=[ 9 | abi.Argument(arg_type="uint64", name="first_period_timestamp"), 10 | abi.Argument(arg_type="uint64", name="reward_amount"), 11 | ] 12 | ) 13 | 14 | event_set_reward_amount = Event( 15 | name="set_reward_amount", 16 | args=[ 17 | abi.Argument(arg_type="uint64", name="timestamp"), 18 | abi.Argument(arg_type="uint64", name="reward_amount"), 19 | ] 20 | ) 21 | 22 | event_create_reward_period = Event( 23 | name="create_reward_period", 24 | args=[ 25 | abi.Argument(arg_type="uint64", name="index"), 26 | abi.Argument(arg_type="uint64", name="total_reward_amount"), 27 | abi.Argument(arg_type="uint128", name="total_cumulative_power_delta"), 28 | ] 29 | ) 30 | 31 | event_claim_rewards = Event( 32 | name="claim_rewards", 33 | args=[ 34 | abi.Argument(arg_type="address", name="user_address"), 35 | abi.Argument(arg_type="uint64", name="total_reward_amount"), 36 | abi.Argument(arg_type="uint64", name="period_index_start"), 37 | abi.Argument(arg_type="uint64", name="period_count"), 38 | abi.Argument(arg_type="uint64[]", name="reward_amounts"), 39 | ] 40 | ) 41 | 42 | event_set_manager = Event( 43 | name="set_manager", 44 | args=[ 45 | abi.Argument(arg_type="address", name="manager"), 46 | ] 47 | ) 48 | 49 | event_set_rewards_manager = Event( 50 | name="set_rewards_manager", 51 | args=[ 52 | abi.Argument(arg_type="address", name="rewards_manager"), 53 | ] 54 | ) 55 | 56 | event_reward_period = Event( 57 | name="reward_period", 58 | args=[ 59 | abi.Argument(arg_type="uint64", name="index"), 60 | abi.Argument(arg_type="uint64", name="total_reward_amount"), 61 | abi.Argument(arg_type="uint128", name="total_cumulative_power_delta"), 62 | ] 63 | ) 64 | 65 | event_reward_history = Event( 66 | name="reward_history", 67 | args=[ 68 | abi.Argument(arg_type="uint64", name="index"), 69 | abi.Argument(arg_type="uint64", name="timestamp"), 70 | abi.Argument(arg_type="uint64", name="reward_amount"), 71 | ] 72 | ) 73 | 74 | 75 | rewards_events = [ 76 | # method calls 77 | event_init, 78 | event_set_reward_amount, 79 | event_claim_rewards, 80 | event_create_reward_period, 81 | event_set_manager, 82 | event_set_rewards_manager, 83 | # boxes 84 | event_reward_period, 85 | event_reward_history, 86 | ] 87 | -------------------------------------------------------------------------------- /tinyman/v2/fees.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from tinyman.compat import ( 4 | ApplicationNoOpTxn, 5 | SuggestedParams, 6 | ) 7 | 8 | from tinyman.utils import TransactionGroup 9 | from tinyman.v2.constants import ( 10 | CLAIM_FEES_APP_ARGUMENT, 11 | CLAIM_EXTRA_APP_ARGUMENT, 12 | SET_FEE_APP_ARGUMENT, 13 | ) 14 | 15 | 16 | def prepare_claim_fees_transactions( 17 | validator_app_id: int, 18 | asset_1_id: int, 19 | asset_2_id: int, 20 | pool_address: str, 21 | fee_collector: str, 22 | sender: str, 23 | suggested_params: SuggestedParams, 24 | app_call_note: Optional[str] = None, 25 | ) -> TransactionGroup: 26 | txns = [ 27 | ApplicationNoOpTxn( 28 | sender=sender, 29 | sp=suggested_params, 30 | index=validator_app_id, 31 | app_args=[CLAIM_FEES_APP_ARGUMENT], 32 | foreign_assets=[asset_1_id, asset_2_id], 33 | accounts=[pool_address, fee_collector], 34 | note=app_call_note, 35 | ), 36 | ] 37 | 38 | min_fee = suggested_params.min_fee 39 | app_call_fee = min_fee * 3 40 | txns[-1].fee = app_call_fee 41 | 42 | txn_group = TransactionGroup(txns) 43 | return txn_group 44 | 45 | 46 | def prepare_claim_extra_transactions( 47 | validator_app_id: int, 48 | asset_id: int, 49 | address: str, 50 | fee_collector: str, 51 | sender: str, 52 | suggested_params: SuggestedParams, 53 | app_call_note: Optional[str] = None, 54 | ) -> TransactionGroup: 55 | txns = [ 56 | ApplicationNoOpTxn( 57 | sender=sender, 58 | sp=suggested_params, 59 | index=validator_app_id, 60 | app_args=[CLAIM_EXTRA_APP_ARGUMENT], 61 | foreign_assets=[asset_id], 62 | accounts=[address, fee_collector], 63 | note=app_call_note, 64 | ), 65 | ] 66 | 67 | min_fee = suggested_params.min_fee 68 | app_call_fee = min_fee * 2 69 | txns[-1].fee = app_call_fee 70 | 71 | txn_group = TransactionGroup(txns) 72 | return txn_group 73 | 74 | 75 | def prepare_set_fee_transactions( 76 | validator_app_id: int, 77 | pool_address: str, 78 | total_fee_share: int, 79 | protocol_fee_ratio: int, 80 | fee_manager: str, 81 | suggested_params: SuggestedParams, 82 | app_call_note: Optional[str] = None, 83 | ) -> TransactionGroup: 84 | txns = [ 85 | ApplicationNoOpTxn( 86 | sender=fee_manager, 87 | sp=suggested_params, 88 | index=validator_app_id, 89 | app_args=[SET_FEE_APP_ARGUMENT, total_fee_share, protocol_fee_ratio], 90 | accounts=[pool_address], 91 | note=app_call_note, 92 | ), 93 | ] 94 | txn_group = TransactionGroup(txns) 95 | return txn_group 96 | -------------------------------------------------------------------------------- /tinyman/governance/staking_voting/events.py: -------------------------------------------------------------------------------- 1 | from algosdk import abi 2 | 3 | from tinyman.governance.event import Event 4 | 5 | 6 | event_proposal = Event( 7 | name="proposal", 8 | args=[ 9 | abi.Argument(arg_type="byte[59]", name="proposal_id"), 10 | abi.Argument(arg_type="uint64", name="index"), 11 | abi.Argument(arg_type="uint64", name="creation_timestamp"), 12 | abi.Argument(arg_type="uint64", name="voting_start_timestamp"), 13 | abi.Argument(arg_type="uint64", name="voting_end_timestamp"), 14 | abi.Argument(arg_type="uint64", name="voting_power"), 15 | abi.Argument(arg_type="uint64", name="vote_count"), 16 | abi.Argument(arg_type="bool", name="is_cancelled"), 17 | ] 18 | ) 19 | 20 | event_create_proposal = Event( 21 | name="create_proposal", 22 | args=[ 23 | abi.Argument(arg_type="address", name="user_address"), 24 | abi.Argument(arg_type="byte[59]", name="proposal_id"), 25 | ] 26 | ) 27 | 28 | event_cancel_proposal = Event( 29 | name="cancel_proposal", 30 | args=[ 31 | abi.Argument(arg_type="address", name="user_address"), 32 | abi.Argument(arg_type="byte[59]", name="proposal_id"), 33 | ] 34 | ) 35 | 36 | event_vote = Event( 37 | name="vote", 38 | args=[ 39 | abi.Argument(arg_type="uint64", name="asset_id"), 40 | abi.Argument(arg_type="uint64", name="voting_power"), 41 | abi.Argument(arg_type="uint64", name="vote_percentage"), 42 | ] 43 | ) 44 | 45 | event_cast_vote = Event( 46 | name="cast_vote", 47 | args=[ 48 | abi.Argument(arg_type="address", name="user_address"), 49 | abi.Argument(arg_type="byte[59]", name="proposal_id"), 50 | abi.Argument(arg_type="uint64", name="voting_power"), 51 | ] 52 | ) 53 | 54 | event_set_manager = Event( 55 | name="set_manager", 56 | args=[ 57 | abi.Argument(arg_type="address", name="manager"), 58 | ] 59 | ) 60 | 61 | event_set_proposal_manager = Event( 62 | name="set_proposal_manager", 63 | args=[ 64 | abi.Argument(arg_type="address", name="proposal_manager"), 65 | ] 66 | ) 67 | 68 | event_set_voting_delay = Event( 69 | name="set_voting_delay", 70 | args=[ 71 | abi.Argument(arg_type="uint64", name="voting_delay"), 72 | ] 73 | ) 74 | 75 | event_set_voting_duration = Event( 76 | name="set_voting_duration", 77 | args=[ 78 | abi.Argument(arg_type="uint64", name="voting_duration"), 79 | ] 80 | ) 81 | 82 | staking_voting_events = [ 83 | # method calls 84 | event_create_proposal, 85 | event_cancel_proposal, 86 | event_cast_vote, 87 | event_set_manager, 88 | event_set_proposal_manager, 89 | event_set_voting_delay, 90 | event_set_voting_duration, 91 | # boxes 92 | event_vote, 93 | event_proposal, 94 | ] 95 | -------------------------------------------------------------------------------- /tinyman/governance/event.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | from Cryptodome.Hash import SHA512 5 | from algosdk import abi 6 | from algosdk.abi.base_type import ABI_LENGTH_SIZE 7 | 8 | 9 | @dataclass 10 | class Event: 11 | name: str 12 | args: [abi.Argument] 13 | 14 | @property 15 | def signature(self): 16 | arg_string = ",".join(str(arg.type) for arg in self.args) 17 | event_signature = "{}({})".format(self.name, arg_string) 18 | return event_signature 19 | 20 | @property 21 | def selector(self): 22 | sha_512_256_hash = SHA512.new(truncate="256") 23 | sha_512_256_hash.update(self.signature.encode("utf-8")) 24 | selector = sha_512_256_hash.digest()[:4] 25 | return selector 26 | 27 | def decode(self, log): 28 | selector, event_data = log[:4], log[4:] 29 | assert self.selector == selector 30 | 31 | data = { 32 | "event_name": self.name 33 | } 34 | start = 0 35 | for arg in self.args: 36 | if arg.type.is_dynamic(): 37 | if isinstance(arg.type, abi.StringType): 38 | size = int.from_bytes(event_data[start:start + ABI_LENGTH_SIZE], "big") 39 | elif isinstance(arg.type, abi.ArrayDynamicType): 40 | size = int.from_bytes(event_data[start:start + ABI_LENGTH_SIZE], "big") * arg.type.child_type.byte_len() 41 | else: 42 | raise NotImplementedError() 43 | 44 | end = start + ABI_LENGTH_SIZE + size 45 | else: 46 | end = start + arg.type.byte_len() 47 | 48 | value = event_data[start:end] 49 | if isinstance(arg.type, abi.ArrayStaticType) and isinstance(arg.type.child_type, abi.ByteType): 50 | data[arg.name] = bytes(arg.type.decode(value)) 51 | else: 52 | data[arg.name] = arg.type.decode(value) 53 | start = end 54 | return data 55 | 56 | def encode(self, parameters: Optional[list] = None): 57 | log = self.selector 58 | if parameters is None: 59 | parameters = [] 60 | 61 | assert len(parameters) == len(self.args) 62 | for parameter, arg in zip(parameters, self.args): 63 | log += arg.type.encode(parameter) 64 | return log 65 | 66 | 67 | def get_event_by_log(log: bytes, events: list[Event]): 68 | event_selector = log[:4] 69 | events_filtered = [event for event in events if event.selector == event_selector] 70 | assert len(events_filtered) == 1 71 | event = events_filtered[0] 72 | return event 73 | 74 | 75 | def decode_logs(logs: list[bytes], events: list[Event]): 76 | decoded_logs = [] 77 | 78 | for log in logs: 79 | event = get_event_by_log(log, events) 80 | decoded_logs.append(event.decode(log)) 81 | 82 | return decoded_logs 83 | -------------------------------------------------------------------------------- /tinyman/governance/proposal_voting/constants.py: -------------------------------------------------------------------------------- 1 | from tinyman.constants import MINIMUM_BALANCE_REQUIREMENT_PER_BOX, MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE 2 | 3 | # Global States 4 | PROPOSAL_INDEX_COUNTER_KEY = b'proposal_index_counter' 5 | VOTING_DELAY_KEY = b'voting_delay' 6 | VOTING_DURATION_KEY = b'voting_duration' 7 | PROPOSAL_THRESHOLD_KEY = b'proposal_threshold' 8 | PROPOSAL_THRESHOLD_NUMERATOR_KEY = b'proposal_threshold_numerator' 9 | QUORUM_THRESHOLD_KEY = b'quorum_threshold' 10 | MANAGER_KEY = b'manager' 11 | PROPOSAL_MANAGER_KEY = b'proposal_manager' 12 | APPROVAL_REQUIREMENT_KEY = b'approval_requirement' 13 | 14 | # Box 15 | PROPOSAL_BOX_PREFIX = b'p' 16 | ATTENDANCE_SHEET_BOX_PREFIX = b'a' 17 | 18 | PROPOSAL_BOX_SIZE = 116 + 34 + 32 19 | ACCOUNT_ATTENDANCE_SHEET_BOX_SIZE = 24 20 | 21 | PROPOSAL_VOTING_APP_MINIMUM_BALANCE_REQUIREMENT = 100_000 22 | 23 | PROPOSAL_BOX_COST = MINIMUM_BALANCE_REQUIREMENT_PER_BOX + MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE * (60 + PROPOSAL_BOX_SIZE) 24 | ATTENDANCE_SHEET_BOX_COST = MINIMUM_BALANCE_REQUIREMENT_PER_BOX + MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE * (41 + ACCOUNT_ATTENDANCE_SHEET_BOX_SIZE) 25 | 26 | CREATE_PROPOSAL_APP_ARGUMENT = b"create_proposal" 27 | CAST_VOTE_APP_ARGUMENT = b"cast_vote" 28 | GET_PROPOSAL_APP_ARGUMENT = b"get_proposal" 29 | GET_PROPOSAL_STATE_APP_ARGUMENT = b"get_proposal_state" 30 | HAS_VOTED_APP_ARGUMENT = b"has_voted" 31 | APPROVE_PROPOSAL_APP_ARGUMENT = b"approve_proposal" 32 | CANCEL_PROPOSAL_APP_ARGUMENT = b"cancel_proposal" 33 | EXECUTE_PROPOSAL_APP_ARGUMENT = b"execute_proposal" 34 | DISABLE_APPROVAL_REQUIREMENT_APP_ARGUMENT = b"disable_approval_requirement" 35 | SET_PROPOSAL_THRESHOLD_APP_ARGUMENT = b"set_proposal_threshold" 36 | SET_PROPOSAL_THRESHOLD_NUMERATOR_APP_ARGUMENT = b"set_proposal_threshold_numerator" 37 | SET_QUORUM_THRESHOLD_APP_ARGUMENT = b"set_quorum_threshold" 38 | 39 | PROPOSAL_STATE_WAITING_FOR_APPROVAL = 0 40 | PROPOSAL_STATE_CANCELLED = 1 41 | PROPOSAL_STATE_PENDING = 2 42 | PROPOSAL_STATE_ACTIVE = 3 43 | PROPOSAL_STATE_DEFEATED = 4 44 | PROPOSAL_STATE_SUCCEEDED = 5 45 | PROPOSAL_STATE_EXECUTED = 6 46 | 47 | # Executors app arguments 48 | VALIDATE_TRANSACTION_APP_ARGUMENT = b"validate_transaction" 49 | VALIDATE_GROUP_APP_ARGUMENT = b"validate_group" 50 | SET_FEE_SETTER_APP_ARGUMENT = b"set_fee_setter" 51 | SET_FEE_MANAGER_APP_ARGUMENT = b"set_fee_manager" 52 | SET_FEE_COLLECTOR_APP_ARGUMENT = b"set_fee_collector" 53 | SET_FEE_FOR_POOL_APP_ARGUMENT = b"set_fee_for_pool" 54 | SEND_APP_ARGUMENT = b"send" 55 | ASSET_OPTIN_APP_ARGUMENT = b"asset_optin" 56 | 57 | # Executors hash prefixes 58 | VALIDATE_TRANSACTION_HASH_PREFIX = b"vt" 59 | VALIDATE_GROUP_HASH_PREFIX = b"vg" 60 | SET_FEE_SETTER_HASH_PREFIX = b"fs" 61 | SET_FEE_MANAGER_HASH_PREFIX = b"fm" 62 | SET_FEE_COLLECTOR_HASH_PREFIX = b"fc" 63 | SET_FEE_FOR_POOL_HASH_PREFIX = b"sf" 64 | SEND_HASH_PREFIX = b"sn" 65 | 66 | EXECUTION_HASH_SIZE = 2 + 32 67 | CREATE_PROPOSAL_DEFAULT_EXECUTION_HASH_ARGUMENT = b"\x00" * EXECUTION_HASH_SIZE 68 | -------------------------------------------------------------------------------- /tinyman/v2/client.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from algosdk.v2client.algod import AlgodClient 4 | 5 | from tinyman.client import BaseTinymanClient 6 | from tinyman.errors import LogicError 7 | from tinyman.staking.constants import ( 8 | TESTNET_STAKING_APP_ID, 9 | MAINNET_STAKING_APP_ID, 10 | ) 11 | from tinyman.swap_router.constants import ( 12 | TESTNET_SWAP_ROUTER_APP_ID_V1, 13 | MAINNET_SWAP_ROUTER_APP_ID_V1, 14 | ) 15 | from tinyman.utils import find_app_id_from_txn_id, parse_error 16 | from tinyman.v2.constants import ( 17 | TESTNET_VALIDATOR_APP_ID, 18 | MAINNET_VALIDATOR_APP_ID, 19 | ) 20 | from tinyman.v2.utils import lookup_error, get_tealishmap 21 | 22 | 23 | class TinymanV2Client(BaseTinymanClient): 24 | def __init__(self, *args, **kwargs): 25 | self.router_app_id = kwargs.pop("router_app_id", None) 26 | super().__init__(*args, **kwargs) 27 | 28 | def fetch_pool(self, asset_a, asset_b, fetch=True): 29 | from .pools import Pool 30 | 31 | return Pool(self, asset_a, asset_b, fetch=fetch) 32 | 33 | def handle_error(self, exception, txn_group): 34 | error = parse_error(exception) 35 | if isinstance(error, LogicError): 36 | app_id = find_app_id_from_txn_id(txn_group, error.txn_id) 37 | tealishmap = get_tealishmap(app_id) 38 | if tealishmap: 39 | error.app_id = app_id 40 | error.message = lookup_error(error.pc, error.message, tealishmap) 41 | 42 | raise error from None 43 | 44 | 45 | class TinymanV2TestnetClient(TinymanV2Client): 46 | def __init__( 47 | self, 48 | algod_client: AlgodClient, 49 | user_address: Optional[str] = None, 50 | client_name: Optional[str] = None, 51 | api_base_url: Optional[str] = None, 52 | ): 53 | super().__init__( 54 | algod_client, 55 | validator_app_id=TESTNET_VALIDATOR_APP_ID, 56 | router_app_id=TESTNET_SWAP_ROUTER_APP_ID_V1, 57 | api_base_url=api_base_url or "https://testnet.analytics.tinyman.org/api/", 58 | user_address=user_address, 59 | staking_app_id=TESTNET_STAKING_APP_ID, 60 | client_name=client_name, 61 | ) 62 | 63 | 64 | class TinymanV2MainnetClient(TinymanV2Client): 65 | def __init__( 66 | self, 67 | algod_client: AlgodClient, 68 | user_address: Optional[str] = None, 69 | client_name: Optional[str] = None, 70 | api_base_url: Optional[str] = None, 71 | ): 72 | super().__init__( 73 | algod_client, 74 | validator_app_id=MAINNET_VALIDATOR_APP_ID, 75 | router_app_id=MAINNET_SWAP_ROUTER_APP_ID_V1, 76 | api_base_url=api_base_url or "https://mainnet.analytics.tinyman.org/api/", 77 | user_address=user_address, 78 | staking_app_id=MAINNET_STAKING_APP_ID, 79 | client_name=client_name, 80 | ) 81 | -------------------------------------------------------------------------------- /tinyman/governance/vault/constants.py: -------------------------------------------------------------------------------- 1 | from tinyman.constants import MINIMUM_BALANCE_REQUIREMENT_PER_BOX, MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE 2 | from tinyman.governance.constants import WEEK 3 | 4 | MIN_LOCK_TIME = 4 * WEEK # 4 WEEK 5 | MAX_LOCK_TIME = 4 * 52 * WEEK # 364 * 4 Days 6 | MIN_LOCK_AMOUNT = 10_000_000 # 10 TINY 7 | MIN_LOCK_AMOUNT_INCREMENT = 10_000_000 # 10 TINY 8 | TWO_TO_THE_64 = 2 ** 64 # Scaling factor 9 | # TWO_TO_THE_64 = "\x01\x00\x00\x00\x00\x00\x00\x00\x00" 10 | 11 | # Global states 12 | TOTAL_LOCKED_AMOUNT_KEY = b'total_locked_amount' 13 | TOTAL_POWER_COUNT_KEY = b'total_power_count' 14 | LAST_TOTAL_POWER_TIMESTAMP_KEY = b'last_total_power_timestamp' 15 | 16 | # Boxes 17 | TOTAL_POWERS = b'tp' 18 | SLOPE_CHANGES = b'sc' 19 | 20 | ACCOUNT_STATE_BOX_SIZE = 32 21 | SLOPE_CHANGE_BOX_SIZE = 16 22 | 23 | ACCOUNT_POWER_SIZE = 48 24 | ACCOUNT_POWER_BOX_SIZE = 1008 25 | ACCOUNT_POWER_BOX_ARRAY_LEN = 21 26 | 27 | TOTAL_POWER_SIZE = 48 28 | TOTAL_POWER_BOX_SIZE = 1008 29 | TOTAL_POWER_BOX_ARRAY_LEN = 21 30 | 31 | # 100_000 Default 32 | # 100_000 Opt-in 33 | # Box 34 | # https://developer.algorand.org/docs/get-details/dapps/smart-contracts/apps/?from_query=box#minimum-balance-requirement-for-boxes 35 | # 2500 + 400 * (len(n)+s) 36 | # 2_500 Box 37 | # 407_200 = 400 * (10 + 1008) 38 | VAULT_APP_MINIMUM_BALANCE_REQUIREMENT = 609_700 39 | 40 | 41 | ACCOUNT_STATE_BOX_COST = MINIMUM_BALANCE_REQUIREMENT_PER_BOX + MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE * (32 + ACCOUNT_STATE_BOX_SIZE) 42 | SLOPE_CHANGE_BOX_COST = MINIMUM_BALANCE_REQUIREMENT_PER_BOX + MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE * (10 + SLOPE_CHANGE_BOX_SIZE) 43 | ACCOUNT_POWER_BOX_COST = MINIMUM_BALANCE_REQUIREMENT_PER_BOX + MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE * (40 + ACCOUNT_POWER_BOX_SIZE) 44 | TOTAL_POWER_BOX_COST = MINIMUM_BALANCE_REQUIREMENT_PER_BOX + MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE * (10 + TOTAL_POWER_BOX_SIZE) 45 | 46 | INIT_APP_ARGUMENT = b"init" 47 | CREATE_LOCK_APP_ARGUMENT = b"create_lock" 48 | CREATE_CHECKPOINTS_APP_ARGUMENT = b"create_checkpoints" 49 | INCREASE_LOCK_AMOUNT_APP_ARGUMENT = b"increase_lock_amount" 50 | EXTEND_LOCK_END_TIME_APP_ARGUMENT = b"extend_lock_end_time" 51 | WITHDRAW_APP_ARGUMENT = b"withdraw" 52 | GET_TINY_POWER_OF_APP_ARGUMENT = b"get_tiny_power_of" 53 | GET_TINY_POWER_OF_AT_APP_ARGUMENT = b"get_tiny_power_of_at" 54 | GET_TOTAL_TINY_POWER_APP_ARGUMENT = b"get_total_tiny_power" 55 | GET_TOTAL_TINY_POWER_AT_APP_ARGUMENT = b"get_total_tiny_power_at" 56 | GET_TOTAL_CUMULATIVE_POWER_AT_APP_ARGUMENT = b"get_total_cumulative_power_at" 57 | GET_CUMULATIVE_POWER_OF_AT_APP_ARGUMENT = b"get_cumulative_power_of_at" 58 | GET_ACCOUNT_CUMULATIVE_POWER_DELTA_APP_PREFIX = b"get_account_cumulative_power_delta" 59 | GET_TOTAL_CUMULATIVE_POWER_DELTA_APP_PREFIX = b"get_total_cumulative_power_delta" 60 | DELETE_ACCOUNT_POWER_BOXES_APP_ARGUMENT = b"delete_account_power_boxes" 61 | DELETE_ACCOUNT_STATE_APP_ARGUMENT = b"delete_account_state" 62 | -------------------------------------------------------------------------------- /tinyman/governance/staking_voting/storage.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | from algosdk.encoding import decode_address 5 | 6 | from tinyman.governance.proposal_voting.storage import get_proposal_box_name 7 | from tinyman.governance.staking_voting.constants import PROPOSAL_BOX_PREFIX, STAKING_VOTE_BOX_PREFIX, STAKING_ATTENDANCE_BOX_PREFIX 8 | from tinyman.governance.utils import check_nth_bit_from_left, get_raw_box_value 9 | from tinyman.utils import int_to_bytes, bytes_to_int 10 | 11 | 12 | @dataclass 13 | class StakingVotingAppGlobalState: 14 | vault_app_id: int 15 | proposal_index_counter: int 16 | voting_delay: int 17 | voting_duration: int 18 | manager: str 19 | proposal_manager: str 20 | 21 | 22 | @dataclass 23 | class StakingDistributionProposal: 24 | index: int 25 | creation_timestamp: int 26 | voting_start_timestamp: int 27 | voting_end_timestamp: int 28 | voting_power: int 29 | vote_count: int 30 | is_cancelled: bool 31 | 32 | @property 33 | def snapshot_timestamp(self) -> int: 34 | return self.creation_timestamp 35 | 36 | 37 | @dataclass 38 | class StakingVotingAttendanceSheet: 39 | value: bytes 40 | 41 | @property 42 | def attendance_sheet(self) -> list[bool]: 43 | return [check_nth_bit_from_left(self.value, index) for index in range(0, (len(self.value) * 8))] 44 | 45 | def is_vote_casted_for_proposal(self, proposal_index) -> bool: 46 | return check_nth_bit_from_left(self.value, proposal_index) 47 | 48 | 49 | def get_staking_distribution_proposal_box_name(proposal_id: str) -> bytes: 50 | return PROPOSAL_BOX_PREFIX + proposal_id.encode() 51 | 52 | 53 | def get_staking_vote_box_name(proposal_index: int, asset_id: int) -> bytes: 54 | return STAKING_VOTE_BOX_PREFIX + int_to_bytes(proposal_index) + int_to_bytes(asset_id) 55 | 56 | 57 | def get_staking_attendance_sheet_box_name(address: str, box_index: int) -> bytes: 58 | attendance_sheet_box_name = STAKING_ATTENDANCE_BOX_PREFIX + decode_address(address) + int_to_bytes(box_index) 59 | return attendance_sheet_box_name 60 | 61 | 62 | def parse_box_staking_distribution_proposal(raw_box) -> StakingDistributionProposal: 63 | return StakingDistributionProposal( 64 | index=bytes_to_int(raw_box[:8]), 65 | creation_timestamp=bytes_to_int(raw_box[8:16]), 66 | voting_start_timestamp=bytes_to_int(raw_box[16:24]), 67 | voting_end_timestamp=bytes_to_int(raw_box[24:32]), 68 | voting_power=bytes_to_int(raw_box[32:40]), 69 | vote_count=bytes_to_int(raw_box[40:48]), 70 | is_cancelled=bool(bytes_to_int(raw_box[48:49])), 71 | ) 72 | 73 | 74 | def get_staking_distribution_proposal(algod, app_id: int, proposal_id: str) -> Optional[StakingDistributionProposal]: 75 | box_name = get_proposal_box_name(proposal_id) 76 | raw_box = get_raw_box_value(algod, app_id, box_name) 77 | if not raw_box: 78 | return None 79 | return parse_box_staking_distribution_proposal(raw_box) 80 | -------------------------------------------------------------------------------- /examples/v1/swapping1.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | 5 | # For a more verbose version of this example see swapping1_less_convenience.py 6 | 7 | from tinyman.v1.client import TinymanTestnetClient 8 | from algosdk.v2client.algod import AlgodClient 9 | 10 | 11 | # Hardcoding account keys is not a great practice. This is for demonstration purposes only. 12 | # See the README & Docs for alternative signing methods. 13 | account = { 14 | "address": "ALGORAND_ADDRESS_HERE", 15 | "private_key": "base64_private_key_here", # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary 16 | } 17 | 18 | algod = AlgodClient( 19 | "", "http://localhost:8080", headers={"User-Agent": "algosdk"} 20 | ) 21 | client = TinymanTestnetClient(algod_client=algod, user_address=account["address"]) 22 | # By default all subsequent operations are on behalf of user_address 23 | 24 | # Check if the account is opted into Tinyman and optin if necessary 25 | if not client.is_opted_in(): 26 | print("Account not opted into app, opting in now..") 27 | transaction_group = client.prepare_app_optin_transactions() 28 | transaction_group.sign_with_private_key(account["address"], account["private_key"]) 29 | result = client.submit(transaction_group, wait=True) 30 | 31 | 32 | # Fetch our two assets of interest 33 | TINYUSDC = client.fetch_asset(21582668) 34 | ALGO = client.fetch_asset(0) 35 | 36 | # Fetch the pool we will work with 37 | pool = client.fetch_pool(TINYUSDC, ALGO) 38 | 39 | 40 | # Get a quote for a swap of 1 ALGO to TINYUSDC with 1% slippage tolerance 41 | quote = pool.fetch_fixed_input_swap_quote(ALGO(1_000_000), slippage=0.01) 42 | print(quote) 43 | print(f"TINYUSDC per ALGO: {quote.price}") 44 | print(f"TINYUSDC per ALGO (worst case): {quote.price_with_slippage}") 45 | 46 | # We only want to sell if ALGO is > 180 TINYUSDC (It's testnet!) 47 | if quote.price_with_slippage > 180: 48 | print(f"Swapping {quote.amount_in} to {quote.amount_out_with_slippage}") 49 | # Prepare a transaction group 50 | transaction_group = pool.prepare_swap_transactions_from_quote(quote) 51 | # Sign the group with our key 52 | transaction_group.sign_with_private_key(account["address"], account["private_key"]) 53 | # Submit transactions to the network and wait for confirmation 54 | result = client.submit(transaction_group, wait=True) 55 | 56 | # Check if any excess remaining after the swap 57 | excess = pool.fetch_excess_amounts() 58 | if TINYUSDC in excess: 59 | amount = excess[TINYUSDC] 60 | print(f"Excess: {amount}") 61 | # We might just let the excess accumulate rather than redeeming if its < 1 TinyUSDC 62 | if amount > 1_000_000: 63 | transaction_group = pool.prepare_redeem_transactions(amount) 64 | transaction_group.sign_with_private_key( 65 | account["address"], account["private_key"] 66 | ) 67 | result = client.submit(transaction_group, wait=True) 68 | -------------------------------------------------------------------------------- /tinyman/ordering/order_structs.json: -------------------------------------------------------------------------------- 1 | { 2 | "structs": { 3 | "TriggerOrder": { 4 | "size": 80, 5 | "fields": { 6 | "asset_id": { 7 | "type": "int", 8 | "size": 8, 9 | "offset": 0 10 | }, 11 | "amount": { 12 | "type": "int", 13 | "size": 8, 14 | "offset": 8 15 | }, 16 | "target_asset_id": { 17 | "type": "int", 18 | "size": 8, 19 | "offset": 16 20 | }, 21 | "target_amount": { 22 | "type": "int", 23 | "size": 8, 24 | "offset": 24 25 | }, 26 | "filled_amount": { 27 | "type": "int", 28 | "size": 8, 29 | "offset": 32 30 | }, 31 | "collected_target_amount": { 32 | "type": "int", 33 | "size": 8, 34 | "offset": 40 35 | }, 36 | "is_partial_allowed": { 37 | "type": "int", 38 | "size": 8, 39 | "offset": 48 40 | }, 41 | "fee_rate": { 42 | "type": "int", 43 | "size": 8, 44 | "offset": 56 45 | }, 46 | "creation_timestamp": { 47 | "type": "int", 48 | "size": 8, 49 | "offset": 64 50 | }, 51 | "expiration_timestamp": { 52 | "type": "int", 53 | "size": 8, 54 | "offset": 72 55 | } 56 | } 57 | }, 58 | "RecurringOrder": { 59 | "size": 88, 60 | "fields": { 61 | "asset_id": { 62 | "type": "int", 63 | "size": 8, 64 | "offset": 0 65 | }, 66 | "amount": { 67 | "type": "int", 68 | "size": 8, 69 | "offset": 8 70 | }, 71 | "target_asset_id": { 72 | "type": "int", 73 | "size": 8, 74 | "offset": 16 75 | }, 76 | "collected_target_amount": { 77 | "type": "int", 78 | "size": 8, 79 | "offset": 24 80 | }, 81 | "min_target_amount": { 82 | "type": "int", 83 | "size": 8, 84 | "offset": 32 85 | }, 86 | "max_target_amount": { 87 | "type": "int", 88 | "size": 8, 89 | "offset": 40 90 | }, 91 | "remaining_recurrences": { 92 | "type": "int", 93 | "size": 8, 94 | "offset": 48 95 | }, 96 | "interval": { 97 | "type": "int", 98 | "size": 8, 99 | "offset": 56 100 | }, 101 | "fee_rate": { 102 | "type": "int", 103 | "size": 8, 104 | "offset": 64 105 | }, 106 | "last_fill_timestamp": { 107 | "type": "int", 108 | "size": 8, 109 | "offset": 72 110 | }, 111 | "creation_timestamp": { 112 | "type": "int", 113 | "size": 8, 114 | "offset": 80 115 | } 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tinyman/ordering/event.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | from Cryptodome.Hash import SHA512 5 | from algosdk import abi 6 | from algosdk.abi.base_type import ABI_LENGTH_SIZE 7 | 8 | 9 | @dataclass 10 | class Event: 11 | name: str 12 | args: [abi.Argument] 13 | 14 | @property 15 | def signature(self): 16 | arg_string = ",".join(str(arg.type) for arg in self.args) 17 | event_signature = "{}({})".format(self.name, arg_string) 18 | return event_signature 19 | 20 | @property 21 | def selector(self): 22 | sha_512_256_hash = SHA512.new(truncate="256") 23 | sha_512_256_hash.update(self.signature.encode("utf-8")) 24 | selector = sha_512_256_hash.digest()[:4] 25 | return selector 26 | 27 | def decode(self, log): 28 | selector, event_data = log[:4], log[4:] 29 | assert self.selector == selector 30 | 31 | data = { 32 | "event_name": self.name 33 | } 34 | start = 0 35 | for arg in self.args: 36 | if arg.type.is_dynamic(): 37 | if isinstance(arg.type, abi.StringType): 38 | size = int.from_bytes(event_data[start:start + ABI_LENGTH_SIZE], "big") 39 | elif isinstance(arg.type, abi.ArrayDynamicType): 40 | size = int.from_bytes(event_data[start:start + ABI_LENGTH_SIZE], "big") * arg.type.child_type.byte_len() 41 | elif isinstance(arg.type, abi.TupleType): 42 | pass 43 | else: 44 | raise NotImplementedError() 45 | 46 | end = start + ABI_LENGTH_SIZE + size 47 | else: 48 | end = start + arg.type.byte_len() 49 | 50 | value = event_data[start:end] 51 | if isinstance(arg.type, abi.ArrayStaticType) and isinstance(arg.type.child_type, abi.ByteType): 52 | data[arg.name] = bytes(arg.type.decode(value)) 53 | else: 54 | data[arg.name] = arg.type.decode(value) 55 | start = end 56 | return data 57 | 58 | def encode(self, parameters: Optional[list] = None): 59 | log = self.selector 60 | if parameters is None: 61 | parameters = [] 62 | 63 | assert len(parameters) == len(self.args) 64 | for parameter, arg in zip(parameters, self.args): 65 | log += arg.type.encode(parameter) 66 | return log 67 | 68 | 69 | def get_event_by_log(log: bytes, events: list[Event]): 70 | event_selector = log[:4] 71 | events_filtered = [event for event in events if event.selector == event_selector] 72 | if not events_filtered: 73 | return None 74 | assert len(events_filtered) == 1 75 | event = events_filtered[0] 76 | return event 77 | 78 | 79 | def decode_logs(logs: list[bytes], events: list[Event]): 80 | decoded_logs = [] 81 | for log in logs: 82 | event = get_event_by_log(log, events) 83 | if event: 84 | decoded_logs.append(event.decode(log)) 85 | return decoded_logs 86 | -------------------------------------------------------------------------------- /examples/v2/tutorial/12_flash_loan_2_multiple_assets.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | from pprint import pprint 5 | from urllib.parse import quote_plus 6 | 7 | from tinyman.assets import AssetAmount 8 | 9 | from examples.v2.tutorial.common import get_account, get_assets 10 | from examples.v2.utils import get_algod 11 | from tinyman.v2.client import TinymanV2TestnetClient 12 | from tinyman.compat import AssetTransferTxn, PaymentTxn 13 | 14 | account = get_account() 15 | algod = get_algod() 16 | client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) 17 | 18 | ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] 19 | ASSET_A = client.fetch_asset(ASSET_A_ID) 20 | ASSET_B = client.fetch_asset(ASSET_B_ID) 21 | pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) 22 | 23 | position = pool.fetch_pool_position() 24 | 25 | quote = pool.fetch_flash_loan_quote( 26 | loan_amount_a=AssetAmount(pool.asset_1, 3_000_000), 27 | loan_amount_b=AssetAmount(pool.asset_2, 2_000_000), 28 | ) 29 | 30 | print("\nQuote:") 31 | print(quote) 32 | 33 | account_info = algod.account_info(account["address"]) 34 | 35 | for asset in account_info["assets"]: 36 | if asset["asset-id"] == pool.asset_1.id: 37 | asset_1_balance = asset["amount"] 38 | 39 | if pool.asset_2.id == 0: 40 | # Algo 41 | asset_2_balance = ( 42 | account_info["amount"] - account_info["min-balance"] - 4000 43 | ) # 4 tnx fee 44 | elif asset["asset-id"] == pool.asset_2.id: 45 | asset_2_balance = asset["amount"] 46 | 47 | # Transfer amounts are equal to sum of initial account balance and loan amount 48 | # These transactions demonstrate that you can use the total amount 49 | transactions = [ 50 | AssetTransferTxn( 51 | sender=account["address"], 52 | sp=algod.suggested_params(), 53 | receiver=account["address"], 54 | amt=asset_1_balance + quote.amounts_out[pool.asset_1].amount, 55 | index=pool.asset_1.id, 56 | ), 57 | AssetTransferTxn( 58 | sender=account["address"], 59 | sp=algod.suggested_params(), 60 | receiver=account["address"], 61 | amt=asset_2_balance + quote.amounts_out[pool.asset_2].amount, 62 | index=pool.asset_2.id, 63 | ) 64 | if pool.asset_2.id 65 | else PaymentTxn( 66 | sender=account["address"], 67 | sp=algod.suggested_params(), 68 | receiver=account["address"], 69 | amt=asset_2_balance + quote.amounts_out[pool.asset_2].amount, 70 | ), 71 | ] 72 | 73 | txn_group = pool.prepare_flash_loan_transactions_from_quote( 74 | quote=quote, transactions=transactions 75 | ) 76 | 77 | # Sign 78 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 79 | 80 | # Submit transactions to the network and wait for confirmation 81 | txn_info = client.submit(txn_group, wait=True) 82 | print("Transaction Info") 83 | pprint(txn_info) 84 | 85 | print( 86 | f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" 87 | ) 88 | -------------------------------------------------------------------------------- /tinyman/v2/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import importlib.resources 3 | import json 4 | from base64 import b64decode 5 | 6 | import tinyman.v2 7 | from tinyman.swap_router.constants import ( 8 | TESTNET_SWAP_ROUTER_APP_ID_V1, 9 | MAINNET_SWAP_ROUTER_APP_ID_V1, 10 | ) 11 | from tinyman.tealishmap import TealishMap 12 | from tinyman.utils import bytes_to_int 13 | from tinyman.v2.constants import TESTNET_VALIDATOR_APP_ID, MAINNET_VALIDATOR_APP_ID 14 | 15 | if sys.version_info >= (3, 9): 16 | amm_tealishmap = TealishMap( 17 | json.loads( 18 | importlib.resources.files(tinyman.v2) 19 | .joinpath("amm_approval.map.json") 20 | .read_text() 21 | ) 22 | ) 23 | swap_router_tealishmap = TealishMap( 24 | json.loads( 25 | importlib.resources.files(tinyman.v2) 26 | .joinpath("swap_router_approval.map.json") 27 | .read_text() 28 | ) 29 | ) 30 | 31 | 32 | else: 33 | amm_tealishmap = TealishMap( 34 | json.loads(importlib.resources.read_text(tinyman.v2, "amm_approval.map.json")) 35 | ) 36 | swap_router_tealishmap = TealishMap( 37 | json.loads( 38 | importlib.resources.read_text(tinyman.v2, "swap_router_approval.map.json") 39 | ) 40 | ) 41 | 42 | 43 | def decode_logs(logs: "list") -> dict: 44 | decoded_logs = dict() 45 | for log in logs: 46 | if isinstance(log, str): 47 | log = b64decode(log.encode()) 48 | if b"%i" in log: 49 | i = log.index(b"%i") 50 | s = log[0:i].decode() 51 | value = int.from_bytes(log[i + 2 :], "big") 52 | decoded_logs[s] = value 53 | else: 54 | raise NotImplementedError() 55 | return decoded_logs 56 | 57 | 58 | def get_state_from_account_info(account_info, app_id): 59 | try: 60 | app = [a for a in account_info["apps-local-state"] if a["id"] == app_id][0] 61 | except IndexError: 62 | return {} 63 | try: 64 | app_state = {} 65 | for x in app["key-value"]: 66 | key = b64decode(x["key"]).decode() 67 | if x["value"]["type"] == 1: 68 | value = bytes_to_int(b64decode(x["value"].get("bytes", ""))) 69 | else: 70 | value = x["value"].get("uint", 0) 71 | app_state[key] = value 72 | except KeyError: 73 | return {} 74 | return app_state 75 | 76 | 77 | def get_tealishmap(app_id): 78 | maps = { 79 | TESTNET_VALIDATOR_APP_ID: amm_tealishmap, 80 | MAINNET_VALIDATOR_APP_ID: amm_tealishmap, 81 | TESTNET_SWAP_ROUTER_APP_ID_V1: swap_router_tealishmap, 82 | MAINNET_SWAP_ROUTER_APP_ID_V1: swap_router_tealishmap, 83 | } 84 | return maps.get(app_id) 85 | 86 | 87 | def lookup_error(pc, error_message, tealishmap): 88 | tealish_line_no = tealishmap.get_tealish_line_for_pc(int(pc)) 89 | if "assert failed" in error_message or "err opcode executed" in error_message: 90 | custom_error_message = tealishmap.get_error_for_pc(int(pc)) 91 | if custom_error_message: 92 | error_message = custom_error_message 93 | 94 | error_message = f"{error_message} @ line {tealish_line_no}" 95 | return error_message 96 | -------------------------------------------------------------------------------- /examples/folks_lending/create_new_pool.py: -------------------------------------------------------------------------------- 1 | from algosdk import transaction 2 | from algosdk.v2client.algod import AlgodClient 3 | 4 | from tinyman.folks_lending.constants import ( 5 | TESTNET_FOLKS_POOL_MANAGER_APP_ID, 6 | TESTNET_FOLKS_WRAPPER_LENDING_POOL_APP_ID) 7 | from tinyman.folks_lending.transactions import \ 8 | prepare_asset_optin_transaction_group 9 | from tinyman.folks_lending.utils import get_lending_pools 10 | from tinyman.utils import TransactionGroup 11 | from tinyman.v2.client import TinymanV2TestnetClient 12 | 13 | algod = AlgodClient("", "https://testnet-api.algonode.network") 14 | account_sk, account_address = ('YOUR PRIVATE KEY HERE', 'YOUR ADDRESS HERE') 15 | client = TinymanV2TestnetClient(algod_client=algod, user_address=account_address) 16 | 17 | asset_1_id = 67396528 # goBTC 18 | asset_2_id = 0 # Algo 19 | 20 | # Get f_asset ids 21 | 22 | folks_pools = get_lending_pools(algod, TESTNET_FOLKS_POOL_MANAGER_APP_ID) 23 | temp = dict() 24 | for folks_pool in folks_pools: 25 | temp[folks_pool['asset_id']] = folks_pool 26 | folks_pools = temp 27 | 28 | f_asset_1_id = folks_pools[asset_1_id]['f_asset_id'] 29 | f_asset_2_id = folks_pools[asset_2_id]['f_asset_id'] 30 | 31 | pool = client.fetch_pool(f_asset_1_id, f_asset_2_id) 32 | 33 | # Opt-in to assets 34 | 35 | txns = [ 36 | transaction.AssetOptInTxn( 37 | sender=account_address, 38 | sp=algod.suggested_params(), 39 | index=asset_1_id 40 | ), 41 | transaction.AssetOptInTxn( 42 | sender=account_address, 43 | sp=algod.suggested_params(), 44 | index=f_asset_1_id 45 | ), 46 | transaction.AssetOptInTxn( 47 | sender=account_address, 48 | sp=algod.suggested_params(), 49 | index=f_asset_2_id 50 | ) 51 | ] 52 | 53 | if asset_2_id != 0: 54 | txns.append( 55 | transaction.AssetOptInTxn( 56 | sender=account_address, 57 | sp=algod.suggested_params(), 58 | index=asset_2_id 59 | ) 60 | ) 61 | txn_group = TransactionGroup(txns) 62 | txn_group.sign_with_private_key(account_address, account_sk) 63 | txn_group.submit(algod, True) 64 | 65 | # Bootstrap pool. 66 | 67 | txn_group = pool.prepare_bootstrap_transactions( 68 | user_address=account_address, 69 | suggested_params=algod.suggested_params(), 70 | ) 71 | txn_group.sign_with_private_key(account_address, account_sk) 72 | txn_group.submit(algod, True) 73 | 74 | # Opt-in to pool token. 75 | 76 | pool = client.fetch_pool(f_asset_1_id, f_asset_2_id, fetch=True) 77 | 78 | txn_group = TransactionGroup([ 79 | transaction.AssetOptInTxn( 80 | sender=account_address, 81 | sp=algod.suggested_params(), 82 | index=pool.pool_token_asset.id 83 | ) 84 | ]) 85 | txn_group.sign_with_private_key(account_address, account_sk) 86 | txn_group.submit(algod, True) 87 | 88 | # Send an asset_optin appcall. 89 | 90 | txn_group = prepare_asset_optin_transaction_group( 91 | sender=account_address, 92 | suggested_params=algod.suggested_params(), 93 | wrapper_app_id=TESTNET_FOLKS_WRAPPER_LENDING_POOL_APP_ID, 94 | assets_to_optin=[asset_1_id, asset_2_id, f_asset_1_id, f_asset_2_id, pool.pool_token_asset.id] 95 | ) 96 | txn_group.sign_with_private_key(account_address, account_sk) 97 | txn_group.submit(algod, True) 98 | -------------------------------------------------------------------------------- /tinyman/assets.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from decimal import Decimal 3 | from typing import Optional 4 | 5 | 6 | @dataclass 7 | class Asset: 8 | id: int 9 | name: Optional[str] = None 10 | unit_name: Optional[str] = None 11 | decimals: int = None 12 | 13 | def __call__(self, amount: int) -> "AssetAmount": 14 | return AssetAmount(self, amount) 15 | 16 | def __hash__(self) -> int: 17 | return self.id 18 | 19 | def __repr__(self) -> str: 20 | return f"Asset({self.unit_name} - {self.id})" 21 | 22 | def __eq__(self, other) -> bool: 23 | return self.id == other.id 24 | 25 | def fetch(self, algod): 26 | if self.id > 0: 27 | params = algod.asset_info(self.id)["params"] 28 | else: 29 | params = { 30 | "name": "Algo", 31 | "unit-name": "ALGO", 32 | "decimals": 6, 33 | } 34 | self.name = params.get("name") 35 | self.unit_name = params.get("unit-name") 36 | self.decimals = params["decimals"] 37 | return self 38 | 39 | 40 | @dataclass 41 | class AssetAmount: 42 | asset: Asset 43 | amount: int 44 | 45 | def __mul__(self, other: float): 46 | if isinstance(other, (float, int)): 47 | return AssetAmount(self.asset, int(self.amount * other)) 48 | raise TypeError("Unsupported types for *") 49 | 50 | def __add__(self, other: "AssetAmount"): 51 | if isinstance(other, AssetAmount) and other.asset == self.asset: 52 | return AssetAmount(self.asset, int(self.amount + other.amount)) 53 | raise TypeError("Unsupported types for +") 54 | 55 | def __sub__(self, other: "AssetAmount"): 56 | if isinstance(other, AssetAmount) and other.asset == self.asset: 57 | return AssetAmount(self.asset, int(self.amount - other.amount)) 58 | raise TypeError("Unsupported types for -") 59 | 60 | def __gt__(self, other: "AssetAmount"): 61 | if isinstance(other, AssetAmount) and other.asset == self.asset: 62 | return self.amount > other.amount 63 | if isinstance(other, (float, int)): 64 | return self.amount > other 65 | raise TypeError("Unsupported types for >") 66 | 67 | def __lt__(self, other: "AssetAmount"): 68 | if isinstance(other, AssetAmount) and other.asset == self.asset: 69 | return self.amount < other.amount 70 | if isinstance(other, (float, int)): 71 | return self.amount < other 72 | raise TypeError("Unsupported types for <") 73 | 74 | def __eq__(self, other: "AssetAmount"): 75 | if isinstance(other, AssetAmount) and other.asset == self.asset: 76 | return self.amount == other.amount 77 | if isinstance(other, (float, int)): 78 | return self.amount == other 79 | raise TypeError("Unsupported types for ==") 80 | 81 | def __repr__(self) -> str: 82 | if self.asset.decimals is not None: 83 | amount = ( 84 | Decimal(self.amount) / Decimal(10**self.asset.decimals) 85 | ).quantize(1 / Decimal(10**self.asset.decimals)) 86 | return f"{self.asset.unit_name}('{amount}' Base Unit)" 87 | else: 88 | return f"{self.asset.unit_name}('{self.amount}' Micro Unit)" 89 | -------------------------------------------------------------------------------- /tinyman/v2/flash_loan.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from tinyman.compat import ( 4 | Transaction, 5 | ApplicationNoOpTxn, 6 | PaymentTxn, 7 | AssetTransferTxn, 8 | SuggestedParams, 9 | ) 10 | 11 | from tinyman.utils import TransactionGroup 12 | from .constants import ( 13 | FLASH_LOAN_APP_ARGUMENT, 14 | VERIFY_FLASH_LOAN_APP_ARGUMENT, 15 | ) 16 | from .contracts import get_pool_logicsig 17 | 18 | 19 | def prepare_flash_loan_transactions( 20 | validator_app_id: int, 21 | asset_1_id: int, 22 | asset_2_id: int, 23 | asset_1_loan_amount: int, 24 | asset_2_loan_amount: int, 25 | asset_1_payment_amount: int, 26 | asset_2_payment_amount: int, 27 | transactions: "list[Transaction]", 28 | sender: str, 29 | suggested_params: SuggestedParams, 30 | app_call_note: Optional[str] = None, 31 | ) -> TransactionGroup: 32 | assert asset_1_loan_amount or asset_2_loan_amount 33 | 34 | pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) 35 | pool_address = pool_logicsig.address() 36 | min_fee = suggested_params.min_fee 37 | 38 | if asset_1_loan_amount and asset_2_loan_amount: 39 | payment_count = inner_transaction_count = 2 40 | else: 41 | payment_count = inner_transaction_count = 1 42 | 43 | index_diff = len(transactions) + payment_count + 1 44 | txns = [ 45 | # Flash Loan 46 | ApplicationNoOpTxn( 47 | sender=sender, 48 | sp=suggested_params, 49 | index=validator_app_id, 50 | app_args=[ 51 | FLASH_LOAN_APP_ARGUMENT, 52 | index_diff, 53 | asset_1_loan_amount, 54 | asset_2_loan_amount, 55 | ], 56 | foreign_assets=[asset_1_id, asset_2_id], 57 | accounts=[pool_address], 58 | note=app_call_note, 59 | ) 60 | ] 61 | # This app call contains inner transactions 62 | txns[0].fee = min_fee * (inner_transaction_count + 1) 63 | 64 | if transactions: 65 | txns.extend(transactions) 66 | 67 | if asset_1_loan_amount: 68 | txns.append( 69 | AssetTransferTxn( 70 | sender=sender, 71 | sp=suggested_params, 72 | receiver=pool_address, 73 | index=asset_1_id, 74 | amt=asset_1_payment_amount, 75 | ) 76 | ) 77 | 78 | if asset_2_loan_amount: 79 | if asset_2_id: 80 | txns.append( 81 | AssetTransferTxn( 82 | sender=sender, 83 | sp=suggested_params, 84 | receiver=pool_address, 85 | index=asset_2_id, 86 | amt=asset_2_payment_amount, 87 | ) 88 | ) 89 | else: 90 | txns.append( 91 | PaymentTxn( 92 | sender=sender, 93 | sp=suggested_params, 94 | receiver=pool_address, 95 | amt=asset_2_payment_amount, 96 | ) 97 | ) 98 | 99 | # Verify Flash Loan 100 | txns.append( 101 | ApplicationNoOpTxn( 102 | sender=sender, 103 | sp=suggested_params, 104 | index=validator_app_id, 105 | app_args=[VERIFY_FLASH_LOAN_APP_ARGUMENT, index_diff], 106 | foreign_assets=[], 107 | accounts=[pool_address], 108 | note=app_call_note, 109 | ) 110 | ) 111 | 112 | txn_group = TransactionGroup(txns) 113 | return txn_group 114 | -------------------------------------------------------------------------------- /tinyman/v1/client.py: -------------------------------------------------------------------------------- 1 | from base64 import b64decode 2 | from typing import Optional 3 | 4 | from algosdk.encoding import encode_address 5 | from algosdk.v2client.algod import AlgodClient 6 | 7 | from tinyman.assets import AssetAmount 8 | from tinyman.client import BaseTinymanClient 9 | from tinyman.optin import prepare_app_optin_transactions 10 | from tinyman.staking.constants import ( 11 | TESTNET_STAKING_APP_ID, 12 | MAINNET_STAKING_APP_ID, 13 | ) 14 | from tinyman.v1.constants import ( 15 | TESTNET_VALIDATOR_APP_ID, 16 | MAINNET_VALIDATOR_APP_ID, 17 | ) 18 | 19 | 20 | class TinymanClient(BaseTinymanClient): 21 | def fetch_pool(self, asset1, asset2, fetch=True): 22 | from .pools import Pool 23 | 24 | return Pool(self, asset1, asset2, fetch=fetch) 25 | 26 | def prepare_app_optin_transactions(self, user_address=None): 27 | user_address = user_address or self.user_address 28 | suggested_params = self.algod.suggested_params() 29 | txn_group = prepare_app_optin_transactions( 30 | validator_app_id=self.validator_app_id, 31 | sender=user_address, 32 | suggested_params=suggested_params, 33 | app_call_note=self.generate_app_call_note(), 34 | ) 35 | return txn_group 36 | 37 | def fetch_excess_amounts(self, user_address=None): 38 | user_address = user_address or self.user_address 39 | account_info = self.algod.account_info(user_address) 40 | try: 41 | validator_app = [ 42 | a 43 | for a in account_info["apps-local-state"] 44 | if a["id"] == self.validator_app_id 45 | ][0] 46 | except IndexError: 47 | return {} 48 | try: 49 | validator_app_state = { 50 | x["key"]: x["value"] for x in validator_app["key-value"] 51 | } 52 | except KeyError: 53 | return {} 54 | 55 | pools = {} 56 | for key in validator_app_state: 57 | b = b64decode(key.encode()) 58 | if b[-9:-8] == b"e": 59 | value = validator_app_state[key]["uint"] 60 | pool_address = encode_address(b[:-9]) 61 | pools[pool_address] = pools.get(pool_address, {}) 62 | asset_id = int.from_bytes(b[-8:], "big") 63 | asset = self.fetch_asset(asset_id) 64 | pools[pool_address][asset] = AssetAmount(asset, value) 65 | 66 | return pools 67 | 68 | 69 | class TinymanTestnetClient(TinymanClient): 70 | def __init__( 71 | self, 72 | algod_client: AlgodClient, 73 | user_address: Optional[str] = None, 74 | client_name: Optional[str] = None, 75 | ): 76 | super().__init__( 77 | algod_client, 78 | validator_app_id=TESTNET_VALIDATOR_APP_ID, 79 | api_base_url="https://testnet.analytics.tinyman.org/api/", 80 | user_address=user_address, 81 | staking_app_id=TESTNET_STAKING_APP_ID, 82 | client_name=client_name, 83 | ) 84 | 85 | 86 | class TinymanMainnetClient(TinymanClient): 87 | def __init__( 88 | self, 89 | algod_client: AlgodClient, 90 | user_address: Optional[str] = None, 91 | client_name: Optional[str] = None, 92 | ): 93 | super().__init__( 94 | algod_client, 95 | validator_app_id=MAINNET_VALIDATOR_APP_ID, 96 | api_base_url="https://mainnet.analytics.tinyman.org/api/", 97 | user_address=user_address, 98 | staking_app_id=MAINNET_STAKING_APP_ID, 99 | client_name=client_name, 100 | ) 101 | -------------------------------------------------------------------------------- /tinyman/client.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from tinyman.compat import wait_for_confirmation 4 | from algosdk.v2client.algod import AlgodClient 5 | 6 | from tinyman.assets import Asset 7 | from tinyman.optin import prepare_asset_optin_transactions 8 | from tinyman.utils import get_version, generate_app_call_note 9 | 10 | 11 | class BaseTinymanClient: 12 | def __init__( 13 | self, 14 | algod_client: AlgodClient, 15 | validator_app_id: int, 16 | api_base_url: Optional[str] = None, 17 | user_address: Optional[str] = None, 18 | staking_app_id: Optional[int] = None, 19 | client_name: Optional[str] = None, 20 | ): 21 | self.algod = algod_client 22 | self.api_base_url = api_base_url 23 | self.validator_app_id = validator_app_id 24 | self.staking_app_id = staking_app_id 25 | self.assets_cache = {} 26 | self.user_address = user_address 27 | self.client_name = client_name 28 | 29 | def fetch_pool(self, *args, **kwargs): 30 | raise NotImplementedError() 31 | 32 | def fetch_asset(self, asset_id: int): 33 | asset_id = int(asset_id) 34 | 35 | if asset_id not in self.assets_cache: 36 | asset = Asset(asset_id) 37 | asset.fetch(self.algod) 38 | self.assets_cache[asset_id] = asset 39 | return self.assets_cache[asset_id] 40 | 41 | def submit(self, transaction_group, wait=False): 42 | try: 43 | txid = self.algod.send_transactions(transaction_group.signed_transactions) 44 | except Exception as e: 45 | self.handle_error(e, transaction_group) 46 | if wait: 47 | txn_info = wait_for_confirmation(self.algod, txid) 48 | txn_info["txid"] = txid 49 | return txn_info 50 | return {"txid": txid} 51 | 52 | def handle_error(self, exception, transaction_group): 53 | error_message = str(exception) 54 | raise Exception(error_message) from None 55 | 56 | def prepare_asset_optin_transactions( 57 | self, asset_id, user_address=None, suggested_params=None 58 | ): 59 | user_address = user_address or self.user_address 60 | if suggested_params is None: 61 | suggested_params = self.algod.suggested_params() 62 | txn_group = prepare_asset_optin_transactions( 63 | asset_id=asset_id, 64 | sender=user_address, 65 | suggested_params=suggested_params, 66 | ) 67 | return txn_group 68 | 69 | @property 70 | def version(self) -> str: 71 | return get_version(self.validator_app_id) 72 | 73 | def is_opted_in(self, user_address=None): 74 | user_address = user_address or self.user_address 75 | account_info = self.algod.account_info(user_address) 76 | for a in account_info.get("apps-local-state", []): 77 | if a["id"] == self.validator_app_id: 78 | return True 79 | return False 80 | 81 | def asset_is_opted_in(self, asset_id, user_address=None): 82 | user_address = user_address or self.user_address 83 | 84 | if asset_id == 0: 85 | # ALGO 86 | return True 87 | 88 | account_info = self.algod.account_info(user_address) 89 | for a in account_info.get("assets", []): 90 | if a["asset-id"] == asset_id: 91 | return True 92 | return False 93 | 94 | def generate_app_call_note(self, client_name: Optional[str] = None): 95 | note = generate_app_call_note( 96 | version=self.version, 97 | client_name=client_name or self.client_name, 98 | ) 99 | return note 100 | -------------------------------------------------------------------------------- /tinyman/v2/remove_liquidity.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from tinyman.compat import ( 4 | ApplicationNoOpTxn, 5 | AssetTransferTxn, 6 | SuggestedParams, 7 | ) 8 | 9 | from tinyman.utils import TransactionGroup 10 | from .constants import REMOVE_LIQUIDITY_APP_ARGUMENT 11 | from .contracts import get_pool_logicsig 12 | 13 | 14 | def prepare_remove_liquidity_transactions( 15 | validator_app_id: int, 16 | asset_1_id: int, 17 | asset_2_id: int, 18 | pool_token_asset_id: int, 19 | min_asset_1_amount: int, 20 | min_asset_2_amount: int, 21 | pool_token_asset_amount: int, 22 | sender: str, 23 | suggested_params: SuggestedParams, 24 | app_call_note: Optional[str] = None, 25 | ) -> TransactionGroup: 26 | pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) 27 | pool_address = pool_logicsig.address() 28 | 29 | txns = [ 30 | AssetTransferTxn( 31 | sender=sender, 32 | sp=suggested_params, 33 | receiver=pool_address, 34 | index=pool_token_asset_id, 35 | amt=pool_token_asset_amount, 36 | ), 37 | ApplicationNoOpTxn( 38 | sender=sender, 39 | sp=suggested_params, 40 | index=validator_app_id, 41 | app_args=[ 42 | REMOVE_LIQUIDITY_APP_ARGUMENT, 43 | min_asset_1_amount, 44 | min_asset_2_amount, 45 | ], 46 | foreign_assets=[asset_1_id, asset_2_id], 47 | accounts=[pool_address], 48 | note=app_call_note, 49 | ), 50 | ] 51 | 52 | # App call contains 2 inner transactions 53 | min_fee = suggested_params.min_fee 54 | app_call_fee = min_fee * 3 55 | txns[-1].fee = app_call_fee 56 | 57 | txn_group = TransactionGroup(txns) 58 | return txn_group 59 | 60 | 61 | def prepare_single_asset_remove_liquidity_transactions( 62 | validator_app_id: int, 63 | asset_1_id: int, 64 | asset_2_id: int, 65 | pool_token_asset_id: int, 66 | output_asset_id: int, 67 | min_output_asset_amount: int, 68 | pool_token_asset_amount: int, 69 | sender: str, 70 | suggested_params: SuggestedParams, 71 | app_call_note: Optional[str] = None, 72 | ) -> TransactionGroup: 73 | pool_logicsig = get_pool_logicsig(validator_app_id, asset_1_id, asset_2_id) 74 | pool_address = pool_logicsig.address() 75 | 76 | if output_asset_id == asset_1_id: 77 | min_asset_1_amount = min_output_asset_amount 78 | min_asset_2_amount = 0 79 | elif output_asset_id == asset_2_id: 80 | min_asset_1_amount = 0 81 | min_asset_2_amount = min_output_asset_amount 82 | else: 83 | assert False 84 | 85 | txns = [ 86 | AssetTransferTxn( 87 | sender=sender, 88 | sp=suggested_params, 89 | receiver=pool_address, 90 | index=pool_token_asset_id, 91 | amt=pool_token_asset_amount, 92 | ), 93 | ApplicationNoOpTxn( 94 | sender=sender, 95 | sp=suggested_params, 96 | index=validator_app_id, 97 | app_args=[ 98 | REMOVE_LIQUIDITY_APP_ARGUMENT, 99 | min_asset_1_amount, 100 | min_asset_2_amount, 101 | ], 102 | foreign_assets=[output_asset_id], 103 | accounts=[pool_address], 104 | note=app_call_note, 105 | ), 106 | ] 107 | 108 | # App call contains 2 inner transactions 109 | min_fee = suggested_params.min_fee 110 | app_call_fee = min_fee * 3 111 | txns[-1].fee = app_call_fee 112 | 113 | txn_group = TransactionGroup(txns) 114 | return txn_group 115 | -------------------------------------------------------------------------------- /tinyman/governance/vault/events.py: -------------------------------------------------------------------------------- 1 | from algosdk import abi 2 | 3 | from tinyman.governance.event import Event 4 | 5 | event_init = Event( 6 | name="init", 7 | args=[] 8 | ) 9 | 10 | event_create_checkpoints = Event( 11 | name="create_checkpoints", 12 | args=[] 13 | ) 14 | 15 | event_del_box = Event( 16 | name="box_del", 17 | args=[ 18 | abi.Argument(arg_type="byte[]", name="box_name"), 19 | ] 20 | ) 21 | 22 | event_delete_account_state = Event( 23 | name="delete_account_state", 24 | args=[ 25 | abi.Argument(arg_type="address", name="user_address"), 26 | abi.Argument(arg_type="uint64", name="box_index_start"), 27 | abi.Argument(arg_type="uint64", name="box_count"), 28 | ] 29 | ) 30 | 31 | event_delete_account_power_boxes = Event( 32 | name="delete_account_power_boxes", 33 | args=[ 34 | abi.Argument(arg_type="address", name="user_address"), 35 | abi.Argument(arg_type="uint64", name="box_index_start"), 36 | abi.Argument(arg_type="uint64", name="box_count"), 37 | ] 38 | ) 39 | 40 | event_create_lock = Event( 41 | name="create_lock", 42 | args=[ 43 | abi.Argument(arg_type="address", name="user_address"), 44 | abi.Argument(arg_type="uint64", name="locked_amount"), 45 | abi.Argument(arg_type="uint64", name="lock_end_time"), 46 | ] 47 | ) 48 | 49 | event_increase_lock_amount = Event( 50 | name="increase_lock_amount", 51 | args=[ 52 | abi.Argument(arg_type="address", name="user_address"), 53 | abi.Argument(arg_type="uint64", name="locked_amount"), 54 | abi.Argument(arg_type="uint64", name="lock_end_time"), 55 | abi.Argument(arg_type="uint64", name="amount_delta"), 56 | ] 57 | ) 58 | 59 | event_extend_lock_end_time = Event( 60 | name="extend_lock_end_time", 61 | args=[ 62 | abi.Argument(arg_type="address", name="user_address"), 63 | abi.Argument(arg_type="uint64", name="locked_amount"), 64 | abi.Argument(arg_type="uint64", name="lock_end_time"), 65 | abi.Argument(arg_type="uint64", name="time_delta"), 66 | ] 67 | ) 68 | 69 | event_withdraw = Event( 70 | name="withdraw", 71 | args=[ 72 | abi.Argument(arg_type="address", name="user_address"), 73 | abi.Argument(arg_type="uint64", name="amount"), 74 | ] 75 | ) 76 | 77 | event_account_power = Event( 78 | name="account_power", 79 | args=[ 80 | abi.Argument(arg_type="address", name="user_address"), 81 | abi.Argument(arg_type="uint64", name="index"), 82 | abi.Argument(arg_type="uint64", name="bias"), 83 | abi.Argument(arg_type="uint64", name="timestamp"), 84 | abi.Argument(arg_type="uint128", name="slope"), 85 | abi.Argument(arg_type="uint128", name="cumulative_power"), 86 | ] 87 | ) 88 | 89 | event_total_power = Event( 90 | name="total_power", 91 | args=[ 92 | abi.Argument(arg_type="uint64", name="index"), 93 | abi.Argument(arg_type="uint64", name="bias"), 94 | abi.Argument(arg_type="uint64", name="timestamp"), 95 | abi.Argument(arg_type="uint128", name="slope"), 96 | abi.Argument(arg_type="uint128", name="cumulative_power"), 97 | ] 98 | ) 99 | 100 | event_slope_change = Event( 101 | name="slope_change", 102 | args=[ 103 | abi.Argument(arg_type="uint64", name="timestamp"), 104 | abi.Argument(arg_type="uint128", name="slope"), 105 | ] 106 | ) 107 | 108 | vault_events = [ 109 | # method calls 110 | event_init, 111 | event_create_checkpoints, 112 | event_create_lock, 113 | event_increase_lock_amount, 114 | event_extend_lock_end_time, 115 | event_withdraw, 116 | event_del_box, 117 | event_delete_account_state, 118 | event_delete_account_power_boxes, 119 | # boxes 120 | event_account_power, 121 | event_total_power, 122 | event_slope_change, 123 | ] 124 | -------------------------------------------------------------------------------- /tinyman/folks_lending/utils.py: -------------------------------------------------------------------------------- 1 | from base64 import b64decode, b64encode 2 | from datetime import datetime 3 | 4 | from tinyman.utils import bytes_to_int 5 | from tinyman.constants import YEAR, HOURS_PER_YEAR 6 | 7 | 8 | def get_asset_pair_from_pool_app(algod, app_id): 9 | app = algod.application_info(app_id) 10 | global_state = {x["key"]: x["value"]["bytes"] for x in app["params"]["global-state"]} 11 | 12 | b = b64decode(global_state[b64encode(b"a").decode()]) 13 | 14 | asset_id, f_asset_id = bytes_to_int(b[:8]), bytes_to_int(b[8:16]) 15 | return asset_id, f_asset_id 16 | 17 | 18 | def get_lending_pools(algod, pool_manager_app_id): 19 | # Get global state of lending manager app. 20 | app = algod.application_info(pool_manager_app_id) 21 | global_state = {x["key"]: x["value"]["bytes"] for x in app["params"]["global-state"]} 22 | 23 | # Concatanate all the global state values. 24 | data = b"" 25 | for i in range(63): 26 | key = b64encode((i).to_bytes(1, "big")).decode() 27 | data += b64decode(global_state[key]) # 126 bytes 28 | 29 | # Iterate over the data and parse. 30 | pools = [] 31 | for i in range(186): 32 | pool = parse_lending_pool_info(data[(42 * i): (42 * (i + 1))]) 33 | 34 | if pool["pool_app_id"]: 35 | asset_id, f_asset_id = get_asset_pair_from_pool_app(algod, pool["pool_app_id"]) 36 | pool["asset_id"] = asset_id 37 | pool["f_asset_id"] = f_asset_id 38 | 39 | pools.append(pool) 40 | 41 | return pools 42 | 43 | 44 | def exp_by_squaring(x, n, scale): 45 | """Returns: x**n""" 46 | if n == 0: 47 | return scale 48 | 49 | y = scale 50 | while n > 1: 51 | if n % 2: 52 | y = (x * y) / scale 53 | n = (n - 1) // 2 54 | else: 55 | n = n // 2 56 | x = (x * x) / scale 57 | 58 | return int((x * y) / scale) 59 | 60 | 61 | def calculate_borrow_interest_index(variable_borrow_interest_rate, old_variable_borrow_interest_index, timestamp: int): 62 | timedelta = int(datetime.now().timestamp()) - timestamp 63 | return int(old_variable_borrow_interest_index * exp_by_squaring(int(1e16) + variable_borrow_interest_rate / YEAR, timedelta, int(1e16)) / int(1e16)) 64 | 65 | 66 | def calculate_deposit_interest_index(deposit_interest_rate, old_deposit_interest_index, timestamp): 67 | timedelta = int(datetime.now().timestamp()) - timestamp 68 | return int(old_deposit_interest_index * (int(1e16) + (deposit_interest_rate * timedelta) / YEAR) / int(1e16)) 69 | 70 | 71 | def compound(rate, scale, period): 72 | return exp_by_squaring(scale + (rate / period), period, scale) - scale 73 | 74 | 75 | def compound_every_second(rate, scale): 76 | return compound(rate, scale, YEAR) 77 | 78 | 79 | def compound_every_hour(rate, scale): 80 | return compound(rate, scale, HOURS_PER_YEAR) 81 | 82 | 83 | def parse_lending_pool_info(pool_data) -> dict: 84 | pool = {} 85 | pool["pool_app_id"] = bytes_to_int(pool_data[0:6]) 86 | pool["variable_borrow_interest_rate"] = bytes_to_int(pool_data[6:14]) 87 | pool["old_variable_borrow_interest_index"] = bytes_to_int(pool_data[14:22]) 88 | pool["deposit_interest_rate"] = bytes_to_int(pool_data[22:30]) 89 | pool["old_deposit_interest_index"] = bytes_to_int(pool_data[30:38]) 90 | pool["old_timestamp"] = bytes_to_int(pool_data[38:42]) 91 | 92 | pool["variable_borrow_interest_yield"] = compound_every_second(pool["variable_borrow_interest_rate"], int(1e16)) 93 | pool["deposit_interest_yield"] = compound_every_hour(pool["deposit_interest_rate"], int(1e16)) 94 | 95 | pool["variable_borrow_interest_index"] = calculate_borrow_interest_index(pool["variable_borrow_interest_rate"], pool["old_variable_borrow_interest_index"], pool["old_timestamp"]) 96 | pool["deposit_interest_index"] = calculate_deposit_interest_index(pool["deposit_interest_rate"], pool["old_deposit_interest_index"], pool["old_timestamp"]) 97 | 98 | return pool 99 | -------------------------------------------------------------------------------- /examples/v2/tutorial/13_flash_swap_1_pay_in_other_currency.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | from pprint import pprint 5 | from urllib.parse import quote_plus 6 | 7 | from tinyman.compat import AssetTransferTxn, PaymentTxn 8 | 9 | from examples.v2.tutorial.common import get_account, get_assets 10 | from examples.v2.utils import get_algod 11 | from tinyman.v2.client import TinymanV2TestnetClient 12 | from tinyman.v2.flash_swap import prepare_flash_swap_transactions 13 | from tinyman.v2.formulas import calculate_flash_swap_asset_2_payment_amount 14 | 15 | account = get_account() 16 | algod = get_algod() 17 | client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) 18 | 19 | ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] 20 | ASSET_A = client.fetch_asset(ASSET_A_ID) 21 | ASSET_B = client.fetch_asset(ASSET_B_ID) 22 | pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) 23 | 24 | suggested_params = algod.suggested_params() 25 | account_info = algod.account_info(account["address"]) 26 | 27 | for asset in account_info["assets"]: 28 | if asset["asset-id"] == pool.asset_1.id: 29 | balance = asset["amount"] 30 | 31 | asset_1_loan_amount = 1_000_000 32 | asset_2_loan_amount = 0 33 | asset_1_payment_amount = 0 34 | asset_2_payment_amount = calculate_flash_swap_asset_2_payment_amount( 35 | asset_1_reserves=pool.asset_1_reserves, 36 | asset_2_reserves=pool.asset_2_reserves, 37 | total_fee_share=pool.total_fee_share, 38 | protocol_fee_ratio=pool.protocol_fee_ratio, 39 | asset_1_loan_amount=asset_1_loan_amount, 40 | asset_2_loan_amount=asset_2_loan_amount, 41 | asset_1_payment_amount=asset_1_payment_amount, 42 | ) 43 | 44 | # Transfer amount is equal to sum of initial account balance and loan amount 45 | # This transaction demonstrate that you can use the total amount 46 | transfer_amount = balance + asset_1_loan_amount 47 | transactions = [ 48 | AssetTransferTxn( 49 | sender=account["address"], 50 | sp=suggested_params, 51 | receiver=account["address"], 52 | amt=transfer_amount, 53 | index=pool.asset_1.id, 54 | ) 55 | ] 56 | 57 | if asset_1_payment_amount: 58 | transactions.append( 59 | AssetTransferTxn( 60 | sender=account["address"], 61 | sp=suggested_params, 62 | receiver=pool.address, 63 | index=pool.asset_1.id, 64 | amt=asset_1_payment_amount, 65 | ) 66 | ) 67 | 68 | if asset_2_payment_amount: 69 | if pool.asset_2.id: 70 | transactions.append( 71 | AssetTransferTxn( 72 | sender=account["address"], 73 | sp=suggested_params, 74 | receiver=pool.address, 75 | index=pool.asset_2.id, 76 | amt=asset_2_payment_amount, 77 | ) 78 | ) 79 | else: 80 | transactions.append( 81 | PaymentTxn( 82 | sender=account["address"], 83 | sp=suggested_params, 84 | receiver=pool.address, 85 | amt=asset_2_payment_amount, 86 | ) 87 | ) 88 | 89 | txn_group = prepare_flash_swap_transactions( 90 | validator_app_id=pool.validator_app_id, 91 | asset_1_id=pool.asset_1.id, 92 | asset_2_id=pool.asset_2.id, 93 | asset_1_loan_amount=asset_1_loan_amount, 94 | asset_2_loan_amount=asset_2_loan_amount, 95 | transactions=transactions, 96 | suggested_params=suggested_params, 97 | sender=account["address"], 98 | ) 99 | 100 | # Sign 101 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 102 | 103 | # Submit transactions to the network and wait for confirmation 104 | txn_info = client.submit(txn_group, wait=True) 105 | print("Transaction Info") 106 | pprint(txn_info) 107 | 108 | print( 109 | f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" 110 | ) 111 | -------------------------------------------------------------------------------- /examples/v2/tutorial/14_flash_swap_2_pay_in_same_currency.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | from pprint import pprint 5 | from urllib.parse import quote_plus 6 | 7 | from tinyman.compat import AssetTransferTxn, PaymentTxn 8 | 9 | from examples.v2.tutorial.common import get_account, get_assets 10 | from examples.v2.utils import get_algod 11 | from tinyman.v2.client import TinymanV2TestnetClient 12 | from tinyman.v2.flash_swap import prepare_flash_swap_transactions 13 | from tinyman.v2.formulas import calculate_flash_swap_asset_1_payment_amount 14 | 15 | account = get_account() 16 | algod = get_algod() 17 | client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) 18 | 19 | ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] 20 | ASSET_A = client.fetch_asset(ASSET_A_ID) 21 | ASSET_B = client.fetch_asset(ASSET_B_ID) 22 | pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) 23 | 24 | suggested_params = algod.suggested_params() 25 | account_info = algod.account_info(account["address"]) 26 | 27 | for asset in account_info["assets"]: 28 | if asset["asset-id"] == pool.asset_1.id: 29 | balance = asset["amount"] 30 | 31 | asset_1_loan_amount = 1_000_000 32 | asset_2_loan_amount = 0 33 | asset_2_payment_amount = 0 34 | asset_1_payment_amount = calculate_flash_swap_asset_1_payment_amount( 35 | asset_1_reserves=pool.asset_1_reserves, 36 | asset_2_reserves=pool.asset_2_reserves, 37 | total_fee_share=pool.total_fee_share, 38 | protocol_fee_ratio=pool.protocol_fee_ratio, 39 | asset_1_loan_amount=asset_1_loan_amount, 40 | asset_2_loan_amount=asset_2_loan_amount, 41 | asset_2_payment_amount=asset_2_payment_amount, 42 | ) 43 | 44 | # Transfer amount is equal to sum of initial account balance and loan amount 45 | # This transaction demonstrate that you can use the total amount 46 | transfer_amount = balance + asset_1_loan_amount 47 | transactions = [ 48 | AssetTransferTxn( 49 | sender=account["address"], 50 | sp=suggested_params, 51 | receiver=account["address"], 52 | amt=transfer_amount, 53 | index=pool.asset_1.id, 54 | ) 55 | ] 56 | 57 | 58 | if asset_1_payment_amount: 59 | transactions.append( 60 | AssetTransferTxn( 61 | sender=account["address"], 62 | sp=suggested_params, 63 | receiver=pool.address, 64 | index=pool.asset_1.id, 65 | amt=asset_1_payment_amount, 66 | ) 67 | ) 68 | 69 | if asset_2_payment_amount: 70 | if pool.asset_2.id: 71 | transactions.append( 72 | AssetTransferTxn( 73 | sender=account["address"], 74 | sp=suggested_params, 75 | receiver=pool.address, 76 | index=pool.asset_2.id, 77 | amt=asset_2_payment_amount, 78 | ) 79 | ) 80 | else: 81 | transactions.append( 82 | PaymentTxn( 83 | sender=account["address"], 84 | sp=suggested_params, 85 | receiver=pool.address, 86 | amt=asset_2_payment_amount, 87 | ) 88 | ) 89 | 90 | txn_group = prepare_flash_swap_transactions( 91 | validator_app_id=pool.validator_app_id, 92 | asset_1_id=pool.asset_1.id, 93 | asset_2_id=pool.asset_2.id, 94 | asset_1_loan_amount=asset_1_loan_amount, 95 | asset_2_loan_amount=asset_2_loan_amount, 96 | transactions=transactions, 97 | suggested_params=suggested_params, 98 | sender=account["address"], 99 | ) 100 | 101 | # Sign 102 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 103 | 104 | # Submit transactions to the network and wait for confirmation 105 | txn_info = client.submit(txn_group, wait=True) 106 | print("Transaction Info") 107 | pprint(txn_info) 108 | 109 | print( 110 | f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" 111 | ) 112 | -------------------------------------------------------------------------------- /examples/v2/tutorial/15_flash_swap_3_pay_in_multiple_currencies.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | from pprint import pprint 5 | from urllib.parse import quote_plus 6 | 7 | from tinyman.compat import AssetTransferTxn, PaymentTxn 8 | 9 | from examples.v2.tutorial.common import get_account, get_assets 10 | from examples.v2.utils import get_algod 11 | from tinyman.v2.client import TinymanV2TestnetClient 12 | from tinyman.v2.flash_swap import prepare_flash_swap_transactions 13 | from tinyman.v2.formulas import calculate_flash_swap_asset_2_payment_amount 14 | 15 | account = get_account() 16 | algod = get_algod() 17 | client = TinymanV2TestnetClient(algod_client=algod, user_address=account["address"]) 18 | 19 | ASSET_A_ID, ASSET_B_ID = get_assets()["ids"] 20 | ASSET_A = client.fetch_asset(ASSET_A_ID) 21 | ASSET_B = client.fetch_asset(ASSET_B_ID) 22 | pool = client.fetch_pool(ASSET_A_ID, ASSET_B_ID) 23 | 24 | suggested_params = algod.suggested_params() 25 | account_info = algod.account_info(account["address"]) 26 | 27 | for asset in account_info["assets"]: 28 | if asset["asset-id"] == pool.asset_1.id: 29 | balance = asset["amount"] 30 | 31 | asset_1_loan_amount = 1_000_000 32 | asset_2_loan_amount = 0 33 | asset_1_payment_amount = 500_000 34 | asset_2_payment_amount = calculate_flash_swap_asset_2_payment_amount( 35 | asset_1_reserves=pool.asset_1_reserves, 36 | asset_2_reserves=pool.asset_2_reserves, 37 | total_fee_share=pool.total_fee_share, 38 | protocol_fee_ratio=pool.protocol_fee_ratio, 39 | asset_1_loan_amount=asset_1_loan_amount, 40 | asset_2_loan_amount=asset_2_loan_amount, 41 | asset_1_payment_amount=asset_1_payment_amount, 42 | ) 43 | 44 | # Transfer amount is equal to sum of initial account balance and loan amount 45 | # This transaction demonstrate that you can use the total amount 46 | transfer_amount = balance + asset_1_loan_amount 47 | transactions = [ 48 | AssetTransferTxn( 49 | sender=account["address"], 50 | sp=suggested_params, 51 | receiver=account["address"], 52 | amt=transfer_amount, 53 | index=pool.asset_1.id, 54 | ) 55 | ] 56 | 57 | 58 | if asset_1_payment_amount: 59 | transactions.append( 60 | AssetTransferTxn( 61 | sender=account["address"], 62 | sp=suggested_params, 63 | receiver=pool.address, 64 | index=pool.asset_1.id, 65 | amt=asset_1_payment_amount, 66 | ) 67 | ) 68 | 69 | if asset_2_payment_amount: 70 | if pool.asset_2.id: 71 | transactions.append( 72 | AssetTransferTxn( 73 | sender=account["address"], 74 | sp=suggested_params, 75 | receiver=pool.address, 76 | index=pool.asset_2.id, 77 | amt=asset_2_payment_amount, 78 | ) 79 | ) 80 | else: 81 | transactions.append( 82 | PaymentTxn( 83 | sender=account["address"], 84 | sp=suggested_params, 85 | receiver=pool.address, 86 | amt=asset_2_payment_amount, 87 | ) 88 | ) 89 | 90 | txn_group = prepare_flash_swap_transactions( 91 | validator_app_id=pool.validator_app_id, 92 | asset_1_id=pool.asset_1.id, 93 | asset_2_id=pool.asset_2.id, 94 | asset_1_loan_amount=asset_1_loan_amount, 95 | asset_2_loan_amount=asset_2_loan_amount, 96 | transactions=transactions, 97 | suggested_params=suggested_params, 98 | sender=account["address"], 99 | ) 100 | 101 | # Sign 102 | txn_group.sign_with_private_key(account["address"], account["private_key"]) 103 | 104 | # Submit transactions to the network and wait for confirmation 105 | txn_info = client.submit(txn_group, wait=True) 106 | print("Transaction Info") 107 | pprint(txn_info) 108 | 109 | print( 110 | f"Check the transaction group on Algoexplorer: https://testnet.algoexplorer.io/tx/group/{quote_plus(txn_group.id)}" 111 | ) 112 | -------------------------------------------------------------------------------- /tests/test_app_call_note.py: -------------------------------------------------------------------------------- 1 | from base64 import b64encode 2 | from unittest import TestCase 3 | 4 | from algosdk.v2client.algod import AlgodClient 5 | 6 | from tinyman.utils import generate_app_call_note, parse_app_call_note 7 | from tinyman.v1.client import TinymanClient, TinymanTestnetClient, TinymanMainnetClient 8 | from tinyman.v1.constants import TESTNET_VALIDATOR_APP_ID_V1_1 9 | from tinyman.v2.client import ( 10 | TinymanV2Client, 11 | TinymanV2TestnetClient, 12 | TinymanV2MainnetClient, 13 | ) 14 | from tinyman.v2.constants import TESTNET_VALIDATOR_APP_ID_V2 15 | 16 | 17 | class AppCallNoteTestCase(TestCase): 18 | maxDiff = None 19 | 20 | def test_app_call_note(self): 21 | note = generate_app_call_note( 22 | version="v2", client_name="unit-test", extra_data={"extra": "some text"} 23 | ) 24 | expected_result = { 25 | "version": "v2", 26 | "data": {"extra": "some text", "origin": "unit-test"}, 27 | } 28 | 29 | # test possible versions 30 | string_note = note 31 | bytes_note = string_note.encode() 32 | base64_note = b64encode(bytes_note).decode() 33 | 34 | result = parse_app_call_note(string_note) 35 | self.assertDictEqual( 36 | result, 37 | expected_result, 38 | ) 39 | 40 | result = parse_app_call_note(base64_note) 41 | self.assertDictEqual( 42 | result, 43 | expected_result, 44 | ) 45 | 46 | result = parse_app_call_note(base64_note) 47 | self.assertDictEqual( 48 | result, 49 | expected_result, 50 | ) 51 | 52 | result = parse_app_call_note("invalid format") 53 | self.assertEqual(result, None) 54 | result = parse_app_call_note( 55 | "INVALID+dGlueW1hbi92MjpqeyJvcmlnaW4iOiJ0aW55bWFuLXB5dGhvbi1zZGsifQ==" 56 | ) 57 | self.assertEqual(result, None) 58 | result = parse_app_call_note(b"invalid format") 59 | self.assertEqual(result, None) 60 | result = parse_app_call_note( 61 | b'INVALID+tinyman/v2:j{"origin":"tinyman-python-sdk"}' 62 | ) 63 | self.assertEqual(result, None) 64 | 65 | def test_tinyman_clients(self): 66 | algod_client = AlgodClient(algod_token="", algod_address="") 67 | client_name = "test" 68 | 69 | # V1 70 | self.assertEqual( 71 | TinymanClient( 72 | algod_client=algod_client, 73 | client_name=client_name, 74 | validator_app_id=TESTNET_VALIDATOR_APP_ID_V1_1, 75 | ).generate_app_call_note(), 76 | 'tinyman/v1:j{"origin":"test"}', 77 | ) 78 | self.assertEqual( 79 | TinymanTestnetClient( 80 | algod_client=algod_client, client_name=client_name 81 | ).generate_app_call_note(), 82 | 'tinyman/v1:j{"origin":"test"}', 83 | ) 84 | self.assertEqual( 85 | TinymanMainnetClient( 86 | algod_client=algod_client, client_name=client_name 87 | ).generate_app_call_note(), 88 | 'tinyman/v1:j{"origin":"test"}', 89 | ) 90 | 91 | # V2 92 | self.assertEqual( 93 | TinymanV2Client( 94 | algod_client=algod_client, 95 | client_name=client_name, 96 | validator_app_id=TESTNET_VALIDATOR_APP_ID_V2, 97 | ).generate_app_call_note(), 98 | 'tinyman/v2:j{"origin":"test"}', 99 | ) 100 | self.assertEqual( 101 | TinymanV2TestnetClient( 102 | algod_client=algod_client, client_name=client_name 103 | ).generate_app_call_note(), 104 | 'tinyman/v2:j{"origin":"test"}', 105 | ) 106 | self.assertEqual( 107 | TinymanV2MainnetClient( 108 | algod_client=algod_client, client_name=client_name 109 | ).generate_app_call_note(), 110 | 'tinyman/v2:j{"origin":"test"}', 111 | ) 112 | -------------------------------------------------------------------------------- /tinyman/v2/quotes.py: -------------------------------------------------------------------------------- 1 | import math 2 | from dataclasses import dataclass 3 | 4 | from tinyman.assets import AssetAmount, Asset 5 | 6 | 7 | @dataclass 8 | class SwapQuote: 9 | swap_type: str 10 | amount_in: AssetAmount 11 | amount_out: AssetAmount 12 | swap_fees: AssetAmount 13 | slippage: float 14 | price_impact: float 15 | 16 | @property 17 | def amount_out_with_slippage(self) -> AssetAmount: 18 | if self.swap_type == "fixed-output": 19 | return self.amount_out 20 | 21 | amount_with_slippage = self.amount_out.amount - int( 22 | self.amount_out.amount * self.slippage 23 | ) 24 | return AssetAmount(self.amount_out.asset, amount_with_slippage) 25 | 26 | @property 27 | def amount_in_with_slippage(self) -> AssetAmount: 28 | if self.swap_type == "fixed-input": 29 | return self.amount_in 30 | 31 | amount_with_slippage = self.amount_in.amount + int( 32 | self.amount_in.amount * self.slippage 33 | ) 34 | return AssetAmount(self.amount_in.asset, amount_with_slippage) 35 | 36 | @property 37 | def price(self) -> float: 38 | return self.amount_out.amount / self.amount_in.amount 39 | 40 | @property 41 | def price_with_slippage(self) -> float: 42 | return ( 43 | self.amount_out_with_slippage.amount / self.amount_in_with_slippage.amount 44 | ) 45 | 46 | 47 | @dataclass 48 | class InternalSwapQuote: 49 | amount_in: AssetAmount 50 | amount_out: AssetAmount 51 | swap_fees: AssetAmount 52 | price_impact: float 53 | 54 | @property 55 | def price(self) -> float: 56 | return self.amount_out.amount / self.amount_in.amount 57 | 58 | 59 | @dataclass 60 | class FlexibleAddLiquidityQuote: 61 | amounts_in: "dict[Asset, AssetAmount]" 62 | pool_token_asset_amount: AssetAmount 63 | slippage: float 64 | internal_swap_quote: InternalSwapQuote = None 65 | 66 | @property 67 | def min_pool_token_asset_amount_with_slippage(self) -> int: 68 | return self.pool_token_asset_amount.amount - math.ceil( 69 | self.pool_token_asset_amount.amount * self.slippage 70 | ) 71 | 72 | 73 | @dataclass 74 | class SingleAssetAddLiquidityQuote: 75 | amount_in: AssetAmount 76 | pool_token_asset_amount: AssetAmount 77 | slippage: float 78 | internal_swap_quote: InternalSwapQuote = None 79 | 80 | @property 81 | def min_pool_token_asset_amount_with_slippage(self) -> int: 82 | return self.pool_token_asset_amount.amount - math.ceil( 83 | self.pool_token_asset_amount.amount * self.slippage 84 | ) 85 | 86 | 87 | @dataclass 88 | class InitialAddLiquidityQuote: 89 | amounts_in: "dict[Asset, AssetAmount]" 90 | pool_token_asset_amount: AssetAmount 91 | 92 | 93 | @dataclass 94 | class RemoveLiquidityQuote: 95 | amounts_out: "dict[Asset, AssetAmount]" 96 | pool_token_asset_amount: AssetAmount 97 | slippage: float 98 | 99 | @property 100 | def amounts_out_with_slippage(self) -> "dict[Asset, AssetAmount]": 101 | amounts_out = {} 102 | for asset, asset_amount in self.amounts_out.items(): 103 | amount_with_slippage = asset_amount.amount - int( 104 | (asset_amount.amount * self.slippage) 105 | ) 106 | amounts_out[asset] = AssetAmount(asset, amount_with_slippage) 107 | return amounts_out 108 | 109 | 110 | @dataclass 111 | class SingleAssetRemoveLiquidityQuote: 112 | amount_out: AssetAmount 113 | pool_token_asset_amount: AssetAmount 114 | slippage: float 115 | internal_swap_quote: InternalSwapQuote = None 116 | 117 | @property 118 | def amount_out_with_slippage(self) -> AssetAmount: 119 | amount_with_slippage = self.amount_out.amount - int( 120 | self.amount_out.amount * self.slippage 121 | ) 122 | return AssetAmount(self.amount_out.asset, amount_with_slippage) 123 | 124 | 125 | @dataclass 126 | class FlashLoanQuote: 127 | amounts_out: "dict[Asset, AssetAmount]" 128 | amounts_in: "dict[Asset, AssetAmount]" 129 | fees: "dict[Asset, AssetAmount]" 130 | -------------------------------------------------------------------------------- /tests/v2/test_flash_swap.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import ANY 2 | 3 | from algosdk.account import generate_account 4 | from algosdk.constants import APPCALL_TXN 5 | from algosdk.encoding import decode_address 6 | from tinyman.compat import OnComplete 7 | from algosdk.logic import get_application_address 8 | 9 | from tests.v2 import BaseTestCase 10 | from tinyman.utils import int_to_bytes 11 | from tinyman.v2.constants import ( 12 | FLASH_SWAP_APP_ARGUMENT, 13 | VERIFY_FLASH_SWAP_APP_ARGUMENT, 14 | TESTNET_VALIDATOR_APP_ID_V2, 15 | ) 16 | from tinyman.v2.contracts import get_pool_logicsig 17 | from tinyman.v2.flash_swap import prepare_flash_swap_transactions 18 | from tinyman.v2.pools import Pool 19 | 20 | 21 | class FlashSwapTestCase(BaseTestCase): 22 | @classmethod 23 | def setUpClass(cls): 24 | cls.VALIDATOR_APP_ID = TESTNET_VALIDATOR_APP_ID_V2 25 | cls.sender_private_key, cls.user_address = generate_account() 26 | cls.asset_1_id = 10 27 | cls.asset_2_id = 8 28 | cls.pool_token_asset_id = 15 29 | cls.pool_address = get_pool_logicsig( 30 | cls.VALIDATOR_APP_ID, cls.asset_1_id, cls.asset_2_id 31 | ).address() 32 | cls.application_address = get_application_address(cls.VALIDATOR_APP_ID) 33 | cls.pool_state = cls.get_pool_state( 34 | asset_1_reserves=10_000_000, 35 | asset_2_reserves=1_000_000_000, 36 | issued_pool_tokens=100_000_000, 37 | ) 38 | cls.pool = Pool.from_state( 39 | address=cls.pool_address, 40 | state=cls.pool_state, 41 | round_number=100, 42 | client=cls.get_tinyman_client(), 43 | ) 44 | 45 | def test_flash_swap(self): 46 | index_diff = 1 47 | txn_group = prepare_flash_swap_transactions( 48 | validator_app_id=self.VALIDATOR_APP_ID, 49 | asset_1_id=self.pool.asset_1.id, 50 | asset_2_id=self.pool.asset_2.id, 51 | asset_1_loan_amount=1_000_000, 52 | asset_2_loan_amount=100_000_000, 53 | transactions=[], 54 | sender=self.user_address, 55 | suggested_params=self.get_suggested_params(), 56 | app_call_note=self.app_call_note().decode(), 57 | ) 58 | 59 | transactions = txn_group.transactions 60 | self.assertEqual(len(transactions), 2) 61 | self.assertDictEqual( 62 | dict(transactions[0].dictify()), 63 | { 64 | "apaa": [ 65 | FLASH_SWAP_APP_ARGUMENT, 66 | int_to_bytes(index_diff), 67 | int_to_bytes(1_000_000), 68 | int_to_bytes(100_000_000), 69 | ], 70 | "apan": OnComplete.NoOpOC, 71 | "apas": [self.pool.asset_1.id, self.pool.asset_2.id], 72 | "apat": [decode_address(self.pool.address)], 73 | "apid": self.VALIDATOR_APP_ID, 74 | "fee": 3000, 75 | "fv": ANY, 76 | "gh": ANY, 77 | "grp": ANY, 78 | "lv": ANY, 79 | "snd": decode_address(self.user_address), 80 | "type": APPCALL_TXN, 81 | "note": self.app_call_note(), 82 | }, 83 | ) 84 | 85 | # Verify 86 | self.assertDictEqual( 87 | dict(transactions[1].dictify()), 88 | { 89 | "apaa": [ 90 | VERIFY_FLASH_SWAP_APP_ARGUMENT, 91 | int_to_bytes(index_diff), 92 | ], 93 | "apan": OnComplete.NoOpOC, 94 | "apas": [self.pool.asset_1.id, self.pool.asset_2.id], 95 | "apat": [decode_address(self.pool.address)], 96 | "apid": self.VALIDATOR_APP_ID, 97 | "fee": 1000, 98 | "fv": ANY, 99 | "gh": ANY, 100 | "grp": ANY, 101 | "lv": ANY, 102 | "snd": decode_address(self.user_address), 103 | "type": APPCALL_TXN, 104 | "note": self.app_call_note(), 105 | }, 106 | ) 107 | -------------------------------------------------------------------------------- /tinyman/governance/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pickle 3 | from base64 import b64decode 4 | from hashlib import sha256 5 | from typing import Optional 6 | 7 | from algosdk.error import AlgodHTTPError 8 | from multiformats import CID 9 | 10 | from tinyman.constants import MINIMUM_BALANCE_REQUIREMENT_PER_BOX, MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE 11 | 12 | 13 | def get_raw_box_value( 14 | algod, 15 | app_id: int, 16 | box_name: bytes, 17 | cache: bool = False 18 | ) -> Optional[bytes]: 19 | cache_filename = f"tinyman-governance-box-cache-{app_id}" 20 | 21 | cache_data = {} 22 | if cache: 23 | try: 24 | with open(cache_filename, 'rb') as cache_file: 25 | cache_data = pickle.load(cache_file) 26 | except FileNotFoundError: 27 | pass 28 | 29 | if box_name in cache_data: 30 | raw_box = cache_data[box_name] 31 | else: 32 | try: 33 | response = algod.application_box_by_name(app_id, box_name) 34 | except AlgodHTTPError as e: 35 | if str(e) != 'box not found': 36 | raise e 37 | return None 38 | 39 | value = response["value"] 40 | raw_box = b64decode(value) 41 | 42 | if cache: 43 | cache_data[box_name] = raw_box 44 | with open(cache_filename, 'wb') as cache_file: 45 | pickle.dump(cache_data, cache_file) 46 | return raw_box 47 | 48 | 49 | def get_all_box_names(algod, app_id: int) -> list[bytes]: 50 | response = algod.application_boxes(app_id, limit=0) 51 | box_names = [b64decode(box["name"]) for box in response["boxes"]] 52 | return box_names 53 | 54 | 55 | def box_exists(algod, app_id: int, box_name: bytes) -> bool: 56 | return get_raw_box_value(algod, app_id, box_name) is not None 57 | 58 | 59 | def parse_global_state_from_application_info(application_info: dict) -> dict: 60 | raw_global_state = application_info["params"]["global-state"] 61 | 62 | global_state = {} 63 | for pair in raw_global_state: 64 | key = b64decode(pair["key"]).decode() 65 | if pair["value"]["type"] == 1: 66 | value = b64decode(pair["value"].get("bytes", "")) 67 | else: 68 | value = pair["value"].get("uint", 0) 69 | global_state[key] = value 70 | 71 | return global_state 72 | 73 | 74 | def get_global_state(algod, app_id: int) -> dict: 75 | application_info = algod.application_info(app_id) 76 | global_state = parse_global_state_from_application_info(application_info) 77 | return global_state 78 | 79 | 80 | def check_nth_bit_from_left(value_bytes: bytes, n: int) -> int: 81 | # ensure n is within the range of the bytes 82 | if n >= len(value_bytes) * 8: 83 | raise ValueError(f"n should be less than {len(value_bytes) * 8}") 84 | 85 | # convert bytes to int 86 | num = int.from_bytes(value_bytes, 'big') 87 | 88 | # calculate which bit to check from the left 89 | bit_to_check = (len(value_bytes) * 8 - 1) - n 90 | 91 | # create a number with nth bit set 92 | nth_bit = 1 << bit_to_check 93 | 94 | # if the nth bit is set in the given number, return 1. Otherwise, return 0 95 | if num & nth_bit: 96 | return 1 97 | else: 98 | return 0 99 | 100 | 101 | def get_required_minimum_balance_of_box(box_name: bytes, box_size: int): 102 | return MINIMUM_BALANCE_REQUIREMENT_PER_BOX + MINIMUM_BALANCE_REQUIREMENT_PER_BOX_BYTE * (len(box_name) + box_size) 103 | 104 | 105 | def serialize_metadata(metadata: dict) -> str: 106 | serialized_metadata = json.dumps(metadata, sort_keys=True, separators=(",", ":"), ensure_ascii=False) 107 | return serialized_metadata 108 | 109 | 110 | def generate_cid_from_serialized_metadata(serialized_metadata: str) -> str: 111 | digest = sha256(serialized_metadata.encode('utf-8')).digest() 112 | cid = CID("base32", 1, "raw", ("sha2-256", digest)) 113 | return str(cid) 114 | 115 | 116 | def generate_cid_from_proposal_metadata(metadata: dict) -> str: 117 | serialized_metadata = serialize_metadata(metadata) 118 | return generate_cid_from_serialized_metadata(serialized_metadata) 119 | -------------------------------------------------------------------------------- /examples/v1/swapping1_less_convenience.py: -------------------------------------------------------------------------------- 1 | # This sample is provided for demonstration purposes only. 2 | # It is not intended for production use. 3 | # This example does not constitute trading advice. 4 | 5 | 6 | # This example has exactly the same functionality as swapping1.py but is purposely more verbose, using less convenience functions. 7 | # It is intended to give an understanding of what happens under those convenience functions. 8 | 9 | from tinyman.v1.pools import Pool 10 | from tinyman.assets import Asset 11 | from tinyman.compat import wait_for_confirmation 12 | from algosdk.v2client.algod import AlgodClient 13 | from tinyman.v1.client import TinymanClient 14 | 15 | 16 | # Hardcoding account keys is not a great practice. This is for demonstration purposes only. 17 | # See the README & Docs for alternative signing methods. 18 | account = { 19 | "address": "ALGORAND_ADDRESS_HERE", 20 | "private_key": "base64_private_key_here", # Use algosdk.mnemonic.to_private_key(mnemonic) if necessary 21 | } 22 | 23 | 24 | algod = AlgodClient( 25 | "", "http://localhost:8080", headers={"User-Agent": "algosdk"} 26 | ) 27 | 28 | client = TinymanClient( 29 | algod_client=algod, 30 | validator_app_id=21580889, 31 | ) 32 | 33 | 34 | # Check if the account is opted into Tinyman and optin if necessary 35 | if not client.is_opted_in(account["address"]): 36 | print("Account not opted into app, opting in now..") 37 | transaction_group = client.prepare_app_optin_transactions(account["address"]) 38 | for i, txn in enumerate(transaction_group.transactions): 39 | if txn.sender == account["address"]: 40 | transaction_group.signed_transactions[i] = txn.sign(account["private_key"]) 41 | txid = client.algod.send_transactions(transaction_group.signed_transactions) 42 | wait_for_confirmation(algod, txid) 43 | 44 | 45 | # Fetch our two assets of interest 46 | TINYUSDC = Asset(id=21582668, name="TinyUSDC", unit_name="TINYUSDC", decimals=6) 47 | ALGO = Asset(id=0, name="Algo", unit_name="ALGO", decimals=6) 48 | 49 | # Create the pool we will work with and fetch its on-chain state 50 | pool = Pool(client, asset_a=TINYUSDC, asset_b=ALGO, fetch=True) 51 | 52 | 53 | # Get a quote for a swap of 1 ALGO to TINYUSDC with 1% slippage tolerance 54 | quote = pool.fetch_fixed_input_swap_quote(ALGO(1_000_000), slippage=0.01) 55 | print(quote) 56 | print(f"TINYUSDC per ALGO: {quote.price}") 57 | print(f"TINYUSDC per ALGO (worst case): {quote.price_with_slippage}") 58 | 59 | # We only want to sell if ALGO is > 180 TINYUSDC (It's testnet!) 60 | if quote.price_with_slippage > 180: 61 | print(f"Swapping {quote.amount_in} to {quote.amount_out_with_slippage}") 62 | # Prepare a transaction group 63 | transaction_group = pool.prepare_swap_transactions( 64 | amount_in=quote.amount_in, 65 | amount_out=quote.amount_out_with_slippage, 66 | swap_type="fixed-input", 67 | swapper_address=account["address"], 68 | ) 69 | # Sign the group with our key 70 | for i, txn in enumerate(transaction_group.transactions): 71 | if txn.sender == account["address"]: 72 | transaction_group.signed_transactions[i] = txn.sign(account["private_key"]) 73 | txid = algod.send_transactions(transaction_group.signed_transactions) 74 | wait_for_confirmation(algod, txid) 75 | 76 | # Check if any excess remaining after the swap 77 | excess = pool.fetch_excess_amounts(account["address"]) 78 | if TINYUSDC.id in excess: 79 | amount = excess[TINYUSDC.id] 80 | print(f"Excess: {amount}") 81 | # We might just let the excess accumulate rather than redeeming if its < 1 TinyUSDC 82 | if amount > 1_000_000: 83 | transaction_group = pool.prepare_redeem_transactions( 84 | amount, account["address"] 85 | ) 86 | # Sign the group with our key 87 | for i, txn in enumerate(transaction_group.transactions): 88 | if txn.sender == account["address"]: 89 | transaction_group.signed_transactions[i] = txn.sign( 90 | account["private_key"] 91 | ) 92 | txid = algod.send_transactions(transaction_group.signed_transactions) 93 | wait_for_confirmation(algod, txid) 94 | --------------------------------------------------------------------------------