How to build a dApp on Algorand?

Advanced

 

How to build a dApp on Algorand?

For Algorand dApp development, let us consider a situation where a client named Ashley asks a developer to create an auction dApp for NFTs. The developer is asked to create a customized dApp with the following requirements:

  • For each piece of artwork, sellers must be able to generate a new auction. The contract should be able to hold the artwork from the opening of the auction until closing.

  • The auction can be terminated even before starting, where the item would return to the seller.

  • Sellers can reserve a price for the artwork, and if bidders do not meet the condition, the item will be returned to the seller.

  • During each bid, when the new bid is higher than the bid before it, the previous bidder will be refunded, and the new bid will be recorded.

  • When the auction is successful and the reserve price is met, the seller will get the full bid amount, and the highest bidder will receive the artwork.

Let us develop the dApp based on the above features using Python.

This tutorial is divided into two sections. In the first section, we will deploy the dApp and run a demo auction. The second part sketches an outline of the various components of the auction application.

Section 1: Launch the application

To run a node, Algorand issues a Docker instance. So, install docker before moving ahead to the next steps.

Prerequisites

  • Docker

  • Python 3.6 or higher

Step 1: Clone the auction demo application

Use the commands given below to replicate the repository:

git clone https://github.com/algorand/auction-demo  cd auction-demo

Step 2: Install Sandbox

If you are a Mac or Linux user, run the following codes directly from your terminal. However, if you use Windows, run from a terminal supporting bash, such as Git Bash. We can clone the sandbox repository to “./_sandbox” and start some docker containers using this command:

./sandbox up

Step 3: Setup environment and run tests

Install the necessary prerequisites and use a Python virtual environment for the project. The below commands can activate the virtual environment and then install every dependency, including PyTeal and the Algorand Python SDK.

Now, set up a Python environment or “venv” (one time):

python3 -m venv venv

Activate “venv”. Substitute “bin” with “Scripts” on Windows.

If your shell is bash/zsh, use the following codes:

. venv/bin/activate

Now, install dependencies:

pip3 install -r requirements.txt

Run the file, “example.py,” which can run a 30-second auction:

python3 example.py

#output 
Ashley is generating temporary accounts...
Ashley is generating an example NFT... 
The NFT ID is: 15
Ashley is creating auction smart contract that lasts 30 seconds to auction off NFT... 
Ashley is setting up and funding NFT auction... 
Ashley's algo balance: 99998100 algos 
The smart contract now holds the following: {0: 202000, 15: 1} 
Jasmine wants to bid on NFT, her algo balance: 100000100 algos 
Jasmine is placing bid for: 1000000 algos 
Jasmine is opting into NFT with id: 15 
Ashley is closing out the auction.... 
The smart contract now holds the following: {0: 0} 
Jasmine's NFT balance: 1 for NFT ID: 15
Ashley's balances after auction: {0: 101197100, 15: 0} Algos 
Jasmine's balances after auction: {0: 98997100, 15: 1} Algos

You can run tests if needed:

pytest
 

The above step will run through every test in the “operations_test.py” file and take more than six minutes to execute.

When the testing is finished, use the given command to shutdown the sandbox:

./sandbox down

Now let us delve into the code.

Section 2: Application Overview

The auction demo application auctions off an NFT using a smart contract. To achieve this functionality, the smart contract provides four different techniques.

The first method develops the smart contract and sets up the auction state on the blockchain, which involves the seller’s account, the particular NFT ID to auction, the auction’s beginning and ending time, the reserved auction price, and the minimum bid increment.

The second technique completes the auction by funding the smart contract with a limited number of algos (to meet minimum balance criteria and pay transaction fees) and putting the NFT into the smart contract.

The bid scenario is created using the third way. The potential buyer submits the bid to the smart contract using algos. The contract will hold the algos if the bid is successful, and bidding can only be done between the auction’s start and end times. The contract immediately returns the algos of the prior higher bidder if the bid replaces a previous bid.

