We setup Python virtual environment. The following commands will install and activate the virtual environment and then install dependencies.
pip install virtualenv
virtualenv venv
source venv/bin/activate
pip install pyteal
deactivate
to deactivate the virtual environment.
source venv/bin/activate
to activate virtualenv once again. Replace bin with Scripts on Windows.
PyTeal contracts are written in Python using any editor of your choice. compileProgram
method produces the TEAL code which is then compiled into bytecode and deployed to the blockchain.
The PyTeal smart contract consists of two programs. These are called the approval program and the clear programs. In PyTeal both of these programs are generally created in the same Python file.
The approval_program
is responsible for processing all application calls to the contract, with the exception of the clear call. The program is responsible for implementing most of the logic of an application.
The clear_program
is used to handle accounts using the clear call to remove the smart contract from their balance record.
To write PyTeal code we create a new directory and amm.py config.py helpers.py __init__.py
files:
mkdir contracts
cd contracts
touch amm.py config.py helpers.py __init__.py
In amm.py
we keep the high-level logic of the contract, helpers.py
contains lower level methods and config.py
keeps track of global variable and key configuration variables.
In config.py
we contain the global variables, except for TOKEN_DEFAULT_AMOUNT
, to congifure the smart contract.
from pyteal import Bytes, Int
CREATOR_KEY = Bytes("creator_key")
RESULT = Bytes("result")
TOKEN_FUNDING_KEY = Bytes("token_funding_key")
TOKEN_FUNDING_RESERVES = Bytes("token_funding_reserves")
POOL_FUNDING_RESERVES = Bytes("pool_funding_reserves")
POOL_TOKEN_KEY = Bytes("pool_token_key")
POOL_TOKENS_OUTSTANDING_KEY = Bytes("pool_tokens_outstanding_key")
YES_TOKEN_KEY = Bytes("yes_token_key")
YES_TOKENS_OUTSTANDING_KEY = Bytes("yes_tokens_outstanding_key")
YES_TOKENS_RESERVES = Bytes("yes_tokens_reserves")
NO_TOKEN_KEY = Bytes("no_token_key")
NO_TOKENS_OUTSTANDING_KEY = Bytes("no_tokens_outstanding_key")
NO_TOKENS_RESERVES = Bytes("no_tokens_reserves")
MIN_INCREMENT_KEY = Bytes("min_increment_key")
TOKEN_DEFAULT_AMOUNT = Int(10 ** 13)
These variables can be imported as:
from contracts.config import (
CREATOR_KEY, TOKEN_FUNDING_KEY,
POOL_TOKEN_KEY, MIN_INCREMENT_KEY,
POOL_TOKENS_OUTSTANDING_KEY, TOKEN_DEFAULT_AMOUNT,
YES_TOKEN_KEY, NO_TOKEN_KEY,
TOKEN_FUNDING_RESERVES, POOL_FUNDING_RESERVES,
RESULT
)
on_call_method = Txn.application_args[0]
on_call = Cond(
[on_call_method == Bytes("setup"), on_setup],
[on_call_method == Bytes("supply"), on_supply],
[on_call_method == Bytes("withdraw"), on_withdraw],
[on_call_method == Bytes("redeem"), on_redemption],
[on_call_method == Bytes("result"), on_result],
[on_call_method == Bytes("swap"), on_swap],
)
program = Cond(
[Txn.application_id() == Int(0), on_create],
[Txn.on_completion() == OnComplete.NoOp, on_call],
[Txn.on_completion() == OnComplete.DeleteApplication, on_delete],
[
Or(
Txn.on_completion() == OnComplete.OptIn,
Txn.on_completion() == OnComplete.CloseOut,
Txn.on_completion() == OnComplete.UpdateApplication,
),
Reject(),
],
)
This statement is the heart of the smart contract. Based on how the contract is called, it chooses which operation to run. For example, if Txn.application_id()
is 0, then the code from on_create
runs. If Txn.on_completion()
is OnComplete.NoOp
, then on_call
runs. If Txn.application_args[0]
is "setup", then on_setup
runs. If either Txn.on_completion()
are OnComplete.OptIn
,OnComplete.CloseOut
,OnComplete.UpdateApplication
or none of the described cases are true, the program will exit an with error. Let’s look at each of these cases below.
on_create = Seq(
App.globalPut(CREATOR_KEY, Txn.application_args[0]),
App.globalPut(TOKEN_FUNDING_KEY, Btoi(Txn.application_args[1])),
App.globalPut(MIN_INCREMENT_KEY, Btoi(Txn.application_args[2])),
App.globalPut(TOKEN_FUNDING_RESERVES, Int(0)),
App.globalPut(POOL_FUNDING_RESERVES, Int(0)),
App.globalPut(RESULT, Int(0)),
Approve(),
)
This part of the program is responsible for setting up the initial state of the smart contract. It writes the keys to its global state.
The values of CREATOR_KEY
,TOKEN_FUNDING_KEY
and MIN_INCREMENT_KEY
keys are determined by the application call arguments from the Txn.application_args
list. Meanhwile, TOKEN_FUNDING_RESERVES
, POOL_FUNDING_RESERVES
and RESULT
are initialised as 0 integer values.
def get_setup():
pool_token_id = App.globalGetEx(Global.current_application_id(), POOL_TOKEN_KEY)
pool_tokens_outstanding = App.globalGetEx(
Global.current_application_id(), POOL_TOKENS_OUTSTANDING_KEY
)
on_setup = Seq(
pool_token_id,
pool_tokens_outstanding,
Assert(Not(pool_token_id.hasValue())),
Assert(Not(pool_tokens_outstanding.hasValue())),
createPoolToken(TOKEN_DEFAULT_AMOUNT),
optIn(TOKEN_FUNDING_KEY),
createNoToken(TOKEN_DEFAULT_AMOUNT),
optIn(NO_TOKEN_KEY),
createYesToken(TOKEN_DEFAULT_AMOUNT),
optIn(YES_TOKEN_KEY),
Approve(),
)
return on_setup
This code runs when an account calls Txn.application_args[0]
that equals to Bytes("setup")
into the smart contract. It returns true if the current pool token has not been created and there are no outstanding tokens, meaning that the smart contract can only be set up once.
The methods create pool token, yes and no tokens, which the contract opts in the application.
def createPoolToken(pool_token_amount: TealType.uint64) -> Expr:
return Seq(
InnerTxnBuilder.Begin(),
InnerTxnBuilder.SetFields(
{
TxnField.type_enum: TxnType.AssetConfig,
TxnField.config_asset_total: pool_token_amount,
TxnField.config_asset_name: Bytes("PoolToken"),
TxnField.config_asset_default_frozen: Int(0),
TxnField.config_asset_decimals: Int(0),
TxnField.config_asset_reserve: Global.current_application_address(),
}
),
InnerTxnBuilder.Submit(),
App.globalPut(POOL_TOKEN_KEY, InnerTxn.created_asset_id()),
App.globalPut(POOL_TOKENS_OUTSTANDING_KEY, Int(0)),
)
def optIn(token_key: TealType.bytes) -> Expr:
return sendToken(token_key, Global.current_application_address(), Int(0))
def sendToken(
token_key: TealType.bytes, receiver: TealType.bytes, amount: TealType.uint64
) -> Expr:
return Seq(
InnerTxnBuilder.Begin(),
InnerTxnBuilder.SetFields(
{
TxnField.type_enum: TxnType.AssetTransfer,
TxnField.xfer_asset: App.globalGet(token_key),
TxnField.asset_receiver: receiver,
TxnField.asset_amount: amount,
}
),
InnerTxnBuilder.Submit(),
)
def createNoToken(token_amount: TealType.uint64) -> Expr:
return Seq(
InnerTxnBuilder.Begin(),
InnerTxnBuilder.SetFields(
{
TxnField.type_enum: TxnType.AssetConfig,
TxnField.config_asset_total: token_amount,
TxnField.config_asset_name: Bytes("NoToken"),
TxnField.config_asset_unit_name: Bytes("No"),
TxnField.config_asset_default_frozen: Int(0),
TxnField.config_asset_decimals: Int(0),
TxnField.config_asset_reserve: Global.current_application_address(),
}
),
InnerTxnBuilder.Submit(),
App.globalPut(NO_TOKEN_KEY, InnerTxn.created_asset_id()),
App.globalPut(NO_TOKENS_OUTSTANDING_KEY, Int(0)),
App.globalPut(NO_TOKENS_RESERVES, Int(0)),
)
def createYesToken(token_amount: TealType.uint64) -> Expr:
return Seq(
InnerTxnBuilder.Begin(),
InnerTxnBuilder.SetFields(
{
TxnField.type_enum: TxnType.AssetConfig,
TxnField.config_asset_total: token_amount,
TxnField.config_asset_name: Bytes("YesToken"),
TxnField.config_asset_unit_name: Bytes("Yes"),
TxnField.config_asset_default_frozen: Int(0),
TxnField.config_asset_decimals: Int(0),
TxnField.config_asset_reserve: Global.current_application_address(),
}
),
InnerTxnBuilder.Submit(),
App.globalPut(YES_TOKEN_KEY, InnerTxn.created_asset_id()),
App.globalPut(YES_TOKENS_OUTSTANDING_KEY, Int(0)),
App.globalPut(YES_TOKENS_RESERVES, Int(0)),
)
def get_supply():
token_txn_index = Txn.group_index() - Int(1)
on_supply = Seq(
Assert(
And(
validateTokenReceived(token_txn_index, TOKEN_FUNDING_KEY),
Gtxn[token_txn_index].asset_amount()
>= App.globalGet(MIN_INCREMENT_KEY),
)
),
mintAndSendPoolToken(
Txn.sender(),
Gtxn[token_txn_index].asset_amount(),
),
Approve(),
)
return on_supply
This code runs when an account calls Txn.application_args[0]
that equals to Bytes("supply")
into the smart contract. It returns true if the token received by the smart contract is the same funding (reserve) token declared in the on_create
part of the program and is larger in quantity than the minimum value.
validateTokenReceived
validates the transfer of the tokens.
mintAndSendPoolToken
keeps track of the pool funding reserves and disburses the pool token based on the proportion of the provided to the exisitng funds.
def validateTokenReceived(
transaction_index: TealType.uint64, token_key: TealType.bytes
) -> Expr:
return And(
Gtxn[transaction_index].type_enum() == TxnType.AssetTransfer,
Gtxn[transaction_index].sender() == Txn.sender(),
Gtxn[transaction_index].asset_receiver()
== Global.current_application_address(),
Gtxn[transaction_index].xfer_asset() == App.globalGet(token_key),
Gtxn[transaction_index].asset_amount() > Int(0),
)
def mintAndSendPoolToken(receiver: TealType.bytes, amount: TealType.uint64) -> Expr:
ratio: ScratchVar = ScratchVar(TealType.uint64)
return Seq(
ratio.store(
(Int(1) + App.globalGet(NO_TOKENS_RESERVES)) / (Int(1) + App.globalGet(YES_TOKENS_RESERVES))
),
If(App.globalGet(POOL_FUNDING_RESERVES) > Int(0))
.Then(
Seq(
sendToken(POOL_TOKEN_KEY, receiver, amount * App.globalGet(POOL_TOKENS_OUTSTANDING_KEY) / App.globalGet(POOL_FUNDING_RESERVES) ),
App.globalPut(
POOL_TOKENS_OUTSTANDING_KEY,
App.globalGet(POOL_TOKENS_OUTSTANDING_KEY) + amount * App.globalGet(POOL_TOKENS_OUTSTANDING_KEY) / App.globalGet(POOL_FUNDING_RESERVES)
))
)
.ElseIf(App.globalGet(POOL_FUNDING_RESERVES) == Int(0))
.Then(
Seq(
sendToken(POOL_TOKEN_KEY, receiver, amount ),
App.globalPut(
POOL_TOKENS_OUTSTANDING_KEY,
App.globalGet(POOL_TOKENS_OUTSTANDING_KEY) + amount,
),
)
),
App.globalPut(NO_TOKENS_RESERVES, ratio.load() * (amount / Int(4)) + App.globalGet(NO_TOKENS_RESERVES) ),
App.globalPut(YES_TOKENS_RESERVES, ( Int(1) / ratio.load() )* (amount / Int(4)) + App.globalGet(YES_TOKENS_RESERVES) ),
App.globalPut(
POOL_FUNDING_RESERVES, App.globalGet(POOL_FUNDING_RESERVES) + amount
),
)
def get_withdraw():
pool_token_txn_index = Txn.group_index() - Int(1)
on_withdraw = Seq(
Assert(
validateTokenReceived(pool_token_txn_index, POOL_TOKEN_KEY)
),
withdrawLPToken(
Txn.sender(),
Gtxn[pool_token_txn_index].asset_amount(),
),
Approve(),
)
return on_withdraw
The code runs when an account calls Txn.application_args[0]
that equals to Bytes("withdraw")
into the smart contract. It returns true if the token received by the smart contract is the same pool token created in the on_create
part of the program.
withdrawLPToken
sends the proportional amount of funding (reserve) token to outstanding pool token, supplied pool tokens and funding reserves.
def withdrawLPToken(
receiver: TealType.bytes,
pool_token_amount: TealType.uint64,
) -> Expr:
ratio: ScratchVar = ScratchVar(TealType.uint64)
return Seq(
sendToken(
TOKEN_FUNDING_KEY,
receiver,
App.globalGet(POOL_FUNDING_RESERVES) * pool_token_amount / App.globalGet(POOL_TOKENS_OUTSTANDING_KEY),
),
App.globalPut(
POOL_FUNDING_RESERVES, App.globalGet(POOL_FUNDING_RESERVES) - (App.globalGet(POOL_FUNDING_RESERVES) * pool_token_amount / App.globalGet(POOL_TOKENS_OUTSTANDING_KEY)),
),
App.globalPut(
POOL_TOKENS_OUTSTANDING_KEY,
App.globalGet(POOL_TOKENS_OUTSTANDING_KEY) - pool_token_amount,
),
If(App.globalGet(RESULT)==Int(0)).Then(
Seq(
ratio.store(
(Int(1) + App.globalGet(NO_TOKENS_RESERVES)) / (Int(1) + App.globalGet(YES_TOKENS_RESERVES))
),
App.globalPut(NO_TOKENS_RESERVES, App.globalGet(NO_TOKENS_RESERVES) - (App.globalGet(POOL_FUNDING_RESERVES) * pool_token_amount / App.globalGet(POOL_TOKENS_OUTSTANDING_KEY)) / Int(4) * ratio.load() ),
App.globalPut(YES_TOKENS_RESERVES, App.globalGet(YES_TOKENS_RESERVES) - (App.globalGet(POOL_FUNDING_RESERVES) * pool_token_amount / App.globalGet(POOL_TOKENS_OUTSTANDING_KEY)) / Int(4) * (Int(1)/ratio.load()) ),
)),
)
def get_swap():
token_txn_index = Txn.group_index() - Int(1)
option = Txn.application_args[1]
on_swap = Seq(
Assert(
validateTokenReceived(token_txn_index, TOKEN_FUNDING_KEY),
),
If(option == Bytes("buy_yes"))
.Then(
Seq(
mintAndSendYesToken(
Txn.sender(),
Gtxn[token_txn_index].asset_amount(),
),
Approve()
),
)
.ElseIf(option == Bytes("buy_no"))
.Then(
Seq(
mintAndSendNoToken(
Txn.sender(),
Gtxn[token_txn_index].asset_amount(),
),
Approve()
),
),
Reject()
)
return on_swap
The code runs when an account calls Txn.application_args[0]
that equals to Bytes("swap")
into the smart contract. It returns true if the token received by the smart contract is the same funding (reserve) token declared in on_create
part of the program.
Depending on the argument in Txn.application_args[1]
, the user either can choose a yes or no option token. The price of each token is explicitly determined by the existing liquidity, reserves of yes and no tokens and the size of the trade.
def mintAndSendNoToken(
receiver: TealType.bytes, amount: TealType.uint64
) -> Expr:
funding = AssetHolding.balance(
Global.current_application_address(), App.globalGet(TOKEN_FUNDING_KEY)
)
tokensOut: ScratchVar = ScratchVar(TealType.uint64)
return Seq(
tokensOut.store(
App.globalGet(YES_TOKENS_RESERVES) * amount / (App.globalGet(NO_TOKENS_RESERVES) + amount )
),
App.globalPut(NO_TOKENS_OUTSTANDING_KEY, App.globalGet(NO_TOKENS_OUTSTANDING_KEY) + tokensOut.load()),
App.globalPut(NO_TOKENS_RESERVES, App.globalGet(NO_TOKENS_RESERVES) - tokensOut.load()),
sendToken(NO_TOKEN_KEY, receiver, tokensOut.load()),
If(App.globalGet(NO_TOKENS_OUTSTANDING_KEY) > App.globalGet(YES_TOKENS_OUTSTANDING_KEY))
.Then(
App.globalPut(TOKEN_FUNDING_RESERVES, App.globalGet(NO_TOKENS_OUTSTANDING_KEY) * Int(2))
),
funding,
App.globalPut(
POOL_FUNDING_RESERVES,
funding.value() - App.globalGet(TOKEN_FUNDING_RESERVES)
),
)
def mintAndSendYesToken(
receiver: TealType.bytes, amount: TealType.uint64
) -> Expr:
funding = AssetHolding.balance(
Global.current_application_address(), App.globalGet(TOKEN_FUNDING_KEY)
)
tokensOut: ScratchVar = ScratchVar(TealType.uint64)
return Seq(
tokensOut.store(
(App.globalGet(NO_TOKENS_RESERVES) * amount / (App.globalGet(YES_TOKENS_RESERVES) + amount ))
),
App.globalPut(YES_TOKENS_OUTSTANDING_KEY, App.globalGet(YES_TOKENS_OUTSTANDING_KEY) + tokensOut.load()),
App.globalPut(YES_TOKENS_RESERVES, App.globalGet(YES_TOKENS_RESERVES) - tokensOut.load()),
sendToken(YES_TOKEN_KEY, receiver, tokensOut.load()),
If(App.globalGet(YES_TOKENS_OUTSTANDING_KEY) > App.globalGet(NO_TOKENS_OUTSTANDING_KEY))
.Then(
App.globalPut(TOKEN_FUNDING_RESERVES, App.globalGet(YES_TOKENS_OUTSTANDING_KEY) * Int(2))
),
funding,
App.globalPut(
POOL_FUNDING_RESERVES,
funding.value() - App.globalGet(TOKEN_FUNDING_RESERVES)
),
)
def get_result():
result = Txn.application_args[1]
on_result = Seq(
Assert(
Txn.sender() == App.globalGet(CREATOR_KEY)
),
If(result == Bytes("yes"))
.Then(
Seq(
App.globalPut(RESULT, App.globalGet(YES_TOKEN_KEY)),
Approve()
)
)
.ElseIf(
result == Bytes("no"),
)
.Then(
Seq(
App.globalPut(RESULT, App.globalGet(NO_TOKEN_KEY)),
Approve()
)
),
Reject(),
)
return on_result
The code runs when an account calls Txn.application_args[0]
that equals to Bytes("result")
into the smart contract. It returns true if the token received by the smart contract is the creator account declared in on_create
part of the program.
Depending on the argument in Txn.application_args[1]
, the creator should choose a yes or no result of the event to declare the winning option.
def get_redemption():
token_txn_index = Txn.group_index() - Int(1)
on_redemption = Seq(
Assert(
And(
validateTokenReceived(token_txn_index, RESULT),
)
),
redeemToken(
Txn.sender(),
Gtxn[token_txn_index].asset_amount(),
),
Approve(),
)
return on_redemption
The code runs when an account calls Txn.application_args[0]
that equals to Bytes("redeem")
into the smart contract. It returns true if the token received by the smart contract is the winner decided in on_result
part of the program.
redeemToken
tracks the withdrawal of Yes/No Tokens. Since the initial distribution of liquidity provided was distribtued in 50%/50% towards each token, after the deadline the winning token should be worth double its initial price.
def redeemToken(
receiver: TealType.bytes,
result_token_amount: TealType.uint64,
) -> Expr:
return Seq(
sendToken(
TOKEN_FUNDING_KEY,
receiver,
result_token_amount * Int(2)
),
App.globalPut(
TOKEN_FUNDING_RESERVES, App.globalGet(TOKEN_FUNDING_RESERVES) - result_token_amount * Int(2)
),
If( App.globalGet(RESULT) == App.globalGet(YES_TOKEN_KEY) )
.Then(
App.globalPut(
YES_TOKENS_OUTSTANDING_KEY, App.globalGet(YES_TOKENS_OUTSTANDING_KEY) - result_token_amount
),
)
.ElseIf(App.globalGet(RESULT) == App.globalGet(NO_TOKEN_KEY) )
.Then(
App.globalPut(
NO_TOKENS_OUTSTANDING_KEY, App.globalGet(NO_TOKENS_OUTSTANDING_KEY) - result_token_amount
),
),
)
To deploy a smart contract we create an account and fund it using Testnet Dispensary.
from algosdk import account
# generate an account
private_key, address = account.generate_account()
print("Private key:", private_key)
print("Address:", address)
We register on the purestake.com to obtain the api key and initialize the Algorand Client.
algod_token = "Enter Yout token here"
algod_address = "https://testnet-algorand.api.purestake.io/ps2"
headers = {
"X-API-Key": algod_token,
}
# initialize an algodClient
client = algod.AlgodClient(algod_token, algod_address, headers)
We create an asset that will be used for reserves in the contract and return the asset index.
from algosdk import account
from algosdk.future import transaction
def wait_for_confirmation(client, txid):
last_round = client.status().get("last-round")
txinfo = client.pending_transaction_info(txid)
while not (txinfo.get("confirmed-round") and txinfo.get("confirmed-round") > 0):
print("Waiting for confirmation...")
last_round += 1
client.status_after_block(last_round)
txinfo = client.pending_transaction_info(txid)
print(
"Transaction {} confirmed in round {}.".format(
txid, txinfo.get("confirmed-round")
)
)
return txinfo
def create_asset(client, private_key):
# declare sender
sender = account.address_from_private_key(private_key)
params = client.suggested_params()
txn = transaction.AssetConfigTxn(
sender=sender,
sp=params,
total=1_000_000_000,
default_frozen=False,
unit_name="Copio",
asset_name="coin",
manager=sender,
reserve=sender,
freeze=sender,
clawback=sender,
strict_empty_address_check=False,
url=None,
decimals=0)
# Sign with secret key of creator
stxn = txn.sign(private_key)
# Send the transaction to the network and retrieve the txid.
txid = client.send_transaction(stxn)
print("Signed transaction with txID: {}".format(txid))
# Wait for the transaction to be confirmed
response = wait_for_confirmation(client, txid)
print("TXID: ", txid)
print("Result confirmed in round: {}".format(response['confirmed-round']))
return response['asset-index']
First, to interact with the smart contract we need to compile and deploy it. createAmmApp
function requires client, reserve token id, minimum increment of the reserve token, creator address and private_key to sign the transaction.
def waitForTransaction(
client: AlgodClient, txID: str, timeout: int = 10
):
lastStatus = client.status()
lastRound = lastStatus["last-round"]
startRound = lastRound
while lastRound < startRound + timeout:
pending_txn = client.pending_transaction_info(txID)
if pending_txn.get("confirmed-round", 0) > 0:
return pending_txn
if pending_txn["pool-error"]:
raise Exception("Pool error: {}".format(pending_txn["pool-error"]))
lastStatus = client.status_after_block(lastRound + 1)
lastRound += 1
raise Exception(
"Transaction {} not confirmed after {} rounds".format(txID, timeout)
)
def fullyCompileContract(
client: AlgodClient, contract: Expr
) -> bytes:
teal = compileTeal(contract, mode=Mode.Application, version=6)
response = client.compile(teal)
return b64decode(response["result"])
def getContracts(client: AlgodClient) -> Tuple[bytes, bytes]:
"""Get the compiled TEAL contracts for the amm.
Args:q
client: An algod client that has the ability to compile TEAL programs.
Returns:
A tuple of 2 byte strings. The first is the approval program, and the
second is the clear state program.
"""
APPROVAL_PROGRAM = fullyCompileContract(client, approval_program())
CLEAR_STATE_PROGRAM = fullyCompileContract(client, clear_program())
return APPROVAL_PROGRAM, CLEAR_STATE_PROGRAM
def createAmmApp(
client: AlgodClient,
token: int,
minIncrement: int,
creator: str,
private_key: str
) -> int:
"""Creates a new amm.
Args:
client: An algod client.
creator: The account that will create the amm application.
token: The id of token A in the liquidity pool,
Returns:
The ID of the newly created amm app.
"""
approval, clear = getContracts(client)
globalSchema = transaction.StateSchema(num_uints=13, num_byte_slices=1)
localSchema = transaction.StateSchema(num_uints=0, num_byte_slices=0)
app_args = [
encoding.decode_address(creator),
token.to_bytes(8, "big"),
minIncrement.to_bytes(8, "big"),
]
txn = transaction.ApplicationCreateTxn(
sender=creator,
on_complete=transaction.OnComplete.NoOpOC,
approval_program=approval,
clear_program=clear,
global_schema=globalSchema,
local_schema=localSchema,
app_args=app_args,
sp=client.suggested_params(),
)
signedTxn = txn.sign(private_key)
client.send_transaction(signedTxn)
response = waitForTransaction(client, signedTxn.get_txid())
assert response["application-index"] is not None and response["application-index"] > 0
return response["application-index"]
MIN_BALANCE_REQUIREMENT = (
# min account balance
110_000
# additional min balance for 4 assets
+ 100_000 * 4
)
def setupAmmApp(
client: AlgodClient,
appID: int,
token: int,
funder: str,
private_key: str
) -> int:
"""Finish setting up an amm.
This operation funds the pool account, creates pool token,
and opts app into tokens A and B, all in one atomic transaction group.
Args:
client: An algod client.
appID: The app ID of the amm.
funder: The account providing the funding for the escrow account.
token: Token id.
private_key
Return: pool token id
"""
appAddr = get_application_address(appID)
suggestedParams = client.suggested_params()
fundAppTxn = transaction.PaymentTxn(
sender=funder,
receiver=appAddr,
amt=MIN_BALANCE_REQUIREMENT,
sp=suggestedParams,
)
setupTxn = transaction.ApplicationCallTxn(
sender=funder,
index=appID,
on_complete=transaction.OnComplete.NoOpOC,
app_args=[b"setup"],
foreign_assets=[token],
sp=suggestedParams,
)
transaction.assign_group_id([fundAppTxn, setupTxn])
signedFundAppTxn = fundAppTxn.sign(private_key)
signedSetupTxn = setupTxn.sign(private_key)
client.send_transactions([signedFundAppTxn, signedSetupTxn])
waitForTransaction(client, signedFundAppTxn.get_txid())
glob_state = client.application_info(appID)['params']['global-state']
ids = {}
for i in range(len(glob_state)):
if b64decode(glob_state[i]['key']) == b"pool_token_key":
ids['pool_token_key'] = glob_state[i]['value']['uint']
elif b64decode(glob_state[i]['key']) == b"yes_token_key":
ids['yes_token_key'] = glob_state[i]['value']['uint']
elif b64decode(glob_state[i]['key']) == b"no_token_key":
ids['no_token_key'] = glob_state[i]['value']['uint']
return ids
setupAmmApp
method requires an appID
as one of the arguments. It funds the app and calls the smart contract with the given arguments all in one atomic transaction group. The method returns the dictionary with ids of created pool and option tokens.
Further we need to opt in to the created new assets:
def optInToPoolToken(
client: AlgodClient,
poolToken: int,
account: str,
private_key: str
) -> None:
"""Opts into Pool Token
Args:
client: An algod client.
account: The account opting into the token.
poolToken: Token id.
private_key: to sign the tx.
"""
suggestedParams = client.suggested_params()
optInTxn = transaction.AssetOptInTxn(
sender=account, index=poolToken, sp=suggestedParams
)
signedOptInTxn = optInTxn.sign(private_key)
client.send_transaction(signedOptInTxn)
waitForTransaction(client, signedOptInTxn.get_txid())
def supply(
client: AlgodClient, appID: int, q: int, supplier: str, private_key: str, \
token: int, poolToken: int, yesToken: int, noToken: int
) -> None:
"""Supply liquidity to the pool.
"""
appAddr = get_application_address(appID)
suggestedParams = client.suggested_params()
# pay for the fee incurred by AMM for sending back the pool token
feeTxn = transaction.PaymentTxn(
sender=supplier,
receiver=appAddr,
amt=MIN_BALANCE_REQUIREMENT,
sp=suggestedParams,
)
tokenTxn = transaction.AssetTransferTxn(
sender=supplier,
receiver=appAddr,
index=token,
amt=q,
sp=suggestedParams,
)
appCallTxn = transaction.ApplicationCallTxn(
sender=supplier,
index=appID,
on_complete=transaction.OnComplete.NoOpOC,
app_args=[b"supply"],
foreign_assets=[token, poolToken, yesToken, noToken],
sp=suggestedParams,
)
transaction.assign_group_id([feeTxn, tokenTxn, appCallTxn])
signedFeeTxn = feeTxn.sign(private_key)
signedTokenTxn = tokenTxn.sign(private_key)
signedAppCallTxn = appCallTxn.sign(private_key)
client.send_transactions(
[signedFeeTxn, signedTokenTxn, signedAppCallTxn]
)
waitForTransaction(client, signedAppCallTxn.get_txid())
def swap(
client: AlgodClient,
appID: int,
option: str,
q: int,
supplier: int,
private_key: str,
token: int,
poolToken: int,
yesToken: int,
noToken: int
) -> None:
if option == 'yes':
second_argument = b"buy_yes"
elif option =='no':
second_argument = b"buy_no"
else:
return
appAddr = get_application_address(appID)
suggestedParams = client.suggested_params()
# pay for the fee incurred by AMM for sending back the pool token
feeTxn = transaction.PaymentTxn(
sender=supplier,
receiver=appAddr,
amt=2_000,
sp=suggestedParams,
)
tokenTxn = transaction.AssetTransferTxn(
sender=supplier,
receiver=appAddr,
index=token,
amt=q,
sp=suggestedParams,
)
appCallTxn = transaction.ApplicationCallTxn(
sender=supplier,
index=appID,
on_complete=transaction.OnComplete.NoOpOC,
app_args=[ b"swap", second_argument],
foreign_assets=[token, poolToken, yesToken, noToken],
sp=suggestedParams,
)
transaction.assign_group_id([feeTxn, tokenTxn, appCallTxn])
signedFeeTxn = feeTxn.sign(private_key)
signedTokenTxn = tokenTxn.sign(private_key)
signedAppCallTxn = appCallTxn.sign(private_key)
client.send_transactions(
[signedFeeTxn, signedTokenTxn, signedAppCallTxn]
)
waitForTransaction(client, signedAppCallTxn.get_txid())
def withdraw(
client: AlgodClient,
appID: int,
poolToken: int,
poolTokenAmount: int,
withdrawAccount: str,
token: int,
private_key: str
) -> None:
"""Withdraw liquidity + rewards from the pool back to supplier.
Supplier should receive stablecoin + fees proportional to the liquidity share in the pool they choose to withdraw.
"""
appAddr = get_application_address(appID)
suggestedParams = client.suggested_params()
# pay for the fee incurred by AMM for sending back tokens A and B
feeTxn = transaction.PaymentTxn(
sender=withdrawAccount,
receiver=appAddr,
amt=2_000,
sp=suggestedParams,
)
poolTokenTxn = transaction.AssetTransferTxn(
sender=withdrawAccount,
receiver=appAddr,
index=poolToken,
amt=poolTokenAmount,
sp=suggestedParams,
)
appCallTxn = transaction.ApplicationCallTxn(
sender=withdrawAccount,
index=appID,
on_complete=transaction.OnComplete.NoOpOC,
app_args=[b"withdraw"],
foreign_assets=[token, poolToken],
sp=suggestedParams,
)
transaction.assign_group_id([feeTxn, poolTokenTxn, appCallTxn])
signedFeeTxn = feeTxn.sign(private_key)
signedPoolTokenTxn = poolTokenTxn.sign(private_key)
signedAppCallTxn = appCallTxn.sign(private_key)
client.send_transactions([signedFeeTxn, signedPoolTokenTxn, signedAppCallTxn])
waitForTransaction(client, signedAppCallTxn.get_txid())
def set_result(
client: AlgodClient,
appID: int,
funder: str,
private_key: str,
second_argument
):
""" Set the winning token key
"""
appAddr = get_application_address(appID)
suggestedParams = client.suggested_params()
feeTxn = transaction.PaymentTxn(
sender=funder,
receiver=appAddr,
amt=2_000,
sp=suggestedParams,
)
callTxn = transaction.ApplicationCallTxn(
sender=funder,
index=appID,
on_complete=transaction.OnComplete.NoOpOC,
app_args=[b"result", second_argument],
sp=suggestedParams,
)
transaction.assign_group_id([feeTxn, callTxn])
signedFeeTxn = feeTxn.sign(private_key)
signedAppCallTxn = callTxn.sign(private_key)
client.send_transactions([signedFeeTxn, signedAppCallTxn])
waitForTransaction(client, signedAppCallTxn.get_txid())
def redeem(
client: AlgodClient, appID: int, Token:int, TokenAmount: int,
withdrawAccount: str, token: int, private_key: str
) -> None:
appAddr = get_application_address(appID)
suggestedParams = client.suggested_params()
# pay for the fee incurred by AMM for sending back tokens A and B
feeTxn = transaction.PaymentTxn(
sender=withdrawAccount,
receiver=appAddr,
amt=2_000,
sp=suggestedParams,
)
TokenTxn = transaction.AssetTransferTxn(
sender=withdrawAccount,
receiver=appAddr,
index=Token,
amt=TokenAmount,
sp=suggestedParams,
)
appCallTxn = transaction.ApplicationCallTxn(
sender=withdrawAccount,
index=appID,
on_complete=transaction.OnComplete.NoOpOC,
app_args=[b"redeem"],
foreign_assets=[token, Token],
sp=suggestedParams,
)
transaction.assign_group_id([feeTxn, TokenTxn, appCallTxn])
signedFeeTxn = feeTxn.sign(private_key)
signedPoolTokenTxn = TokenTxn.sign(private_key)
signedAppCallTxn = appCallTxn.sign(private_key)
client.send_transactions([signedFeeTxn, signedPoolTokenTxn, signedAppCallTxn])
waitForTransaction(client, signedAppCallTxn.get_txid())
def closeAmm(
client: AlgodClient, appID: int, closer: str, private_key: str
)-> None:
"""Close an AMM.
Args:
client: An Algod client.
appID: The app ID of the amm.
closer: closer account public address. Must be the original creator of the pool.
private_key: closer account private key to sign the transactions.
"""
deleteTxn = transaction.ApplicationDeleteTxn(
sender=closer,
index=appID,
sp=client.suggested_params(),
)
signedDeleteTxn = deleteTxn.sign(private_key)
client.send_transaction(signedDeleteTxn)
waitForTransaction(client, signedDeleteTxn.get_txid())
import os
from dotenv import load_dotenv
from algosdk import account
from algosdk.v2client import algod
from create_asset import create_asset
from amm_api import createAmmApp, setupAmmApp, optInToPoolToken, \
supply, withdraw, swap, set_result, closeAmm, redeem
load_dotenv()
private_key = os.getenv('key')
creator = account.address_from_private_key(private_key)
algod_token = os.getenv('algod_token')
algod_address = "https://testnet-algorand.api.purestake.io/ps2"
headers = {
"X-API-Key": algod_token,
}
# initialize an algodClient
client = algod.AlgodClient(algod_token, algod_address, headers)
# create (stable) asset
token = create_asset(client, private_key)
appID = createAmmApp(
client=client,
creator=creator,
private_key=private_key,
token=token,
minIncrement=1000,
)
print(f"Alice is setting up and funding amm {appID}")
Tokens = setupAmmApp(
client=client,
appID=appID,
funder=creator,
private_key=private_key,
token=token,
)
poolToken = Tokens['pool_token_key']
yesToken = Tokens['yes_token_key']
noToken = Tokens['no_token_key']
print(Tokens['pool_token_key'], Tokens['yes_token_key'], Tokens['no_token_key'])
optInToPoolToken(client, creator, private_key, poolToken)
optInToPoolToken(client, creator, private_key, yesToken)
optInToPoolToken(client, creator, private_key, noToken)
print("Supplying AMM with initial token")
poolTokenFirstAmount = 500_000
supply(
client=client,
appID=appID,
q=poolTokenFirstAmount,
supplier=creator,
private_key=private_key,
token=token,
poolToken=poolToken,
yesToken=yesToken, noToken=noToken
)
print("Supplying AMM with more tokens")
poolTokenSecondAmount = 1_500_000
supply(
client=client,
appID=appID,
q=poolTokenSecondAmount,
supplier=creator,
private_key=private_key,
token=token,
poolToken=poolToken,
yesToken=yesToken, noToken=noToken
)
print("Swapping")
yesTokenAmount = 100_000
# buy yes token
swap(
client=client,
appID=appID,
option="yes",
q=yesTokenAmount,
supplier=creator,
private_key=private_key,
token=token,
poolToken=poolToken,
yesToken=yesToken,
noToken=noToken
)
#buy no token
swap(
client=client,
appID=appID,
option="no",
q=yesTokenAmount,
supplier=creator,
private_key=private_key,
token=token,
poolToken=poolToken,
yesToken=yesToken,
noToken=noToken
)
print("Withdrawing")
AllTokens = 2_000_000
withdraw(
client = client,
appID = appID,
poolTokenAmount = AllTokens,
poolToken = poolToken,
withdrawAccount = creator,
private_key = private_key,
token = token
)
print("Result")
#set winner
set_result(
client = client,
appID = appID,
second_argument=b"yes",
funder=creator,
private_key = private_key
)
# redemption for for yes/no
print("Redeeming")
YesTokensAmount = 95_238
redeem(
client = client,
appID = appID,
TokenAmount = YesTokensAmount,
Token = yesToken,
withdrawAccount = creator,
private_key = private_key,
token = token
)
# Delete
print("Deleting")
closeAmm(
client = client,
appID = appID,
closer=creator,
private_key = private_key
)
Alogrand: Smart contract details
Creating Stateful Algorand Smart Contracts in Python with PyTeal