ASA Recurring Withdrawal DApp
Overview
This solution will guide you in developing and deploying an orchestration of Algorand protocol features that addresses the following use case:
Building a decentralized application that regulates recurring withdrawals of tokenized assets.
The solution architecture relies on Algorand Standard Asset (ASA), Atomic Transfers (AT) and Stateful & Stateless Smart Contracts (ASC1), whose source code is provided both in TEAL and PyTeal. We will use goal CLI to deploy the application and interact with it manually. Note that the application’s deployment and interaction steps could be executed programmatically using any Algorand SDK instead of goal CLI, consider this as your next step exercise.
Introduction
Regulating recurring payments or withdrawals is a good use-case for a blockchain application. While an implementation of recurring payment escrow could be achieved just making use of Stateless ASC1, integrating Stateful ASC1 provides higher levels of generalization and customization for withdrawal approval schemes.
In the context of the proposed solution, the Stateful component let us model the following withdrawal approval schema:
Users can book withdrawals locking an amount of ASA in the Asset Escrow Account. Once their booked withdrawal has been processed, users can withdraw double the amount of ASA deposited in the Asset Escrow Account. The dApp prevents both withdrawals double-booking and over-booking.
Unlike purely Stateless solutions, a Stateful component lets the recurring withdrawal interval be even grater than 1000 blocks (that is the greatest validity range for any leased Algorand transaction, which limits pure Stateless verification domain).
The proposed withdrawal approval logic could fit, for example, a simple ASA distribution model. However, nothing prevents you from model more sophisticated withdrawal conditions starting from the proposed solution.
Solution Architecture
Algorand provides great protocol features directly on Layer-1, their composition turns out to be extremely powerful and expressive. Before deep diving into technical details of the implementation, is useful to sketch up the solution architecture on high level, pointing out the role that each little brick plays into the dApp.
- Algorand Standard Asset: implements any kind of token over which the dApp applies its withdrawal approval logic.
- Atomic Transfers: implements a group of transactions that force the simultaneous interaction between the Stateful and Sateless components (as explained in the prerequisite article linked below). The first transaction calls the Stateful component while the second transaction transfers ASA from or to the Stateless component.
- Stateful Algorand Smart Contract: implements the Application, triggered by the first transaction in the AT, that reads both the blockchain global state and users accounts’ local state to check if the withdrawal request matches both the ASA balance and block validity approval conditions.
- Stateless Algorand Smart Contract: implements the Contract Account that approves the asset transfer transaction to the users if and only if the previous call to the Stateful component succeeds.
The integration between Stateless and Stateful ASC1 is exhaustively covered in this article, which should be considered as a fundamental prerequisite for the following solution.
Let’s examine each of those bricks gradually.
Stateful ASC1 component
The main purpose of the Stateful component is to store and read permanent on-chain information, according to predefined TEAL logic branches. In the context of the proposed recurring ASA withdrawal solution, the Stateful Application makes use of the following variables:
Name | Domain | Type |
---|---|---|
AssetEscrow | Global | ByteSlice |
AssetID | Global | UInt |
Creator | Global | ByteSlice |
WithdrawalBookableAmount | Global | UInt |
WithdrawalProcessingRounds | Global | UInt |
WithdrawalBookedAmount | Local | UInt |
WithdrawalBookingRound | Local | UInt |
Stateful Applications are composed by two distinct TEAL programs:
- Approval Program
- Clear Program
Their roles are described in the lifecycle of a Stateful Smart Contract.
Clear Program
In the proposed solution the Clear Program will simply approve any clear call from the users to remove the application from their accounts balance.
#pragma version 2
int 1
Int(1)
Approval Program
The Approval Program is the TEAL logic that really define the application behaviour. The interaction between users accounts and Stateful applications is governed by the following five types of application calls:
- NoOp
- OptIn
- DeleteApplication
- UpdateApplication
- CloseOut
The Stateful Smart Contract should be able to detect any application call type and execute specific branches of TEAL logic accordingly. Other than these five application call types, it is also useful to catch the application create condition, in order to implement a specific branch of logic executed only on the application create transaction.
The Stateful ASC1 boilerplate is a great structured template that handles the required TEAL branches:
#pragma version 2
txn ApplicationID
int 0
==
bnz branch_create
txn OnCompletion
int OptIn
==
bnz branch_opt_in
txn OnCompletion
int CloseOut
==
bnz branch_close_out
txn OnCompletion
int UpdateApplication
==
bnz branch_update
txn OnCompletion
int DeleteApplication
==
bnz branch_delete
txn OnCompletion
int NoOp
==
bnz branch_app_calls
int 0
return
program = Cond(
[Txn.application_id() == Int(0), on_creation],
[Txn.on_completion() == OnComplete.OptIn, handle_optin],
[Txn.on_completion() == OnComplete.CloseOut, handle_closeout],
[Txn.on_completion() == OnComplete.UpdateApplication, handle_updateapp],
[Txn.on_completion() == OnComplete.DeleteApplication, handle_deleteapp],
[Txn.on_completion() == OnComplete.NoOp, handle_noop]
)
Let’s go through each TEAL branch to understand the application’s behaviour.
Application Create
Usually the application create is the branch in which variables initialization takes place.
branch_create:
byte "Creator"
txn Sender
app_global_put
int 1
return
b end_program
on_creation = Seq([
App.globalPut(Bytes("Creator"), Txn.sender()),
Return(Int(1))
])
On application create transaction the global variable Creator is initialized with the transaction sender, so that further management actions over the application can be authorized only by the Creator.
Users Opt-In
In the context of this solution there are no specific restrictions to the application opt-in. Note that the TEAL logic in the opt-in branch can be really useful when the application needs some kind of permission on accounts participation.
branch_opt_in:
int 1
return
b end_program
handle_optin = Return(Int(1))
Users Close-Out
In the context of this solution the colse-out condition does not need any kind of specific restriction, so it will always be simply approved by the TEAL logic.
branch_close_out:
int 1
return
b end_program
handle_closeout = Return(Int(1))
Application Update
The approval program of a Stateful Smart Contract could potentially be updated, unless expressly prevented. Although updating a Smart Contract may seem almost contradictory for someone, this feature expands ASC1 capabilities for those use case who need it and should be handled very carefully. This application does not allow any kind of approval program update, not even from its Creator, rejecting any application update call.
branch_update:
int 0
return
b end_program
handle_updateapp = Return(Int(0))
Application Delete
A Stateful Smart Contract could potentially be deleted, unless expressly prevented. As well as the updating feature, deleting a Stateful ASC1 should be handled very carefully. This application only allows its Creator to delete it.
branch_delete:
byte "Creator"
app_global_get
txn Sender
==
bnz is_creator
int 0
return
b end_program
is_creator:
int 1
return
b end_program
handle_deleteapp = If(
# Condition
App.globalGet(Bytes("Creator")) == Txn.sender(),
# Then
Return(Int(1)),
# Else
Return(Int(0))
)
Application Calls
The application calls trigger the main parts of a Stateful ASC1 approval program. In this solution 3 different actions can be executed, based on Atomic Transfer contents:
- Withdrawal Set-Up: if application is called by its Creator;
- Booking Withdrawal: if application is called passing
"str:Booking"
as argument; - Withdrawal: if application is called passing
"str:Withdrawal"
as argument;
branch_app_calls:
global GroupSize
int 2
==
byte "Creator"
app_global_get
gtxn 0 Sender
==
&&
bnz branch_withdrawal_setup
global GroupSize
int 2
==
gtxna 0 ApplicationArgs 0
byte "Booking"
==
&&
bnz branch_booking
global GroupSize
int 2
==
gtxna 0 ApplicationArgs 0
byte "Withdrawal"
==
&&
bnz branch_withdrawal
int 0
return
handle_noop = Cond(
[And(
Global.group_size() == Int(2),
App.globalGet(Bytes("Creator")) == Gtxn[0].sender()
), withdrawal_setup],
[And(
Global.group_size() == Int(2),
Gtxn[0].application_args[0] == Bytes("Booking")
), booking],
[And(
Global.group_size() == Int(2),
Gtxn[0].application_args[0] == Bytes("Withdrawal")
), withdrawal]
)
Application Call 1: Withdrawal Set-Up
Application Creator can set up the following global variables executing an Atomic Transfer, composed by an Application Call transaction and an Asset Transfer transaction:
- Asset Escrow Contract Account, passed as application call 0 argument, that is the public key obtained compiling the Stateless Asset Escrow Contract Account TEAL source code.
- Withdrawal Processing Rounds, passed as application call 1 argument, that is the number of blocks users must wait at least before executing booked withdrawals.
- AssetID, read from asset transfer funding transaction, is the Algorand Standard Asset unique identifier for the Asset Escrow.
- Withdrawal Bookable Amount, read from asset transfer funding transaction, is the total bookable amount of Algorand Standard Asset deposited in the Asset Escrow.
branch_withdrawal_setup:
gtxn 0 NumAppArgs
int 2
==
gtxn 1 TypeEnum
int axfer
==
&&
gtxn 1 AssetReceiver
gtxna 0 ApplicationArgs 0
==
&&
gtxn 1 AssetAmount
int 0
>
&&
bnz withdrawal_setup
int 0
return
b end_program
withdrawal_setup:
byte "AssetEscrow"
gtxna 0 ApplicationArgs 0
app_global_put
byte "WithdrawalProcessingRounds"
gtxna 0 ApplicationArgs 1
btoi
app_global_put
byte "AssetID"
gtxn 1 XferAsset
app_global_put
byte "WithdrawalBookableAmount"
gtxn 1 AssetAmount
app_global_put
int 1
return
withdrawal_setup = If(
# Condition
And(
Gtxn[0].application_args.length() == Int(2),
Gtxn[1].type_enum() == TxnType.AssetTransfer,
Gtxn[1].asset_receiver() == Gtxn[0].application_args[0],
Gtxn[1].asset_amount() > Int(0)
),
# Then
Seq([
App.globalPut(Bytes("AssetEscrow"),
Gtxn[0].application_args[0]),
App.globalPut(Bytes("WithdrawalProcessingRounds"),
Btoi(Gtxn[0].application_args[1])),
App.globalPut(Bytes("AssetID"),
Gtxn[1].xfer_asset()),
App.globalPut(Bytes("WithdrawalBookableAmount"),
Gtxn[1].asset_amount()),
Return(Int(1))
]),
# Else
Return(Int(0))
)
Application Call 2: Booking Withdrawals
Users can book ASA withdrawals executing an Atomic Transfer, composed by:
- Application Call, with 0 argument
"str:Booking"
, that registers withdrawal booking block in local state. - Asset Transfer transaction, that deposits the booked amount of ASA in the Asset Escrow.
The approval program checks if users already booked a withdrawal, preventing double-booking. If booking attempt succeeds, the global bookable amount is then reduced, preventing withdrawals over-booking.
branch_booking:
int 0
gtxn 0 ApplicationID
byte "WithdrawalBookingRound"
app_local_get_ex
store 0
store 1
load 0
load 1
int 0
>
&&
bnz booking_failure
gtxn 1 TypeEnum
int axfer
==
gtxn 1 XferAsset
byte "AssetID"
app_global_get
==
&&
gtxn 1 Sender
gtxn 0 Sender
==
&&
gtxn 1 AssetReceiver
byte "AssetEscrow"
app_global_get
==
&&
gtxn 1 AssetAmount
byte "WithdrawalBookableAmount"
app_global_get
<=
&&
bnz booking
int 0
return
booking:
int 0
byte "WithdrawalBookingRound"
global Round
app_local_put
int 0
byte "WithdrawalBookedAmount"
gtxn 1 AssetAmount
app_local_put
byte "WithdrawalBookableAmount"
byte "WithdrawalBookableAmount"
app_global_get
gtxn 1 AssetAmount
-
app_global_put
int 1
return
b end_booking
booking_failure:
int 0
return
end_booking:
int 1
return
withdrawal_booking_round = App.localGetEx(
Int(0), Gtxn[0].application_id(), Bytes("WithdrawalBookingRound")
)
booking = Seq([
withdrawal_booking_round,
If(
# Condition
And(
withdrawal_booking_round.hasValue(),
withdrawal_booking_round.value() > Int(0)
),
# Then
Return(Int(0)),
# Else
Seq([
Assert(
And(
Gtxn[1].type_enum() == TxnType.AssetTransfer,
Gtxn[1].xfer_asset() == App.globalGet(
Bytes("AssetID")),
Gtxn[1].sender() == Gtxn[0].sender(),
Gtxn[1].asset_receiver() == App.globalGet(
Bytes("AssetEscrow")),
Gtxn[1].asset_amount() <= App.globalGet(
Bytes("WithdrawalBookableAmount"))
)
),
App.localPut(Int(0), Bytes("WithdrawalBookingRound"),
Global.round()),
App.localPut(Int(0), Bytes("WithdrawalBookedAmount"),
Gtxn[1].asset_amount()),
App.globalPut(Bytes("WithdrawalBookableAmount"),
App.globalGet(Bytes("WithdrawalBookableAmount"))
- Gtxn[1].asset_amount()),
Return(Int(1))
])
),
Return(Int(1))
])
Depositing booked ASA amount prevents double booking while registering withdrawal booking block prevents malicious users from exploiting opt-in/close-out actions against the Application. In fact, if not properly addressed, malicious users can try to opt-in the application, execute the first withdrawal and then close-out the application clearing their local state, being potentially able to bypass the duration of withdrawal processing interval and executing multiple withdrawals in series. The booking withdrawals logic ensures that users will always have to wait at least the withdrawal processing interval duration before being able to execute the further withdrawals, avoiding the bypass flaw.
Tips
Whenever an application makes use of users local state, be sure that a sequence of opt-in/close-out (or clear state) cannot be exploited as a way to bypass your application logic in undesired ways.
Application Call 3: Withdrawal
Users who already booked a withdrawal can withdraw ASA executing an Atomic Transfer, composed by:
- Application Call, with 0 argument
"str:Withdrawal"
, that verifies users’ booked withdrawals have been already processed at the round of application call transaction. - Asset Transfer transaction, that verifies users’ withdrawal amount is double the amount of ASA deposited in the Asset Escrow.
If withdrawal attempt succeed users can book a new withdrawal.
branch_withdrawal:
int 0
gtxn 0 ApplicationID
byte "WithdrawalBookingRound"
app_local_get_ex
store 0
store 1
int 0
gtxn 0 ApplicationID
byte "WithdrawalBookedAmount"
app_local_get_ex
store 2
store 3
int 0
gtxn 0 ApplicationID
app_opted_in
load 1
int 0
>
&&
load 3
int 0
>
&&
global Round
int 0
byte "WithdrawalBookingRound"
app_local_get
byte "WithdrawalProcessingRounds"
app_global_get
+
>=
&&
gtxn 1 XferAsset
byte "AssetID"
app_global_get
==
&&
gtxn 1 Sender
byte "AssetEscrow"
app_global_get
==
&&
gtxn 1 AssetAmount
load 3
int 2
*
==
&&
bnz withdrawal
int 0
return
withdrawal:
int 0
byte "WithdrawalBookingRound"
int 0
app_local_put
int 0
byte "WithdrawalBookedAmount"
int 0
app_local_put
int 1
return
end_program:
withdrawal_approval_round = Ge(
Global.round(),
App.localGet(Int(0), Bytes("WithdrawalBookingRound")) +
App.globalGet(Bytes("WithdrawalProcessingRounds"))
)
withdrawal_booked_amount = App.localGetEx(
Int(0), Gtxn[0].application_id(), Bytes("WithdrawalBookedAmount")
)
withdrawal = Seq([
withdrawal_booking_round,
withdrawal_booked_amount,
Assert(
And(
App.optedIn(Int(0), Gtxn[0].application_id()),
withdrawal_booking_round.value() > Int(0),
withdrawal_booked_amount.value() > Int(0),
withdrawal_approval_round,
Gtxn[1].xfer_asset() == App.globalGet(Bytes("AssetID")),
Gtxn[1].sender() == App.globalGet(Bytes("AssetEscrow")),
Gtxn[1].asset_amount() == Mul(
withdrawal_booked_amount.value(), Int(2)
)
)
),
App.localPut(Int(0), Bytes("WithdrawalBookingRound"), Int(0)),
App.localPut(Int(0), Bytes("WithdrawalBookedAmount"), Int(0)),
Return(Int(1))
])
Stateless ASC1 component
The main purpose of the Stateless component is to approve or reject transactions, according to predefined TEAL logic branches. In the context of the proposed recurring ASA withdrawal solution, the Stateless Escrow Contract Account approves or rejects the following transactions:
Name | Type |
---|---|
Escrow Contract ASA Opt-In | Single Transaction |
Escrow Contract ASA withdrawal by Stateful Application Users | Group Transaction |
#pragma version 2
global GroupSize
int 1
==
bnz branch_opt_in
global GroupSize
int 2
==
bnz branch_withdrawal
int 0
return
program = Cond(
[Global.group_size() == Int(1), asa_opt_in],
[Global.group_size() == Int(2), asa_withdraw]
)
While the Stateful Component’s parameters can be updated over time passing arguments through application call transactions (as the Asset Escrow Contract Account address or the Withdrawal Processing Rounds), the Stateless Component’s parameters must be hardcoded into the TEAL logic, so must be know in advance. For the Escrow Contract Account these parameter must be know:
TMPL_APP_ID
TMPL_ASSET_ID
Tips
Read carefully the guidelines before stating developing your Stateless ASC1.
Escrow Contract Account ASA Opt-In or Close-Out
The Asset Escrow Contract Account only approves ASA opt-in transaction for one specific AssedID, which must be equal to the AssetID global variable of the Stateful Component.
branch_opt_in:
txn TypeEnum
int axfer
==
txn XferAsset
int TMPL_ASSET_ID
==
&&
txn AssetAmount
int 0
==
&&
txn Fee
int 1000
<=
&&
txn RekeyTo
global ZeroAddress
==
&&
txn AssetCloseTo
global ZeroAddress
==
&&
b end_contract
fee = Int(1000)
asa_opt_in = And(
Txn.type_enum() == TxnType.AssetTransfer,
Txn.xfer_asset() == Int(asa_id),
Txn.asset_amount() == Int(0),
Txn.fee() <= fee,
Txn.rekey_to() == Global.zero_address(),
Txn.asset_close_to() == Global.zero_address(),
)
Escrow Contract Account withdrawal
The Asset Escrow Contract Account only approves ASA withdrawals of AssedID, if and only if withdrawals transactions are submitted as Atomic Transfer, composed by of 2 transactions, that:
- Calls the Stateful Withdrawal Application, ensuring that only processed withdrawals are approved.
- Transfers the right amount of ASA from the Asset Escrow Contract Account to the User.
branch_withdrawal:
gtxn 0 TypeEnum
int appl
==
gtxn 0 ApplicationID
int TMPL_APP_ID
==
&&
gtxn 0 OnCompletion
int NoOp
==
&&
gtxn 1 TypeEnum
int axfer
==
&&
gtxn 1 XferAsset
int TMPL_ASSET_ID
==
&&
gtxn 1 Fee
int 1000
<=
&&
gtxn 1 AssetCloseTo
global ZeroAddress
==
&&
gtxn 1 RekeyTo
global ZeroAddress
==
&&
end_contract:
asa_withdraw = And(
Gtxn[0].type_enum() == TxnType.ApplicationCall,
Gtxn[0].application_id() == Int(app_id),
Gtxn[0].on_completion() == OnComplete.NoOp,
Gtxn[1].type_enum() == TxnType.AssetTransfer,
Gtxn[1].xfer_asset() == Int(asa_id),
Gtxn[1].fee() <= fee,
Gtxn[1].asset_close_to() == Global.zero_address(),
Gtxn[1].rekey_to() == Global.zero_address()
)
Application Deployment
The application is deployed constructing and issuing transactions manually through goal CLI, according a specific timeline. As mentioned in the overview, the application could be deployed programmatically using any Algorand SDK instead of goal CLI, consider this as your next step exercise.
Creating Stateful ASA Withdrawal Application
The recurring withdrawal dApp deployment starts with the creation of its Stateful component, providing the both the withdrawal_approval.teal
and the withdrawal_clear.teal
and declaring the global state schema and the local state schema consumed by the application.
Input
$ ./goal app create --creator TMPL_CREATOR_ADDRESS --approval-prog withdrawal_approval.teal --clear-prog withdrawal_clear.teal --global-byteslices 2 --global-ints 3 --local-byteslices 0 --local-ints 2
Output
Created app with app index TMPL_APP_ID
Querying existing applications on the testing private network with the Indexer we get an overview of its properties:
Input
curl "localhost:8980/v2/applications?pretty"
Output
{
"application": {
"created-at-round": ...,
"id": TMPL_APP_ID,
"params": {
"approval-program": "...",
"clear-state-program": "...",
"creator": "TMPL_CREATOR_ADDRESS",
"global-state": [
{
"key": "Q3JlYXRvcg==",
"value": {
"bytes": "pZwQR2IXbUaqt+fGQ+cmcuVXGz/W407o0AsCi6DA+Fg=",
"type": 1,
"uint": 0
}
}
],
"global-state-schema": {
"num-byte-slice": 2,
"num-uint": 3
},
"local-state-schema": {
"num-byte-slice": 0,
"num-uint": 2
}
}
},
"current-round": ...
}
Then, reading the application global sate, we can check that the Creator global variable has been correctly initialized.
Input
$ ./goal app read --app-id TMPL_APP_ID --global
Output
{
"Creator": {
"tb": "TMPL_CREATOR_ADDRESS",
"tt": 1
}
}%
Initializing Stateless Asset Escrow Contract Account
We can now assign values to Stateless ASC1 parameters, hard-coding them in its TEAL source code:
TMPL_ASSET_ID
TMPL_APP_ID
and compile the withdrawal_escrow.teal
Input
$ ./goal clerk compile withdrawal_escrow.teal
Output
withdrawal_escrow.teal: TMPL_ESCROW_CONTRACT_ADDRESS
As any other account on Algorand, also Contracts Accounts must be activated funding them at least with a minimum balance of 0,1 ALGO. Let’s fund it with 0,3 ALGO.
Input
$ ./goal clerk send -f TMPL_CREATOR_ADDRESS -t TMPL_ASSET_ESCROW_ADDRESS -a 300000
Once the Asset Escrow Contract Address has been funded, it can now opt-in the ASA (the only one allowed by definition in its TEAL logic). Transactions that involve Stateless ASC1 must be signed with Logic Signature, it means that we have to:
write the unsigned transaction on file
Input
$ ./goal asset send --assetid TMPL_ASSET_ID -f TMPL_ASSET_ESCROW_ADDRESS -t TMPL_ASSET_ESCROW_ADDRESS -a 0 -o escrow_optin.txn
sign it providing its TEAL logic
Input
$ ./goal clerk sign -i escrow_optin.txn -p withdrawal_escrow.teal -o escrow_optin.ltxn
submit it to the network
Input
$ ./goal clerk rawsend -f escrow_optin.ltxn
Once the transaction has been committed, we can see its effect reading the Asset Escrow Contract account info:
Input
$ ./goal account info -a TMPL_ESCROW_CONTRACT_ADDRESS
Output
Created Assets:
<none>
Held Assets:
ID TMPL_ASSET_ID, Test, balance 0.0 TST
Created Apps:
<none>
Opted In Apps:
<none>
We are finally ready to fund the Asset Escrow Contract with an amount of ASA in order to back users withdrawals.
Setting Up Stateful ASA Withdrawal Application
The application can be setted up only by its Creator, who must:
- Provide application call arguments in this specific order:
"addr:TMPL_ASSET_ESCROW_ADDRESS"
,"int:TMPL_WITHDRAWALS_PROCESSING_BLOCKS"
- Fund the Asset Escrow with the total bookable amount of ASA.
Unsigned standalone transactions creation
$ ./goal app call --app-id TMPL_APP_ID -f TMPL_CREATOR_ADDRESS --app-arg "addr:TMPL_ASSET_ESCROW_ADDRESS" --app-arg "int:TMPL_WITHDRAWALS_PROCESSING_BLOCKS" -o withdrawal_app_call.txn
$ ./goal asset send --assetid TMPL_ASSET_ID -f TMPL_CREATOR_ADDRESS -t TMPL_ASSET_ESCROW_ADDRESS -a TMPL_BOOKABLE_AMOUNT -o escrow_funding.txn
Grouping unsigned standalone transactions
$ cat withdrawal_app_call.txn escrow_funding.txn > setup.txn
$ ./goal clerk group -i setup.txn -o setup.gtxn
Splitting unsigned group transaction, transactions are no longer valid if submitted as standalone.
Input
$ ./goal clerk split -i setup.gtxn -o unsigned_setup.txn
Output
Wrote transaction 0 to unsigned_setup-0.txn
Wrote transaction 1 to unsigned_setup-1.txn
Signing standalone transactions
$ ./goal clerk sign -i unsigned_setup-0.txn -o setup-0.stxn
$ ./goal clerk sign -i unsigned_setup-1.txn -o setup-1.stxn
Grouping signed standalone transactions, they can be committed only together.
$ cat setup-0.stxn setup-1.stxn > setup.sgtxn
Submitting signed group transaction
$ ./goal clerk rawsend -f setup.sgtxn
Reading the application global sate, we can now check that the other global variables have been correctly initialized.
Input
$ ./goal app read --app-id TMPL_APP_ID --global
Output
{
"AssetEscrow": {
"tb": "TMPL_ASSET_ESCROW_ADDRESS",
"tt": 1
},
"AssetID": {
"tt": 2,
"ui": TMPL_ASSET_ID
},
"Creator": {
"tb": "TMPL_CREATOR_ADDRESS",
"tt": 1
},
"WithdrawalBookableAmount": {
"tt": 2,
"ui": TMPL_BOOKABLE_AMOUNT
},
"WithdrawalProcessingRounds": {
"tt": 2,
"ui": TMPL_WITHDRAWALS_PROCESSING_BLOCKS
}
}
The recurring withdrawal application is now fully deployed and ready to be used.
Application Usage
From users perspective ASA recurring withdrawals dApp consists of the following 3 steps:
- Withdrawal Application Opt-In
- Withdrawal Booking
- Withdrawal
Each one of those requires the submission of different kind of transaction or group transactions.
User Application Opt-In
Any Stateful application that makes use of account’s local state must be opted-in by users in order to be able to interact with them. Application opt-in is a specific kind of application transaction call. In the context of this solution the approval program does not perform any kind of verification on opt-in application calls, so anyone can opt-in:
Input
$ ./goal app optin --app-id TMPL_APP_ID -f TMPL_USER_ADDRESS
User Account’s state has been updated with Stateful application local sate schema that consists only of two uint variables.
Input
$ ./goal account info -a TMPL_USER_ADDRESS
Output
Created Assets:
<none>
Held Assets:
ID TMPL_ASSET_ID, Test, balance 10.0 TST
Created Apps:
<none>
Opted In Apps:
ID TMPL_APP_ID, local state used 0/2 uints, 0/0 byte slices
Withdrawal Booking
Users have to book their withdrawals in advance and wait until they have been processed before being able to actually execute them. Users book a withdrawal through an Atomic Transfer, composed by an Application Call transaction (with argument "str:Booking"
) and an Asset Transfer transaction with which they lock the booked amount into the Asset Escrow.
Unsigned standalone transactions creation
$ ./goal app call --app-id TMPL_APP_ID -f TMPL_USER_ADDRESS --app-arg "str:Booking" -o withdrawal_booking.txn
$ ./goal asset send --assetid TMPL_ASSET_ID -f TMPL_USER_ADDRESS -t TMPL_ASSET_ESCROW -a TMPL_BOOKED_AMOUT -o withdrawal_locking.txn
Grouping unsigned standalone transactions
$ cat withdrawal_booking.txn withdrawal_locking.txn > booking.txn
$ ./goal clerk group -i booking.txn -o booking.gtxn
Splitting unsigned group transaction, transactions are no longer valid if submitted as standalone.
Input
$ ./goal clerk split -i booking.gtxn -o unsigned_booking.txn
Output
Wrote transaction 0 to unsigned_booking-0.txn
Wrote transaction 1 to unsigned_booking-1.txn
Signing standalone transactions
$ ./goal clerk sign -i unsigned_booking-0.txn -o booking-0.stxn
$ ./goal clerk sign -i unsigned_booking-1.txn -o booking-1.stxn
Grouping signed standalone transactions, they can be committed only together.
$ cat booking-0.stxn booking-1.stxn > booking.sgtxn
Submitting signed group transaction
$ ./goal clerk rawsend -f booking.sgtxn
Once the Atomic Transfer has been committed the WITHDRAWAL_BOOKING_BLOCK
and TMPL_BOOKED_AMOUT
are saved into User’s local state and the ASA TMPL_BOOKED_AMOUT
deposited into the Asset Escrow until the withdrawal.
Input
$ ./goal app read --app-id TMPL_APP_ID --local -f TMPL_USER_ADDRESS
Output
{
"WithdrawalBookedAmount": {
"tt": 2,
"ui": TMPL_BOOKED_AMOUT
},
"WithdrawalBookingRound": {
"tt": 2,
"ui": WITHDRAWAL_BOOKING_BLOCK
}
}
The total bookable amount is now reduced, you can check it with
Input
$ ./goal app read --app-id TMPL_APP_ID --global
The user must now wait TMPL_WITHDRAWALS_PROCESSING_BLOCKS
to claim the booked withdrawal.
Withdrawal
Withdrawals are executed as a group transaction, consisting of a Stateful Application call transaction and Stateless Asset Escrow booked amount transfer.
Unsigned standalone transactions creation
$ ./goal app call --app-id TMPL_APP_ID -f TMPL_USER_ADDRESS --app-arg "str:Withdrawal" -o withdrawal_app_call.txn
$ ./goal asset send --assetid TMPL_ASSET_ID -f TMPL_ASSET_ESCROW_ADDRESS -t TMPL_USER_ADDRESS -a DOUBLE_TMPL_BOOKED_AMOUT -o withdrawal_escrow_transfer.txn
Grouping unsigned standalone transactions
$ cat withdrawal_app_call.txn withdrawal_escrow_transfer.txn > withdrawal.txn
$ ./goal clerk group -i withdrawal.txn -o withdrawal.gtxn
Splitting unsigned group transaction, transactions are no longer valid if submitted as standalone.
Input
$ ./goal clerk split -i withdrawal.gtxn -o unsigned_withdrawal.txn
Output
Wrote transaction 0 to unsigned_withdrawal-0.txn
Wrote transaction 1 to unsigned_withdrawal-1.txn
Signing standalone transactions
$ ./goal clerk sign -i unsigned_withdrawal-0.txn -o withdrawal-0.stxn
$ ./goal goal clerk sign -i unsigned_withdrawal-1.txn -p withdrawal_escrow.teal -o withdrawal-1.ltxn
Grouping signed standalone transactions, they can be committed only together.
$ cat withdrawal-0.stxn withdrawal-1.ltxn > withdrawal.sgtxn
Submitting signed group transaction
$ ./goal clerk rawsend -f withdrawal.sgtxn
Once the Atomic Transfer as been committed, the user withdraws from the Asset Escrow both the amount locked on booking process plus the booked amount.
Conclusions
You are now able to create and deploy your own ASA recurring withdrawal dApp, customizing the withdrawal amount approval with the logic that best fit your use case.