The auction can be closed out using the fourth and final method of the contract, which will either assign the NFT to the highest bidder and transfer the algos to the seller or return the NFT to the seller and close out any remaining algos to the seller.

Smart contracts are recorded on the blockchain and can be accessed remotely using a transaction type known as an application transaction. Using the PyTeal library with the Algorand Python SDK is the easiest way to create smart contracts. Write the smart contract logic that will be saved (the on-chain value transfer logic) using PyTeal. Use the SDK to deploy the smart contract and then develop the application transactions that will interface with it (the off-chain logic and smart contract triggers).

The smart contract code and SDK activities are separated into two files by the auction demo code. “contracts.py” has all the logic for the smart contract, and “operations.py” includes the SDK code to deploy the smart contract and interact with it when deployed.

The Smart Contract

The smart contract code is written in the “contracts.py” file using the PyTeal library. A new smart contract is deployed in this scenario for each new auction. As we mentioned earlier, every auction requires the ability to create, set up, bid on, and close off an auction. We’ll go over where each of these eventualities is represented in the code in a moment.

On Algorand, smart contracts are made up of two separate ‘programs.’ The approval program is the first, and the clear program is the second. Because the approval program contains the majority of the code, we don’t need to bother about the clear program for now.

Let’s move on to the top-level routing code for the smart contract’s approval program

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 code takes care of any application transaction sent to the approval program. These types are distinguished by their “OnComplete” values. You may have observed that the first line stands out from the others. The application ID of a smart contract is always zero when it is first formed. The first line uses this to send the “on_create” variable to the auction creation logic.

Next, we’ll look at the creation code.

1. Create Auction
on_create_start_time = Btoi(Txn.application_args[2]) 
on_create_end_time = Btoi(Txn.application_args[3]) 
on_create = Seq( 
App.globalPut(seller_key, Txn.application_args[0]), App.globalPut(nft_id_key, Btoi(Txn.application_args[1])), App.globalPut(start_time_key, on_create_start_time), App.globalPut(end_time_key, on_create_end_time), App.globalPut(reserve_amount_key, Btoi(Txn.application_args[4])), App.globalPut(min_bid_increment_key, Btoi(Txn.application_args[5])), App.globalPut(lead_bid_account_key, Global.zero_address()), 
Assert( 
And( 
Global.latest_timestamp() < on_create_start_time, on_create_start_time < on_create_end_time 
) 
), 
Approve(), 
)

This code collects all auction parameters and stores them in the smart contract’s on-chain global state. It also checks to see if the start and end times have elapsed or not.

A “NoOp” call is the next conditional in our main program routing code. The “NoOp” application transaction is simply a smart contract call that includes arguments such as strings that can be used to activate additional conditions. We may provide a “NoOp” call with the ‘setup’ or ‘bid’ argument in this contract, and it will route to the right logic for each of those circumstances. The “on_call” variable is used to display the next layer routing.

on_call_method = Txn.application_args[0] 
on_call = Cond( 
[on_call_method == Bytes("setup"), on_setup], 
[on_call_method == Bytes("bid"), on_bid], 
)
2. Setup the auction:

Let’s get to the “on_setup” code by following the ‘setup’ path

on_setup = Seq( 
Assert(Global.latest_timestamp() < App.globalGet(start_time_key)), 
# opt into NFT asset -- because you can't opt in if you're already opted in, this is what 
# we'll use to make sure the contract has been set up InnerTxnBuilder.Begin(), 
InnerTxnBuilder.SetFields( 
{ 
TxnField.type_enum: TxnType.AssetTransfer, 
TxnField.xfer_asset: App.globalGet(nft_id_key), TxnField.asset_receiver: Global.current_application_address(), 
} 
), 
InnerTxnBuilder.Submit(), 
Approve(), 
)

This code checks that the start time has not passed and then uses a smart contract transaction to enroll the smart contract into the NFT going to be auctioned. It’s worth noting that this transaction will be grouped with two others: a payment transaction to fund the smart contract with some algos to cover minimum balance requirements and transaction fees, an application transaction to initiate the setup, and an asset transfer transaction to move the NFT from the seller’s account to the smart contract.

3. Bid in the auction:

Let’s have a look at the “on_bid” scenario, which will be tested if a user submits a ‘bid’ with a “NoOp” application transaction.

on_bid_txn_index = Txn.group_index() - Int(1) 
on_bid_nft_holding = AssetHolding.balance( Global.current_application_address(), App.globalGet(nft_id_key) 
) 
on_bid = Seq( 
on_bid_nft_holding, 
Assert( 
And( 
# the auction has been set up 
on_bid_nft_holding.hasValue(), 
on_bid_nft_holding.value() > Int(0), 
# the auction has started 
App.globalGet(start_time_key) <= Global.latest_timestamp(), 
# the auction has not ended 
Global.latest_timestamp() < App.globalGet(end_time_key),  # the actual bid payment is before the app call Gtxn[on_bid_txn_index].type_enum() == TxnType.Payment, Gtxn[on_bid_txn_index].sender() == Txn.sender(), Gtxn[on_bid_txn_index].receiver()  == Global.current_application_address(), Gtxn[on_bid_txn_index].amount() >= Global.min_txn_fee(), 
)
), 
If(
Gtxn[on_bid_txn_index].amount() 
>= App.globalGet(lead_bid_amount_key) + App.globalGet(min_bid_increment_key) 
).Then(
Seq(
If(App.globalGet(lead_bid_account_key) != Global.zero_address()).Then( repayPreviousLeadBidder(    App.globalGet(lead_bid_account_key), App.globalGet(lead_bid_amount_key), 
) 
),
App.globalPut(lead_bid_amount_key, Gtxn[on_bid_txn_index].amount()), App.globalPut(lead_bid_account_key, Gtxn[on_bid_txn_index].sender()), App.globalPut(num_bids_key, App.globalGet(num_bids_key) + Int(1)), 
Approve(), 
) 
), 
Reject(), 
)Here, we determine if the bid is higher than the existing bid leader (stored in the contract state on-chain). If this is a new bidder, and the new bid exceeds the previous one, the smart contract issues a refund transaction for the previous highest bidder. This code also confirms that the auction is still running and that the bidder sent a payment transaction (their bid) along with the smart contract application transaction.
4. Close out the auction:

A “DeleteApplication” application transaction is used to end the auction. After assessing the logic in the “on_delete” variable, this transaction will attempt to delete the smart contract.

on_delete = Seq( 
If(Global.latest_timestamp() < App.globalGet(start_time_key)).Then( 
Seq( 
# the auction has not yet started, it's ok to delete 
Assert( 
Or( 
# sender must either be the seller or the auction creator Txn.sender() == App.globalGet(seller_key), 
Txn.sender() == Global.creator_address(), 
)
), 
# if the auction contract account has opted into the nft, close it out closeNFTTo(App.globalGet(nft_id_key), App.globalGet(seller_key)), 
# if the auction contract still has funds, send them all to the seller closeAccountTo(App.globalGet(seller_key)), 
Approve(), 
) 
), 
If(App.globalGet(end_time_key) <= Global.latest_timestamp()).Then(  Seq(  # the auction has ended, pay out assets If(App.globalGet(lead_bid_account_key) != Global.zero_address()) .Then(  If( App.globalGet(lead_bid_amount_key)  >= App.globalGet(reserve_amount_key) 
) 
.Then( 
# the auction was successful: send lead bid account the nft closeNFTTo( 
App.globalGet(nft_id_key), App.globalGet(lead_bid_account_key), 
) 
) 
.Else( 
Seq( 
# the auction was not successful because the reserve was not met: return 
# the nft to the seller and repay the lead bidder closeNFTTo( 
App.globalGet(nft_id_key), App.globalGet(seller_key) 
), 
repayPreviousLeadBidder( App.globalGet(lead_bid_account_key), App.globalGet(lead_bid_amount_key), 
), 
) 
) 
) 
.Else( 
# the auction was not successful because no bids were placed: return the nft to the seller closeNFTTo(App.globalGet(nft_id_key), App.globalGet(seller_key)) 
), 
# send remaining funds to the seller 
closeAccountTo(App.globalGet(seller_key)), Approve(), 
) 
), 
Reject(), 
)

Because it must handle numerous close-out scenarios, this code is somewhat long. The first case is when the auction is canceled before it even begins where the smart contract returns the NFT and all algos back to the seller. The smart contract will transfer the NFT to the winning bidder and send the algos to the seller if the auction is completed and the reserve is reached. If the bidder cannot meet the reserve, the highest bidder will be refunded and the seller will get the NFT back. Likewise, if no one bids in the auction, the NFT and the algos will be refunded to the seller. Any account can end the auction.

Deploying and communicating with the smart contract

We learned how the smart contract program protects crucial value transfer scenarios during auctions (e.g., sending bids, closing it out). Let’s see how to put it on the blockchain and communicate with it now. The relevant transactions are built using Algorand Python SDK in the “operations.py” file. It has a method for each of the four auction scenarios supported by the application. The first scenario, which is stated in the “createAuctionApp” method, is to deploy and build the auction contract.

def createAuctionApp( 
client: AlgodClient, 
sender: Account, 
seller: str, 
nftID: int, 
startTime: int, 
endTime: int, 
reserve: int, 
minBidIncrement: int, 
) -> int: 
"""Create a new auction. 
Args: 
client: An algod client. 
sender: The account that will create the auction application. 
seller: The address of the seller that currently holds the NFT being auctioned. 
nftID: The ID of the NFT being auctioned. 
startTime: A UNIX timestamp representing the start time of the auction. This must be greater than the current UNIX timestamp. endTime: A UNIX timestamp representing the end time of the auction. This must be greater than startTime. 
reserve: The reserve amount of the auction. If the auction ends without a bid that is equal to or greater than this amount, the auction will fail, meaning the bid amount will be refunded to the lead bidder and the NFT will return to the seller. 
minBidIncrement: The minimum difference required between a new bid and the current leading bid. 
Returns: 
The ID of the newly created auction app. 
""" 
approval, clear = getContracts(client) 
globalSchema = transaction.StateSchema(num_uints=7, num_byte_slices=2) 
localSchema = transaction.StateSchema(num_uints=0, num_byte_slices=0) 
app_args = [ 
encoding.decode_address(seller), 
nftID.to_bytes(8, "big"), 
startTime.to_bytes(8, "big"), 
endTime.to_bytes(8, "big"), 
reserve.to_bytes(8, "big"), 
minBidIncrement.to_bytes(8, "big"), 
] 
txn = transaction.ApplicationCreateTxn( 
sender=sender.getAddress(), 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(sender.getPrivateKey()) client.send_transaction(signedTxn) 
response = waitForTransaction(client, signedTxn.get_txid()) 
assert response.applicationIndex is not None and response.applicationIndex > 0 return response.applicationIndexThe parameters for the auction (reserve price, NFT ID, and so on) are configured, and the smart contract is deployed in this function. It uses the “ApplicationCreateTxn” function, which sends the smart contract’s compiled version to the blockchain. The smart contract will be given a unique ID and an Algorand address once launched.

Next is the “setupAuctionApp” function, used to fund the smart contract with a small amount of algos to support fees and the Algorand account’s minimum balance requirement, an application transaction instructing the smart contract to set up the auction, and an asset transfer transaction from the NFT to the smart contract.

def setupAuctionApp( 
client: AlgodClient, 
appID: int, funder: Account, nftHolder: Account, nftID: int, nftAmount: int, ) 
appAddr = getAppAddress(appID) suggestedParams = client.suggested_params() 
fundingAmount = ( # min account balance 100_000 # additional min balance to opt into NFT + 100_000 # 3 * min txn fee + 3 * 1_000 ) 
fundAppTxn = transaction.PaymentTxn( sender=funder.getAddress(), 
receiver=appAddr, amt=fundingAmount, sp=suggestedParams, ) 
setupTxn = transaction.ApplicationCallTxn( sender=funder.getAddress(), 
index=appID, on_complete=transaction.
OnComplete.NoOpOC, app_args=[b"setup"], 
foreign_assets=[nftID], sp=suggestedParams, )
fundNftTxn = transaction.AssetTransferTxn( sender=nftHolder.getAddress(), 
receiver=appAddr, index=nftID, amt=nftAmount, sp=suggestedParams, ) transaction.assign_group_id([fundAppTxn, setupTxn, fundNftTxn]) 
signedFundAppTxn = fundAppTxn.sign(funder.getPrivateKey()) 
signedSetupTxn = setupTxn.sign(funder.getPrivateKey()) 
signedFundNftTxn = fundNftTxn.sign(nftHolder.getPrivateKey()) 
client.send_transactions([signedFundAppTxn, signedSetupTxn, signedFundNftTxn]) 
waitForTransaction(client, signedFundAppTxn.get_txid())

The three transactions are combined and uploaded to the blockchain.

The “placeBid” function is used in the auction application to place a bid on a certain NFT. This function generates a payment transaction from the bidder to the smart contract and an application transaction declaring the bid to the smart contract.

def placeBid(client: AlgodClient, appID: int, bidder: Account, bidAmount: int) 
appAddr = getAppAddress(appID) 
appGlobalState = getAppGlobalState(client, appID) 
nftID = appGlobalState[b"nft_id"] 
if any(appGlobalState[b"bid_account"]):
# if "bid_account" is not the zero address prevBidLeader = encoding.encode_address(appGlobalState[b"bid_account"]) 
else: prevBidLeader = None suggestedParams = client.suggested_params() 
payTxn = transaction.PaymentTxn( sender=bidder.getAddress(), 
receiver=appAddr, amt=bidAmount, sp=suggestedParams, ) 
appCallTxn = transaction.ApplicationCallTxn( sender=bidder.getAddress(), 
index=appID, on_complete=transaction.OnComplete.NoOpOC, app_args=[b"bid"], 
foreign_assets=[nftID], # must include the previous lead bidder here to the app can refund that bidder's payment accounts=[prevBidLeader] 
if prevBidLeader is not None else [], sp=suggestedParams, ) 
transaction.assign_group_id([payTxn, appCallTxn]) signedPayTxn = payTxn.sign(bidder.getPrivateKey()) 
signedAppCallTxn = appCallTxn.sign(bidder.getPrivateKey()) 
client.send_transactions([signedPayTxn, signedAppCallTxn]) 
waitForTransaction(client, appCallTxn.get_txid())

These two transactions have been combined. On Algorand, grouping transactions, also known as atomic transfers, is a powerful feature.

The auction is ended with the “closeAuction” function. Any account can access this function.

def closeAuction(client: AlgodClient, appID: int, closer: Account):  
appGlobalState = getAppGlobalState(client, appID) 
nftID = appGlobalState[b"nft_id"] accounts: List[str] = [encoding.encode_address(appGlobalState[b"seller"])] 
if any(appGlobalState[b"bid_account"]): 
# if "bid_account" is not the zero address accounts.append(encoding.encode_address(appGlobalState[b"bid_account"])) 
deleteTxn = transaction.ApplicationDeleteTxn( sender=closer.getAddress(), 
index=appID, accounts=accounts, foreign_assets=[nftID], sp=client.suggested_params(), ) 
signedDeleteTxn = deleteTxn.sign(closer.getPrivateKey()) 
client.send_transaction(signedDeleteTxn) 
waitForTransaction(client, signedDeleteTxn.get_txid())

Using the “ApplicationDeleteTxn” function, this method deletes an application transaction.

You’ve now completed the guide to launching an Algorand auction dApp.