Build Algorand Android Smart Contract using Kotlin
Kotlin is a modern statically typed programming language used by over 60% of professional Android developers that helps boost productivity, developer satisfaction, and code safety. Kotlin can also be used to create multi-platform (Android/IOS) applications. But Macbook will be required to create Kotlin Multiplatform Mobile (KMM). More info here .
Recently most Android development is done using Kotlin. And as the time of writing this tutorial there is no Kotlin guide for integrating the Algorand SDK & REST-API.
This tutorial is for Android developers using Kotlin who want to integrate the Algorand on their application. For this tutorial we will be using the Algorand Java-SDK and the Purestake REST API as not all queries are available on the REST-API at the time of this tutorial.
I will be following Kotlin/Android development best practice here using Model View ViewModel (MVVM), more info on MVVM can be found here and Couroutines for handling background task, Databinding, Timber for logging and Hilt for dependency injection. Only code snippet relating to actual interraction with the Algorand service will be shown here. The rest of the code can be found on the github repo .
Figure 0-1
This tutorial will cover
-
Account creation
-
Funding accounts
-
Transferring funds
-
Getting account transactions
-
Stateful and Stateless smart contract
Requirements
-
Android studio setup
-
Familiarity with the Java and Kotlin programming language and its use within the Android Environment.
-
Basic understanding of some blockchain terminologies.
Steps
- 1.Workspace setup and configuration in android studio
- 2.Account creation and Generate Algod Pair
- 3.Import an existing account
- 4.Funding an account
- 5.Checking account balance
- 6.Sending a payments
- 7.Display list of transactions
- 8.Smart contracts
- 9.Stateful Smart Contract
- 10.Stateless Smart Contract
- 11.Full code
- 12.What’s next?
- 13.Resources
- 14.Conclusion
1.Workspace setup and configuration in android studio
To get started, your android studio should be up and running. Then add all necessary dependencies to your app/build.gradle file. To start using the Algorand SDK here are the dependencies you would need.
implementation 'com.algorand:algosdk:1.7.0'
To run the completed code, the Minimum version of Android Studio 4.1 or newer. And Java SDK is 1.8.0 will be required.
Note: Constants.kt
hold the constants for the app.
2.Account creation and Generate Algod Pair
Account Creation
In order to send a transaction, you first need an account on Algorand. Create an account by generating an Algorand public/private key pair and then funding the public address with Algos on your chosen network.
To get the Algorand REST- API you will need to register on purestake or another API services such as AlgoExplorer.io or your own node with an ip address (not localhost)
If you are using the Algorand Purestake API, Once you are done with the registration, you will be provided with your unique API-KEY for making API queries.
Generate Algod pair
When creating a new account you will need to generate a private key and Mnemonic. To do that with the Algorand Java SDK here is the code required;
AccountRepository
suspend fun generateAlgodPair(): Account
AccountRepositoryImpl
override suspend fun generateAlgodPair() : Account {
return Account()
}
AccountViewmodel
fun generateAlgodPair() : LiveData<Account> {
viewModelScope.launch {
_algodPair.value = repository.generateAlgodPair()
}
return algodPair
}
MainActivity
private fun createAccount(){
viewModel.generateAlgodPair().observe(this, {
binding.publicKeyAddress.text = it.address.toString()
binding.newPassphrase.text = it.toMnemonic()
Timber.d("address ${it.address}")
Timber.d("phrase ${it.toMnemonic()}")
})
}
3.Import an existing account
To import an existing Algorand Account, you need to get the Mnemonic from where ever you saved it from the one you generated previously when creating a new account.It is always advisable to save your Mnemonic properly and not to be shared with anyone.If you loose your Mnemonic you will lost access to your account and most likely loose your assets in that account.
Here is what the code looks like
AccountRepository
suspend fun recoverAccount(passPhrase : String): Account
AccountRepositoryImpl
override suspend fun recoverAccount(passPhrase : String): Account {
val account = Account(passPhrase)
Timber.d("Account $account")
return account
}
AccountViewmodel
fun recoverAccount(passsPhrase : String) : LiveData<Account>{
viewModelScope.launch {
_algodPair.value = repository.recoverAccount(passsPhrase)
}
return algodPair
}
MainActivity
private fun recoverAccount(){
viewModel.recoverAccount(SRC_MNEMONIC).observe(this, Observer {
binding.recoveryPublicKeyAddress.text = it.address.toString()
binding.recoveryPassphrase.text = it.toMnemonic()
})
}
4.Funding an account
To fund your Algord test account you can use the Algorand Dispenser
Here is the code to fund wallet from our example
Dashboard
private fun fundAccount() {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Constants.FUND_ACCOUNT))
try {
if (!Constants.FUND_ACCOUNT.startsWith("http://") && !Constants.FUND_ACCOUNT.startsWith(
"https://"
)
)
Constants.FUND_ACCOUNT = "http://" + Constants.FUND_ACCOUNT;
startActivity(browserIntent)
} catch (e: Exception) {
Timber.d("Host not available ${e.message}")
}
}
5.Checking account balance
To check account balance we will need to get the account info. To do this we can use either the Algorand SDK or the Purestake API. For this example we will be making use of the PureStake API.
WalletAccount Model
class WalletAccount(
val account: Account,
@SerializedName("current-round")
val currentRound: Long
)
data class Account(
val address: String,
val amount: Long,
@SerializedName("amount-without-pending-rewards")
val amountWithoutPendingRewards: Long,
@SerializedName("created-at-round")
val createdAtRound: Long,
val deleted: Boolean,
@SerializedName("pending-rewards")
val pendingRewards: Long,
@SerializedName("reward-base")
val rewardBase: Long,
val rewards: Long,
val round: Long,
@SerializedName("sig-type")
val sigType: String,
val status: String
)
AlgorandRESTService - Purestake API endpoint
@Headers(HEADER_VALUE)
@GET("v2/accounts/{account-id}")
suspend fun getAccountByAddress(@Path("account-id") account_id: String?): Response<WalletAccount>
AccountRepository
suspend fun getAccount(address : String) : WalletAccount?
AccountRepositoryImpl
override suspend fun getAccount(address: String): WalletAccount? {
val response = apiService.service.getAccountByAddress(address)
try {
val accountInfo = response.body()
if (response.isSuccessful) {
val account = response.body()?.account
Timber.d("acctResponse $response")
Timber.d("address is $address and account is $account")
Timber.d("account info ${accountInfo.toString()}")
} else {
Timber.d("Error ${response.errorBody()}")
}
}catch (e : Exception){
Timber.d(e)
}
return response.body()
}
AccountViewmodel
fun getAccount(address: String) : LiveData<WalletAccount>{
viewModelScope.launch {
try{
_account.value = repository.getAccount(address)
}catch(e: Exception){e.message}
}
return account
}
Dashboard
private fun getWalletBalance() {
viewModel.getAccount(CREATOR_ADDRESS)
.observe(this, {
try {
binding.balance.text = it.account.amount.toString()
Timber.d("amount ${it.account.amount}")
} catch (e: Exception) {
e.message
}
})
}
6.Sending a payments
To transfer fund using the Algorand SDK, first you need to setup your AlgodClient provide the appropriate values. This is the code required to get that done.
TransactionRepository
suspend fun transferFund(amount: Long, receiverAddress: String)
suspend fun submitTransaction(signedTxn: SignedTransaction): String
suspend fun waitForConfirmation(txID: String)
TransactionRepositoryImpl
private val client: AlgodClient = AlgodClient(
Constants.ALGOD_API_ADDR,
Constants.ALGOD_PORT,
Constants.ALGOD_API_TOKEN,
)
var headers = arrayOf("X-API-Key")
var values = arrayOf(ALGOD_API_TOKEN_KEY)
override suspend fun transferFund(amount: Long, receiverAddress: String) {
val passPhrase = PASSPHRASE
val myAccount = Account(passPhrase)
<!-- Construct the transaction -->
val RECEIVER = RECIEVER
val sender = SENDER
withContext(Dispatchers.IO) {
val resp: Response<TransactionParametersResponse> = client.TransactionParams().execute(
headers,
values
)
try {
if (!resp.isSuccessful) {
Timber.d("message ${resp.message()}")
throw Exception(resp.message())
}
val params: TransactionParametersResponse = resp.body()
?: throw Exception("Params retrieval error")
val txn: Transaction = Transaction.PaymentTransactionBuilder()
.sender(sender)
.amount(amount)
.receiver(Address(receiverAddress))
.suggestedParams(params)
.build()
//Sign the transaction
val signedTxn: SignedTransaction = myAccount.signTransaction(txn)
Timber.d("Signed transaction with txid:: \" + ${signedTxn.transactionID}")
submitTransaction(signedTxn)
} catch (e: Exception) {
Timber.d("Error ${e.message}")
}
}
}
/**
* utility function to wait on a transaction to be confirmed
* the timeout parameter indicates how many rounds do you wish to check pending transactions for
*/
override suspend fun waitForConfirmation(
myclient: AlgodClient?,
txID: String?,
timeout: Int
): PendingTransactionResponse? {
require(!(myclient == null || txID == null || timeout < 0)) { "Bad arguments for waitForConfirmation." }
var resp = myclient.GetStatus().execute(headers, values)
if (!resp.isSuccessful) {
throw java.lang.Exception(resp.message())
}
val nodeStatusResponse = resp.body()
val startRound = nodeStatusResponse.lastRound + 1
var currentRound = startRound
while (currentRound < startRound + timeout) {
// Check the pending transactions
val resp2 = myclient.PendingTransactionInformation(txID).execute(headers, values)
if (resp2.isSuccessful) {
val pendingInfo = resp2.body()
if (pendingInfo != null) {
if (pendingInfo.confirmedRound != null && pendingInfo.confirmedRound > 0) {
// Got the completed Transaction
return pendingInfo
}
if (pendingInfo.poolError != null && pendingInfo.poolError.length > 0) {
// If there was a pool error, then the transaction has been rejected!
throw java.lang.Exception("The transaction has been rejected with a pool error: " + pendingInfo.poolError)
}
}
}
resp = myclient.WaitForBlock(currentRound).execute(headers, values)
if (!resp.isSuccessful) {
throw java.lang.Exception(resp.message())
}
currentRound++
}
throw java.lang.Exception("Transaction not confirmed after $timeout rounds!")
}
override suspend fun submitTransaction(signedTxn: SignedTransaction): String {
val txHeaders: Array<String> = ArrayUtils.add(headers, "Content-Type")
val txValues: Array<String> = ArrayUtils.add(values, "application/x-binary")
var id = ""
try {
// Submit the transaction to the network
val encodedTxBytes: ByteArray = encodeToMsgPack(signedTxn)
val rawtxresponse = client.RawTransaction().rawtxn(encodedTxBytes).execute(
txHeaders,
txValues
)
if (!rawtxresponse.isSuccessful) {
Timber.d("raw ${rawtxresponse.message()}")
throw Exception(rawtxresponse.message())
} else {
id = rawtxresponse.body().txId
Timber.d("Successfully sent tx with ID: $id")
waitForConfirmation(id)
}
}catch (e : Exception){
e.message
}
Timber.d("id is $id")
return id
}
override suspend fun getTransactionsByAddress(address: String): AccountTransactions? {
val response = apiService.service.getAcccountTransactionsByAddress(address)
try {
if (response.isSuccessful){
response.body()
Timber.d("trans $response")
Timber.d("trans ${response.body()}")
}else{
response.errorBody()
}
}catch (t : Throwable){
t.message
}
return response.body()
}
TransactionViewmodel
private val _showMessage : MutableLiveData<String> = MutableLiveData()
val showMessage : LiveData<String> get() = _showMessage
private val _showProgress : MutableLiveData<Boolean> = MutableLiveData()
val showProgress : LiveData<Boolean> get() = _showProgress
fun transferFund(amount: Long, receiverAddress : String) {
try {
viewModelScope.launch {
_showProgress.value = true
repository.transferFund(amount, receiverAddress)
_showProgress.value = false
}
}catch (e: Exception){
_showMessage.postValue("Unable to connect to server!")
_showProgress.postValue(false)
}
}
TransferFundDialog
Here, I created a dialog, where the user can enter the amount and the wallet address to do transfers. Once the transfer button is clicked this code will be initialized.
private fun validateTransfer() {
val amount = binding.amount.text.toString().trim()
val receiverAddress : String = binding.address.text.toString()
if (amount.isEmpty() && receiverAddress.isEmpty()) {
Toast.makeText(context, "Fields must be filled", Toast.LENGTH_LONG).show()
return
} else {
transactionViewModel.transferFund(amount.toLong(), receiverAddress)
dismiss()
}
}
7.Display list of transactions
To get list of transactions we will use the Purestake API endpoint.
AlgorandRESTService - Purestake API endpoint
@Headers(HEADER_VALUE)
@GET("v2/accounts/{account-id}/transactions")
suspend fun getAcccountTransactionsByAddress(@Path("account-id") account_id : String?): Response<AccountTransactions>
TransactionRepository
suspend fun getTransactionsByAddress(address : String) : AccountTransactions?
TransactionRepositoryImpl
override suspend fun getTransactionsByAddress(address: String): AccountTransactions? {
val response = apiService.service.getAcccountTransactionsByAddress(address)
try {
if (response.isSuccessful){
response.body()
Timber.d("trans $response")
Timber.d("trans ${response.body()}")
}else{
response.errorBody()
}
}catch (t : Throwable){
t.message
}
return response.body()
}
TransactionViewmodel
fun getTransactions(address: String) : LiveData<AccountTransactions>{
try {
viewModelScope.launch {
_showProgress.value = true
_transactions.value = repository.getTransactionsByAddress(address)
_showProgress.value = false
}
}catch (e : Throwable){
_showMessage.postValue("Unable to connect to server!")
_showProgress.postValue(false)
}
return transactions
}
Dashboard
You will create a Recyclerview Adapter to display the list of transactions
private fun setData() {
transactionViewModel.getTransactions(CREATOR_ADDRESS).observe(this, Observer {
transactions = it.transactions
Timber.d("Transactions ${transactions.size}")
if (transactions.isNotEmpty()) {
binding.recyclerview.visibility = View.VISIBLE
myAdapter.setData(transactions)
binding.emptyState.visibility = View.GONE
} else {
binding.recyclerview.visibility = View.GONE
binding.emptyState.visibility = View.VISIBLE
}
})
}
Fig 0-2
8.Smart contracts
Algorand Smart Contracts (ASC1) are small programs that serve various functions on the blockchain and operate on layer-1. Smart contracts are separated into two main categories, stateful and stateless.
Both types of contracts are written in the Transaction Execution Approval Language (TEAL), which is an assembly-like language that is interpreted by the Algorand Virtual Machine (AVM) running within an Algorand node.
9.Stateful Smart Contract
Stateful smart contracts are contracts that live on the chain and are used to keep track of some form of global and/or local state for the contract. For example, a voting application may be implemented as a stateful smart contract, where the list of candidates and their current vote tallies would be considered global state values. When an account casts a vote, their local account may be marked by the stateful smart contract with a boolean indicating that the account has already voted.
The Lifecycle of a Stateful Smart Contract
-
NoOp - Generic application calls to execute the ApprovalProgram
-
OptIn - Accounts use this transaction to opt into the smart contract to participate (local storage usage).
-
DeleteApplication - Transaction to delete the application.
-
UpdateApplication - Transaction to update TEAL Programs for a contract.
-
CloseOut - Accounts use this transaction to close out their participation in the contract. This call can fail based on the TEAL logic, preventing the account from removing the contract from its balance record.
-
ClearState - Similar to CloseOut, but the transaction will always clear a contract from the account’s balance record whether the program succeeds or fails.
-
The ClearStateProgram handles the ClearState transaction and the ApprovalProgram handles all other ApplicationCall transactions
The above lifecycle can be seen in the code implementation below;
StatefulContractRepository
interface StatefulContractRepository {
suspend fun statefulSmartContract()
suspend fun compileProgram(client: AlgodClient, programSource: ByteArray?): String?
suspend fun waitForConfirmation(txID: String?)
suspend fun createApp(
client: AlgodClient,
creator: Account,
approvalProgramSource: TEALProgram?,
clearProgramSource: TEALProgram?,
globalInts: Int,
globalBytes: Int,
localInts: Int,
localBytes: Int
): Long?
suspend fun optInApp(client: AlgodClient, account: Account, appId: Long?) : Long
suspend fun callApp(
client: AlgodClient,
account: Account,
appId: Long?,
appArgs: List<ByteArray>?
) : Long
suspend fun readLocalState(client: AlgodClient, account: Account, appId: Long?) : String
suspend fun readGlobalState(client: AlgodClient, account: Account, appId: Long?) : String
suspend fun updateApp(
client: AlgodClient,
creator: Account,
appId: Long?,
approvalProgramSource: TEALProgram?,
clearProgramSource: TEALProgram?
) : PendingTransactionResponse
suspend fun closeOutApp(client: AlgodClient, userAccount: Account, appId: Long?) : PendingTransactionResponse
suspend fun clearApp(client: AlgodClient, userAccount: Account, appId: Long?) : PendingTransactionResponse
suspend fun deleteApp(client: AlgodClient, creatorAccount: Account, appId: Long?) : PendingTransactionResponse
}
StatefulContractRepositoryImpl
package com.africinnovate.algorandandroidkotlin.repositoryImpl
import android.os.Build
import com.africinnovate.algorandandroidkotlin.ClientService.APIService
import com.africinnovate.algorandandroidkotlin.repository.StatefulContractRepository
import com.africinnovate.algorandandroidkotlin.utils.Constants
import com.africinnovate.algorandandroidkotlin.utils.Constants.ALGOD_API_ADDR
import com.africinnovate.algorandandroidkotlin.utils.Constants.ALGOD_API_TOKEN
import com.africinnovate.algorandandroidkotlin.utils.Constants.ALGOD_PORT
import com.africinnovate.algorandandroidkotlin.utils.Constants.CREATOR_MNEMONIC
import com.africinnovate.algorandandroidkotlin.utils.Constants.USER_MNEMONIC
import com.algorand.algosdk.account.Account
import com.algorand.algosdk.crypto.Address
import com.algorand.algosdk.crypto.TEALProgram
import com.algorand.algosdk.logic.StateSchema
import com.algorand.algosdk.transaction.SignedTransaction
import com.algorand.algosdk.transaction.Transaction
import com.algorand.algosdk.transaction.Transaction.ApplicationOptInTransactionBuilder
import com.algorand.algosdk.util.Encoder.encodeToMsgPack
import com.algorand.algosdk.v2.client.common.AlgodClient
import com.algorand.algosdk.v2.client.common.Response
import com.algorand.algosdk.v2.client.model.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.apache.commons.lang3.ArrayUtils
import timber.log.Timber
import java.nio.file.Files
import java.nio.file.Paths
import java.sql.Date
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
import com.algorand.algosdk.v2.client.model.TransactionParametersResponse
class StatefulContractRepositoryImpl @Inject constructor(private val apiService: APIService) :
StatefulContractRepository {
private var client: AlgodClient = AlgodClient(
ALGOD_API_ADDR,
ALGOD_PORT,
ALGOD_API_TOKEN,
)
// utility function to connect to a node
private fun connectToNetwork(): AlgodClient {
return client
}
var headers = arrayOf("X-API-Key")
var values = arrayOf(Constants.ALGOD_API_TOKEN_KEY)
val txHeaders: Array<String> = ArrayUtils.add(headers, "Content-Type")
val txValues: Array<String> = ArrayUtils.add(values, "application/x-binary")
// user declared account mnemonics
val creatorMnemonic = CREATOR_MNEMONIC
val userMnemonic = USER_MNEMONIC
// declare application state storage (immutable)
var localInts = 1
var localBytes = 1
var globalInts = 1
var globalBytes = 0
var clearProgramSource = """
#pragma version 4
int 1
""".trimIndent()
// get account from mnemonic
var creatorAccount: Account = Account(creatorMnemonic)
var sender: Address = creatorAccount.address
// get node suggested parameters
var params = client.TransactionParams().execute().body()
// helper function to compile program source
override suspend fun compileProgram(client: AlgodClient, programSource: ByteArray?): String? {
lateinit var compileResponse: Response<CompileResponse>
try {
compileResponse = client.TealCompile().source(programSource).execute(
headers,
values
)
} catch (e: Exception) {
e.printStackTrace()
}
Timber.d("compileResponse ${compileResponse.body().result}")
return compileResponse.body().result
}
// utility function to wait on a transaction to be confirmed
override suspend fun waitForConfirmation(txID: String?) {
withContext(Dispatchers.IO) {
// if (client == null) client = connectToNetwork()
var lastRound: Long = client.GetStatus().execute(headers, values).body().lastRound
while (true) {
try {
// Check the pending transactions
val pendingInfo: Response<PendingTransactionResponse> =
client.PendingTransactionInformation(txID).execute(headers, values)
if (pendingInfo.body().confirmedRound != null && pendingInfo.body().confirmedRound > 0) {
// Got the completed Transaction
Timber.d("Transaction $txID + confirmed in round ${pendingInfo.body().confirmedRound}")
break
}
lastRound++
client.WaitForBlock(lastRound).execute(headers, values)
} catch (e: Exception) {
throw e
}
}
}
}
override suspend fun createApp(
client: AlgodClient,
creator: Account,
approvalProgramSource: TEALProgram?,
clearProgramSource: TEALProgram?,
globalInts: Int,
globalBytes: Int,
localInts: Int,
localBytes: Int
): Long? {
// define sender as creator
val sender: Address = creator.address
// get node suggested parameters
val params: TransactionParametersResponse? =
client.TransactionParams().execute(headers, values).body()
// create unsigned transaction
val txn: Transaction = Transaction.ApplicationCreateTransactionBuilder()
.sender(sender)
.suggestedParams(params)
.approvalProgram(approvalProgramSource)
.clearStateProgram(clearProgramSource)
.globalStateSchema(
StateSchema(
globalInts,
globalBytes
)
)
.localStateSchema(
StateSchema(
localInts,
localBytes
)
)
.build()
// sign transaction
val signedTxn: SignedTransaction = creator.signTransaction(txn)
Timber.d("Signed transaction with txid: \" + ${signedTxn.transactionID}")
var id = ""
try {
// Submit the transaction to the network
val encodedTxBytes: ByteArray = encodeToMsgPack(signedTxn)
val rawtxresponse = client.RawTransaction().rawtxn(encodedTxBytes).execute(
txHeaders,
txValues
)
if (!rawtxresponse.isSuccessful) {
Timber.d("raw ${rawtxresponse.message()}")
throw Exception(rawtxresponse.message())
} else {
id = rawtxresponse.body().txId
Timber.d("Successfully sent tx with ID: $id")
waitForConfirmation(id)
}
}catch (e: Exception){
e.message
}
// display results
val pTrx: PendingTransactionResponse? =
client.PendingTransactionInformation(id).execute(headers, values).body()
val appId = pTrx?.applicationIndex
Timber.d("Created new app-id: $appId")
return appId
}
override suspend fun optInApp(client: AlgodClient, account: Account, appId: Long?) : Long {
// declare sender
val sender: Address = account.address
println("OptIn from account: $sender")
// get node suggested parameters
val params: TransactionParametersResponse =
client.TransactionParams().execute(headers, values).body()
// create unsigned transaction
val txn: Transaction = ApplicationOptInTransactionBuilder()
.sender(sender)
.suggestedParams(params)
.applicationId(appId)
.build()
// sign transaction
val signedTxn: SignedTransaction = account.signTransaction(txn)
// send to network
val encodedTxBytes: ByteArray = encodeToMsgPack(signedTxn)
val id: String =
client.RawTransaction().rawtxn(encodedTxBytes).execute(txHeaders, txValues).body().txId
// await confirmation
waitForConfirmation(id)
// display results
val pTrx: PendingTransactionResponse =
client.PendingTransactionInformation(id).execute(headers, values).body()
Timber.d("OptIn to app-id: %s", pTrx.txn.tx.applicationId)
return pTrx.txn.tx.applicationId
}
override suspend fun callApp(
client: AlgodClient,
account: Account,
appId: Long?,
appArgs: List<ByteArray>?
) : Long{
// declare sender
val sender: Address = account.address
println("Call from account: $sender")
val params: TransactionParametersResponse =
client.TransactionParams().execute(headers, values).body()
// create unsigned transaction
val txn: Transaction = Transaction.ApplicationCallTransactionBuilder()
.sender(sender)
.suggestedParams(params)
.applicationId(appId)
.args(appArgs)
.build()
// sign transaction
val signedTxn: SignedTransaction = account.signTransaction(txn)
// save signed transaction to a file
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Files.write(Paths.get("callArgs.stxn"), encodeToMsgPack(signedTxn))
// }
// send to network
/* val encodedTxBytes: ByteArray = encodeToMsgPack(signedTxn)
val id: String =
client.RawTransaction().rawtxn(encodedTxBytes).execute(headers, values).body().txId
// await confirmation
waitForConfirmation(id)*/
var id = ""
try {
// Submit the transaction to the network
val encodedTxBytes: ByteArray = encodeToMsgPack(signedTxn)
val rawtxresponse = client.RawTransaction().rawtxn(encodedTxBytes).execute(
txHeaders,
txValues
)
if (!rawtxresponse.isSuccessful) {
Timber.d("raw ${rawtxresponse.message()}")
throw Exception(rawtxresponse.message())
} else {
id = rawtxresponse.body().txId
Timber.d("Successfully sent tx with ID: $id")
waitForConfirmation(id)
}
}catch (e: Exception){
e.message
}
// display results
val pTrx: PendingTransactionResponse =
client.PendingTransactionInformation(id).execute(headers, values).body()
Timber.d("Called app-id: %s", pTrx.txn.tx.applicationId)
if (pTrx.globalStateDelta != null) {
Timber.d(" Global state: \" + pTrx.globalStateDelta.toString()")
}
if (pTrx.localStateDelta != null) {
Timber.d("Local state: \" + pTrx.localStateDelta.toString()")
}
return pTrx.txn.tx.applicationId
}
override suspend fun readLocalState(client: AlgodClient, account: Account, appId: Long?) : String{
val acctResponse: Response<com.algorand.algosdk.v2.client.model.Account> =
client.AccountInformation(account.address).execute(headers, values)
val applicationLocalState: List<ApplicationLocalState> =
acctResponse.body().appsLocalState
for (i in applicationLocalState.indices) {
if (applicationLocalState[i].id == appId) {
Timber.d(
"User's application local state: %s",
applicationLocalState[i].keyValue.toString()
)
}
}
return applicationLocalState.toString()
}
override suspend fun readGlobalState(client: AlgodClient, account: Account, appId: Long?) : String{
val acctResponse: Response<com.algorand.algosdk.v2.client.model.Account> =
client.AccountInformation(account.address).execute(headers, values)
val createdApplications: List<Application> = acctResponse.body().createdApps
for (i in createdApplications.indices) {
if (createdApplications[i].id.equals(appId)) {
Timber.d("Application global state: ${createdApplications[i].params.globalState.toString()}")
}
}
return createdApplications.toString()
}
override suspend fun updateApp(
client: AlgodClient,
creator: Account,
appId: Long?,
approvalProgramSource: TEALProgram?,
clearProgramSource: TEALProgram?
) : PendingTransactionResponse{
// define sender as creator
val sender: Address = creator.address
// get node suggested parameters
val params: TransactionParametersResponse =
client.TransactionParams().execute(headers, values).body()
// create unsigned transaction
val txn: Transaction = Transaction.ApplicationUpdateTransactionBuilder()
.sender(sender)
.suggestedParams(params)
.applicationId(appId)
.approvalProgram(approvalProgramSource)
.clearStateProgram(clearProgramSource)
.build()
// sign transaction
val signedTxn: SignedTransaction = creator.signTransaction(txn)
// send to network
val encodedTxBytes: ByteArray = encodeToMsgPack(signedTxn)
val id: String =
client.RawTransaction().rawtxn(encodedTxBytes).execute(txHeaders, txValues).body().txId
// await confirmation
waitForConfirmation(id)
// display results
val pTrx: PendingTransactionResponse =
client.PendingTransactionInformation(id).execute(headers, values).body()
Timber.d("Updated new app-id: $appId and $pTrx")
return pTrx
}
override suspend fun closeOutApp(client: AlgodClient, userAccount: Account, appId: Long?) : PendingTransactionResponse {
// define sender as creator
val sender: Address = userAccount.address
// get node suggested parameters
val params: TransactionParametersResponse =
client.TransactionParams().execute(headers, values).body()
// create unsigned transaction
val txn: Transaction = Transaction.ApplicationCloseTransactionBuilder()
.sender(sender)
.suggestedParams(params)
.applicationId(appId)
.build()
// sign transaction
val signedTxn: SignedTransaction = userAccount.signTransaction(txn)
// send to network
val encodedTxBytes: ByteArray = encodeToMsgPack(signedTxn)
val id: String =
client.RawTransaction().rawtxn(encodedTxBytes).execute(txHeaders, txValues).body().txId
// await confirmation
waitForConfirmation(id)
// display results
val pTrx: PendingTransactionResponse =
client.PendingTransactionInformation(id).execute(headers, values).body()
Timber.d("Closed out from app-id: $appId and $pTrx")
return pTrx
}
override suspend fun clearApp(client: AlgodClient, userAccount: Account, appId: Long?) : PendingTransactionResponse{
// define sender as creator
val sender: Address = userAccount.address
// get node suggested parameters
val params: TransactionParametersResponse = client.TransactionParams().execute(
headers,
values
).body()
// create unsigned transaction
val txn: Transaction = Transaction.ApplicationClearTransactionBuilder()
.sender(sender)
.suggestedParams(params)
.applicationId(appId)
.build()
// sign transaction
val signedTxn: SignedTransaction = userAccount.signTransaction(txn)
// send to network
val encodedTxBytes: ByteArray = encodeToMsgPack(signedTxn)
val id: String =
client.RawTransaction().rawtxn(encodedTxBytes).execute(txHeaders, txValues).body().txId
// await confirmation
waitForConfirmation(id)
// display results
val pTrx: PendingTransactionResponse =
client.PendingTransactionInformation(id).execute(headers, values).body()
Timber.d("Cleared local state for app-id: $appId and $pTrx")
return pTrx
}
override suspend fun deleteApp(client: AlgodClient, creatorAccount: Account, appId: Long?) : PendingTransactionResponse {
// define sender as creator
val sender: Address = creatorAccount.address
// get node suggested parameters
val params: TransactionParametersResponse = client.TransactionParams().execute(
headers,
values
).body()
// create unsigned transaction
val txn: Transaction = Transaction.ApplicationDeleteTransactionBuilder()
.sender(sender)
.suggestedParams(params)
.applicationId(appId)
.build()
// sign transaction
val signedTxn: SignedTransaction = creatorAccount.signTransaction(txn)
// send to network
val encodedTxBytes: ByteArray = encodeToMsgPack(signedTxn)
val id: String =
client.RawTransaction().rawtxn(encodedTxBytes).execute(txHeaders, txValues).body().txId
// await confirmation
waitForConfirmation(id)
// display results
val pTrx: PendingTransactionResponse =
client.PendingTransactionInformation(id).execute(headers, values).body()
Timber.d("Deleted app-id: $appId and $pTrx")
return pTrx
}
override suspend fun statefulSmartContract() {
// user declared account mnemonics
val creatorMnemonic = CREATOR_MNEMONIC
val userMnemonic = USER_MNEMONIC
// declare application state storage (immutable)
val localInts = 1
val localBytes = 1
val globalInts = 1
val globalBytes = 0
// user declared approval program (initial)
val approvalProgramSourceInitial = """
#pragma version 2
///// Handle each possible OnCompletion type. We don't have to worry about
//// handling ClearState, because the ClearStateProgram will execute in that
//// case, not the ApprovalProgram.
txn OnCompletion
int NoOp
==
bnz handle_noop
txn OnCompletion
int OptIn
==
bnz handle_optin
txn OnCompletion
int CloseOut
==
bnz handle_closeout
txn OnCompletion
int UpdateApplication
==
bnz handle_updateapp
txn OnCompletion
int DeleteApplication
==
bnz handle_deleteapp
//// Unexpected OnCompletion value. Should be unreachable.
err
handle_noop:
//// Handle NoOp
//// Check for creator
addr 5XWY6RBNYHCSY2HK5HCTO62DUJJ4PT3G4L77FQEBUKE6ZYRGQAFTLZSQQ4
txn Sender
==
bnz handle_optin
//// read global state
byte "counter"
dup
app_global_get
//// increment the value
int 1
+
//// store to scratch space
dup
store 0
//// update global state
app_global_put
//// read local state for sender
int 0
byte "counter"
app_local_get
//// increment the value
int 1
+
store 1
//// update local state for sender
int 0
byte "counter"
load 1
app_local_put
//// load return value as approval
load 0
return
handle_optin:
//// Handle OptIn
//// approval
int 1
return
handle_closeout:
//// Handle CloseOut
////approval
int 1
return
handle_deleteapp:
//// Check for creator
addr 5XWY6RBNYHCSY2HK5HCTO62DUJJ4PT3G4L77FQEBUKE6ZYRGQAFTLZSQQ4
txn Sender
==
return
handle_updateapp:
//// Check for creator
addr 5XWY6RBNYHCSY2HK5HCTO62DUJJ4PT3G4L77FQEBUKE6ZYRGQAFTLZSQQ4
txn Sender
==
return
""".trimIndent()
// user declared approval program (refactored)
val approvalProgramSourceRefactored = """
#pragma version 2
//// Handle each possible OnCompletion type. We don't have to worry about
//// handling ClearState, because the ClearStateProgram will execute in that
//// case, not the ApprovalProgram.
txn OnCompletion
int NoOp
==
bnz handle_noop
txn OnCompletion
int OptIn
==
bnz handle_optin
txn OnCompletion
int CloseOut
==
bnz handle_closeout
txn OnCompletion
int UpdateApplication
==
bnz handle_updateapp
txn OnCompletion
int DeleteApplication
==
bnz handle_deleteapp
//// Unexpected OnCompletion value. Should be unreachable.
err
handle_noop:
//// Handle NoOp
//// Check for creator
addr 5XWY6RBNYHCSY2HK5HCTO62DUJJ4PT3G4L77FQEBUKE6ZYRGQAFTLZSQQ4
txn Sender
==
bnz handle_optin
//// read global state
byte "counter"
dup
app_global_get
//// increment the value
int 1
+
//// store to scratch space
dup
store 0
//// update global state
app_global_put
//// read local state for sender
int 0
byte "counter"
app_local_get
//// increment the value
int 1
+
store 1
//// update local state for sender
//// update "counter"
int 0
byte "counter"
load 1
app_local_put
//// update "timestamp"
int 0
byte "timestamp"
txn ApplicationArgs 0
app_local_put
//// load return value as approval
load 0
return
handle_optin:
//// Handle OptIn
//// approval
int 1
return
handle_closeout:
//// Handle CloseOut
////approval
int 1
return
handle_deleteapp:
//// Check for creator
addr 5XWY6RBNYHCSY2HK5HCTO62DUJJ4PT3G4L77FQEBUKE6ZYRGQAFTLZSQQ4
txn Sender
==
return
handle_updateapp:
//// Check for creator
addr 5XWY6RBNYHCSY2HK5HCTO62DUJJ4PT3G4L77FQEBUKE6ZYRGQAFTLZSQQ4
txn Sender
==
return
""".trimIndent()
// declare clear state program source
val clearProgramSource = """
#pragma version 2
int 1
""".trimIndent()
withContext(Dispatchers.IO) {
try {
// Create an algod client
// if (client == null) client = connectToNetwork()
// get accounts from mnemonic
val creatorAccount = Account(creatorMnemonic)
val userAccount = Account(userMnemonic)
// compile programs
var approvalProgram = compileProgram(
client,
approvalProgramSourceInitial.toByteArray(charset("UTF-8"))
)
val clearProgram =
compileProgram(client, clearProgramSource.toByteArray(charset("UTF-8")))
// create new application
val appId = createApp(
client,
creatorAccount,
TEALProgram(approvalProgram),
TEALProgram(clearProgram),
globalInts,
globalBytes,
localInts,
localBytes
)
// opt-in to application
optInApp(client, userAccount, appId)
// call application without arguments
callApp(client, userAccount, appId, null)
// read local state of application from user account
readLocalState(client, userAccount, appId)
// read global state of application
readGlobalState(client, creatorAccount, appId)
// update application
approvalProgram = compileProgram(
client,
approvalProgramSourceRefactored.toByteArray(charset("UTF-8"))
)
updateApp(
client,
creatorAccount,
appId,
TEALProgram(approvalProgram),
TEALProgram(clearProgram)
)
// call application with arguments
val formatter = SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss")
val date = Date(System.currentTimeMillis())
Timber.d("${formatter.format(date)}")
val appArgs: MutableList<ByteArray> = ArrayList()
appArgs.add(formatter.format(date).toString().toByteArray(charset("UTF8")))
callApp(client, userAccount, appId, appArgs)
// read local state of application from user account
readLocalState(client, userAccount, appId)
// close-out from application
closeOutApp(client, userAccount, appId)
// opt-in again to application
optInApp(client, userAccount, appId)
// call application with arguments
callApp(client, userAccount, appId, appArgs)
// read local state of application from user account
readLocalState(client, userAccount, appId)
// delete application
deleteApp(client, creatorAccount, appId)
// clear application from user account
clearApp(client, userAccount, appId)
} catch (e: Exception) {
System.err.println("Exception raised: " + e.message)
}
}
}
}
StatefulSmartContractViewModel
fun statefulSmartContract(){
viewModelScope.launch {
repository.statefulSmartContract()
}
}
fun compileProgram(client: AlgodClient, programSource: ByteArray?) : LiveData<String> {
viewModelScope.launch {
try {
_response.value = repository.compileProgram(client, programSource)
} catch (e: Exception) {
Timber.e(e)
}
}
return response
}
fun createApp(client: AlgodClient,
creator: Account,
approvalProgramSource: TEALProgram?,
clearProgramSource: TEALProgram?,
globalInts: Int,
globalBytes: Int,
localInts: Int,
localBytes: Int
): LiveData<Long>?{
viewModelScope.launch {
try {
_appid.value = repository.createApp(client,creator, approvalProgramSource, clearProgramSource,globalInts,globalBytes,localInts,localBytes)
} catch (e: Exception) {
Timber.e(e)
}
}
return appid
}
fun optInApp(client: AlgodClient, userAccount: Account, apppId: Long?) : LiveData<Long>{
viewModelScope.launch {
try {
_appid.value = repository.optInApp(client,userAccount, apppId)
} catch (e: Exception) {
Timber.e(e)
}
}
return appid
}
fun callApp(client: AlgodClient,
userAccount: Account,
appId: Long?,
appArgs: List<ByteArray>?) : LiveData<Long>{
viewModelScope.launch {
try {
_appid.value = repository.callApp(client,userAccount, appId, appArgs)
} catch (e: Exception) {
Timber.e(e)
}
}
return appid
}
fun readLocalState(client: AlgodClient, userAccount: Account, appId: Long?) : LiveData<String>{
viewModelScope.launch {
try {
_response.value = repository.readLocalState(client,userAccount, appId)
} catch (e: Exception) {
Timber.e(e)
}
}
return response
}
fun readGlobalState(client: AlgodClient, userAccount: Account, appId: Long?) : LiveData<String>{
viewModelScope.launch {
try {
_response.value = repository.readGlobalState(client,userAccount, appId)
} catch (e: Exception) {
Timber.e(e)
}
}
return response
}
fun updateApp(client: AlgodClient,
creator: Account,
appId: Long?,
approvalProgramSource: TEALProgram?,
clearProgramSource: TEALProgram?) : LiveData<PendingTransactionResponse>{
viewModelScope.launch {
try {
_pTrx.value = repository.updateApp(client,creator, appId, approvalProgramSource, clearProgramSource)
} catch (e: Exception) {
Timber.e(e)
}
}
return pTrx
}
fun closeOutApp(client: AlgodClient, userAccount: Account, appId: Long?) : LiveData<PendingTransactionResponse>{
viewModelScope.launch {
try {
_pTrx.value = repository.closeOutApp(client,userAccount, appId)
} catch (e: Exception) {
Timber.e(e)
}
}
return pTrx
}
fun clearApp(client: AlgodClient, userAccount: Account, appId: Long?) : LiveData<PendingTransactionResponse>{
viewModelScope.launch {
try {
_pTrx.value = repository.clearApp(client,userAccount, appId)
} catch (e: Exception) {
Timber.e(e)
}
}
return pTrx
}
fun deleteApp(client: AlgodClient, userAccount: Account, appId: Long?) : LiveData<PendingTransactionResponse>{
viewModelScope.launch {
try {
_pTrx.value = repository.deleteApp(client,userAccount, appId)
} catch (e: Exception) {
Timber.e(e)
}
}
return pTrx
}
MainActivity
On StatefulSmartContract button clicked this method will be called and results will be displayed on the console
binding.stflContract.setOnClickListener {
val intent = Intent(this, StatefulActivity::class.java)
startActivity(intent)
}
StatefulActivity
@AndroidEntryPoint
class StatefulActivity : AppCompatActivity() {
private val viewModel: StatefulSmartContractViewModel by viewModels()
private lateinit var binding: ActivityStatefulBinding
private var client: AlgodClient = AlgodClient(
Constants.ALGOD_API_ADDR,
Constants.ALGOD_PORT,
Constants.ALGOD_API_TOKEN,
)
// compile programs
var approvalProgram: String = ""
var approvalProgram2: String = ""
var clearProgram: String = ""
var appId: Long = 0L
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_stateful)
binding = DataBindingUtil.setContentView(this, R.layout.activity_stateful)
binding.compile.setOnClickListener {
viewModel.compileProgram(
client,
approvalProgramSourceInitial.toByteArray(charset("UTF-8"))
).observe(this, {
binding.stflOutput.text = it
})
}
viewModel.compileProgram(
client,
approvalProgramSourceRefactored.toByteArray(charset("UTF-8"))
).observe(this, {
approvalProgram = it
})
viewModel.compileProgram(
client,
approvalProgramSourceInitial.toByteArray(charset("UTF-8"))
).observe(this, {
approvalProgram2 = it
})
viewModel.compileProgram(
client,
clearProgramSource.toByteArray(charset("UTF-8"))
).observe(this, {
clearProgram = it
})
binding.createApp.setOnClickListener {
createApp()
}
binding.optInApp.setOnClickListener { optionApp() }
binding.callApp.setOnClickListener { callApp() }
binding.readLocalState.setOnClickListener { readLocalState() }
binding.readGlobalState.setOnClickListener { readGlobalState() }
binding.updateApp.setOnClickListener { updateApp() }
binding.closeOutApp.setOnClickListener { closeOutApp() }
binding.clearApp.setOnClickListener { clearApp() }
binding.deleteApp.setOnClickListener { deleteApp() }
binding.stateful.setOnClickListener {
viewModel.statefulSmartContract()
}
}
private fun createApp(){
val approve = intent.getStringExtra("approval")
val clear = intent.getStringExtra("clear")
Timber.d("program1 $approve")
Timber.d("clear1 $clear")
viewModel.createApp(
client, creatorAccount, TEALProgram(approvalProgram),
TEALProgram(clearProgram),
globalInts,
globalBytes,
localInts,
localBytes
)?.observe(this, {
appId = it
Timber.d("appId $appId")
binding.stflOutput.text = "Created app id: $it"
})
}
private fun optionApp() {
viewModel.optInApp(client, userAccount, appId).observe(this, { appId ->
binding.stflOutput.text = "OptIn from account: ${userAccount.address}\n OptIn to app-id: $appId"
})
}
private fun callApp() {
viewModel.callApp(client, userAccount, appId, null).observe(this, { appId ->
binding.stflOutput.text = " Call from account: ${userAccount.address}\n Called app-id: $appId"
})
}
private fun readLocalState() {
viewModel.readLocalState(client, userAccount, appId).observe(this, { appId ->
binding.stflOutput.text = " User's Application Local State: $appId"
})
}
private fun readGlobalState() {
viewModel.readGlobalState(client, userAccount, appId).observe(this, { appId ->
binding.stflOutput.text = "Application Global State: $appId"
})
}
private fun updateApp() {
viewModel.updateApp(
client, creatorAccount, appId, TEALProgram(approvalProgram2),
TEALProgram(clearProgram)
).observe(this, { ptr ->
binding.stflOutput.text = "Updated new app-id: $appId and $ptr"
})
}
private fun closeOutApp(){
viewModel.closeOutApp(
client, userAccount, appId
).observe(this, { ptr ->
binding.stflOutput.text = "Closed out from app-id: $appId and $ptr"
})
}
private fun clearApp(){
viewModel.clearApp(
client, userAccount, appId
).observe(this, { ptr ->
binding.stflOutput.text = "Cleared local state for app-id: $appId and ${ptr}"
})
}
private fun deleteApp(){
viewModel.deleteApp(
client, creatorAccount, appId
).observe(this, { ptr ->
binding.stflOutput.text = "Deleted app-id: $appId and $ptr"
})
}
companion object {
// user declared account mnemonics
val creatorMnemonic = Constants.CREATOR_MNEMONIC
val userMnemonic = Constants.USER_MNEMONIC
// get accounts from mnemonic
val creatorAccount = Account(creatorMnemonic)
val userAccount = Account(userMnemonic)
// declare application state storage (immutable)
val localInts = 1
val localBytes = 1
val globalInts = 1
val globalBytes = 0
// user declared approval program (initial)
val approvalProgramSourceInitial = """
#pragma version 2
///// Handle each possible OnCompletion type. We don't have to worry about
//// handling ClearState, because the ClearStateProgram will execute in that
//// case, not the ApprovalProgram.
txn OnCompletion
int NoOp
==
bnz handle_noop
txn OnCompletion
int OptIn
==
bnz handle_optin
txn OnCompletion
int CloseOut
==
bnz handle_closeout
txn OnCompletion
int UpdateApplication
==
bnz handle_updateapp
txn OnCompletion
int DeleteApplication
==
bnz handle_deleteapp
//// Unexpected OnCompletion value. Should be unreachable.
err
handle_noop:
//// Handle NoOp
//// Check for creator
addr FR23WI5ZTTRNYTXHA73GIJNS6BDXR3PZA6WETQF7IO6YBBYBS27TXUDNPI
txn Sender
==
bnz handle_optin
//// read global state
byte "counter"
dup
app_global_get
//// increment the value
int 1
+
//// store to scratch space
dup
store 0
//// update global state
app_global_put
//// read local state for sender
int 0
byte "counter"
app_local_get
//// increment the value
int 1
+
store 1
//// update local state for sender
int 0
byte "counter"
load 1
app_local_put
//// load return value as approval
load 0
return
handle_optin:
//// Handle OptIn
//// approval
int 1
return
handle_closeout:
//// Handle CloseOut
////approval
int 1
return
handle_deleteapp:
//// Check for creator
addr FR23WI5ZTTRNYTXHA73GIJNS6BDXR3PZA6WETQF7IO6YBBYBS27TXUDNPI
txn Sender
==
return
handle_updateapp:
//// Check for creator
addr FR23WI5ZTTRNYTXHA73GIJNS6BDXR3PZA6WETQF7IO6YBBYBS27TXUDNPI
txn Sender
==
return
""".trimIndent()
// user declared approval program (refactored)
val approvalProgramSourceRefactored = """
#pragma version 2
//// Handle each possible OnCompletion type. We don't have to worry about
//// handling ClearState, because the ClearStateProgram will execute in that
//// case, not the ApprovalProgram.
txn OnCompletion
int NoOp
==
bnz handle_noop
txn OnCompletion
int OptIn
==
bnz handle_optin
txn OnCompletion
int CloseOut
==
bnz handle_closeout
txn OnCompletion
int UpdateApplication
==
bnz handle_updateapp
txn OnCompletion
int DeleteApplication
==
bnz handle_deleteapp
//// Unexpected OnCompletion value. Should be unreachable.
err
handle_noop:
//// Handle NoOp
//// Check for creator
addr FR23WI5ZTTRNYTXHA73GIJNS6BDXR3PZA6WETQF7IO6YBBYBS27TXUDNPI
txn Sender
==
bnz handle_optin
//// read global state
byte "counter"
dup
app_global_get
//// increment the value
int 1
+
//// store to scratch space
dup
store 0
//// update global state
app_global_put
//// read local state for sender
int 0
byte "counter"
app_local_get
//// increment the value
int 1
+
store 1
//// update local state for sender
//// update "counter"
int 0
byte "counter"
load 1
app_local_put
//// update "timestamp"
int 0
byte "timestamp"
txn ApplicationArgs 0
app_local_put
//// load return value as approval
load 0
return
handle_optin:
//// Handle OptIn
//// approval
int 1
return
handle_closeout:
//// Handle CloseOut
////approval
int 1
return
handle_deleteapp:
//// Check for creator
addr FR23WI5ZTTRNYTXHA73GIJNS6BDXR3PZA6WETQF7IO6YBBYBS27TXUDNPI
txn Sender
==
return
handle_updateapp:
//// Check for creator
addr FR23WI5ZTTRNYTXHA73GIJNS6BDXR3PZA6WETQF7IO6YBBYBS27TXUDNPI
txn Sender
==
return
""".trimIndent()
// declare clear state program source
val clearProgramSource = """
#pragma version 2
int 1
""".trimIndent()
}
}
Screenshots of log output from Stateful Smart Contract
Fig 0-3
Fig 0-4
Fig 0-5
10.Stateless Smart Contract
Stateless smart contracts are primarily used to replace signature authority for transactions. All transactions in Algorand must be signed, by either an account or a multi-signature account.
With stateless smart contracts, transactions can also be signed with logic, which Algorand designates as a LogicSignature. Stateless smart contracts can further be broken into two primary modes of use: contract accounts, and signature delegation.
Below is the code representation of a stateless smart contract.
StatelessContractRepository
interface StateLessContractRepository {
suspend fun compileTealSource() : CompileResponse
suspend fun waitForConfirmation(txID: String)
suspend fun contractAccountExample() : CompileResponse
suspend fun accountDelegationExample() : CompileResponse
}
StatelessSmartContractRepositoryImpl
package com.africinnovate.algorandandroidkotlin.repositoryImpl
import android.os.Build
import androidx.annotation.RequiresApi
import com.africinnovate.algorandandroidkotlin.ClientService.APIService
import com.africinnovate.algorandandroidkotlin.repository.StateLessContractRepository
import com.africinnovate.algorandandroidkotlin.utils.Constants
import com.africinnovate.algorandandroidkotlin.utils.Constants.CREATOR_MNEMONIC
import com.africinnovate.algorandandroidkotlin.utils.Constants.USER_ADDRESS
import com.algorand.algosdk.account.Account
import com.algorand.algosdk.algod.client.ApiException
import com.algorand.algosdk.crypto.Address
import com.algorand.algosdk.crypto.LogicsigSignature
import com.algorand.algosdk.transaction.SignedTransaction
import com.algorand.algosdk.transaction.Transaction
import com.algorand.algosdk.util.Encoder
import com.algorand.algosdk.v2.client.common.AlgodClient
import com.algorand.algosdk.v2.client.common.Response
import com.algorand.algosdk.v2.client.model.PendingTransactionResponse
import org.apache.commons.lang3.ArrayUtils
import org.json.JSONObject
import timber.log.Timber
import java.nio.file.Files.readAllBytes
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
import javax.inject.Inject
class StateLessSmartContractRepositoryImpl @Inject constructor(private val apiService: APIService) :
StateLessContractRepository {
private var client: AlgodClient = AlgodClient(
ALGOD_API_ADDR,
Constants.ALGOD_PORT,
Constants.ALGOD_API_TOKEN,
)
// utility function to connect to a node
private fun connectToNetwork(): AlgodClient {
return client
}
var headers = arrayOf("X-API-Key")
var values = arrayOf(Constants.ALGOD_API_TOKEN_KEY)
val txHeaders: Array<String> = ArrayUtils.add(headers, "Content-Type")
val txValues: Array<String> = ArrayUtils.add(values, "application/x-binary")
override suspend fun compileTealSource(): CompileResponse {
// Initialize an algod client
if (client == null) client = connectToNetwork()
// read file - int 0
// val data: ByteArray = Files.readAllBytes(Paths.get("/sample.teal"))
// val data = byteArrayOf(0)
val data1 = "int 0"
// Timber.d("data : ${data.contentToString()}")
val response: CompileResponse = client.TealCompile().source(data1.toByteArray(charset("UTF-8"))).execute(headers,values).body()
// print results
Timber.d("response: $response")
Timber.d("Hash: " + response.hash)
Timber.d("Result: " + response.result)
return response
}
override suspend fun waitForConfirmation(txID: String) {
if (client == null) client = connectToNetwork()
var lastRound = client.GetStatus().execute(headers, values).body().lastRound
while (true) {
try {
// Check the pending transactions
val pendingInfo: Response<PendingTransactionResponse> =
client.PendingTransactionInformation(txID).execute(headers, values)
if (pendingInfo.body().confirmedRound != null && pendingInfo.body().confirmedRound > 0) {
// Got the completed Transaction
println(
"Transaction " + txID + " confirmed in round " + pendingInfo.body().confirmedRound
)
break
}
lastRound++
client.WaitForBlock(lastRound).execute(headers, values)
} catch (e: Exception) {
throw e
}
}
}
override suspend fun contractAccountExample() : CompileResponse{
// Initialize an algod client
if (client == null) client = connectToNetwork()
// Set the receiver
val RECEIVER = "QUDVUXBX4Q3Y2H5K2AG3QWEOMY374WO62YNJFFGUTMOJ7FB74CMBKY6LPQ"
// Read program from file samplearg.teal
// val source = readAllBytes(Paths.get("./samplearg.teal"))
val source = """
arg_0
btoi
int 123
==
""".trimIndent()
// compile
val response = client.TealCompile().source(source.toByteArray(charset("UTF-8"))).execute(headers, values).body()
// print results
println("response: $response")
println("Hash: " + response.hash)
println("Result: " + response.result)
val program = Base64.getDecoder().decode(response.result.toString())
// create logic sig
// integer parameter
val teal_args = ArrayList<ByteArray>()
val arg1 = byteArrayOf(123)
teal_args.add(arg1)
val lsig = LogicsigSignature(program, teal_args)
// For no args use null as second param
// LogicsigSignature lsig = new LogicsigSignature(program, null);
println("lsig address: " + lsig.toAddress())
val params = client.TransactionParams().execute(headers, values).body()
// create a transaction
val note = "Hello World"
val txn: Transaction = Transaction.PaymentTransactionBuilder()
.sender(
lsig
.toAddress()
)
.note(note.toByteArray())
.amount(100000)
.receiver(Address(RECEIVER))
.suggestedParams(params)
.build()
try {
// create the LogicSigTransaction with contract account LogicSig
val stx: SignedTransaction = Account.signLogicsigTransaction(lsig, txn)
// send raw LogicSigTransaction to network
val encodedTxBytes: ByteArray = Encoder.encodeToMsgPack(stx)
val id = client.RawTransaction().rawtxn(encodedTxBytes).execute(txHeaders, txValues)
.body().txId
// Wait for transaction confirmation
waitForConfirmation(id)
println("Successfully sent tx with id: $id")
// Read the transaction
val pTrx = client.PendingTransactionInformation(id).execute(headers, values).body()
val jsonObj = JSONObject(pTrx.toString())
println("Transaction information (with notes): " + jsonObj.toString(2)) // pretty print
println("Decoded note: " + String(pTrx.txn.tx.note))
} catch (e: ApiException) {
System.err.println("Exception when calling algod#rawTransaction: " + e.getResponseBody())
}
return response
}
override suspend fun accountDelegationExample() : CompileResponse{
// Initialize an algod client
if (client == null) client = connectToNetwork()
// import your private key mnemonic and address
val SRC_ACCOUNT =
"buzz genre work meat fame favorite rookie stay tennis demand panic busy hedgehog snow morning acquire ball grain grape member blur armor foil ability seminar"
val src = Account(SRC_ACCOUNT)
// Set the receiver
val RECEIVER = "QUDVUXBX4Q3Y2H5K2AG3QWEOMY374WO62YNJFFGUTMOJ7FB74CMBKY6LPQ"
// Read program from file samplearg.teal
// val source = readAllBytes(Paths.get("./samplearg.teal"))
val source = """
arg_0
btoi
int 123
==
""".trimIndent()
// compile
val response = client.TealCompile().source(source.toByteArray(charset("UTF-8"))).execute(headers, values).body()
// print results
println("response: $response")
println("Hash: " + response.hash)
println("Result: " + response.result)
val program = Base64.getDecoder().decode(response.result.toString())
// create logic sig
// string parameter
// ArrayList<byte[]> teal_args = new ArrayList<byte[]>();
// String orig = "my string";
// teal_args.add(orig.getBytes());
// LogicsigSignature lsig = new LogicsigSignature(program, teal_args);
// integer parameter
val teal_args = ArrayList<ByteArray>()
val arg1 = byteArrayOf(123)
teal_args.add(arg1)
val lsig = LogicsigSignature(program, teal_args)
// For no args use null as second param
// LogicsigSignature lsig = new LogicsigSignature(program, null);
// sign the logic signature with an account sk
src.signLogicsig(lsig)
val params = client.TransactionParams().execute(headers, values).body()
// create a transaction
val note = "Hello World"
val txn = Transaction.PaymentTransactionBuilder()
.sender(src.address)
.note(note.toByteArray())
.amount(100000)
.receiver(Address(RECEIVER))
.suggestedParams(params)
.build()
try {
// create the LogicSigTransaction with contract account LogicSig
val stx = Account.signLogicsigTransaction(lsig, txn)
// send raw LogicSigTransaction to network
val encodedTxBytes = Encoder.encodeToMsgPack(stx)
val id = client.RawTransaction().rawtxn(encodedTxBytes).execute(txHeaders, txValues)
.body().txId
// Wait for transaction confirmation
waitForConfirmation(id)
println("Successfully sent tx with id: $id")
// Read the transaction
val pTrx = client.PendingTransactionInformation(id).execute(headers, values).body()
val jsonObj = JSONObject(pTrx.toString())
println("Transaction information (with notes): " + jsonObj.toString(2)) // pretty print
println("Decoded note: " + String(pTrx.txn.tx.note))
} catch (e: ApiException) {
System.err.println("Exception when calling algod#rawTransaction: " + e.responseBody)
}
return response
}
StatelessSmartContractViewModel
class StatelessSmartContractViewModel @ViewModelInject
constructor(val repository: StateLessContractRepository) : ViewModel() {
private val _response : MutableLiveData<CompileResponse> = MutableLiveData()
val response : LiveData<CompileResponse> get() = _response
fun compileTealSource() : LiveData<CompileResponse> {
viewModelScope.launch {
try {
_response.value = repository.compileTealSource()
} catch (e: Exception) {
Timber.e(e)
}
}
return response
}
fun contractAccount() : LiveData<CompileResponse> {
viewModelScope.launch {
try {
_response.value = repository.contractAccountExample()
} catch (e: Exception) {
Timber.e(e)
}
}
return response
}
fun accountDelegation() : LiveData<CompileResponse> {
viewModelScope.launch {
try {
_response.value = repository.accountDelegationExample()
}catch (e: Exception) {
Timber.e(e)
}
}
return response
}
}
MainActivity
Onclick on the Stateless Smart Contract button this method will be called inside the oncreate. The resulting output will be displayed on the console.
binding.stlssContract.setOnClickListener {
val intent = Intent(this, StatelessActivity::class.java)
startActivity(intent)
}
@AndroidEntryPoint
class StatelessActivity : AppCompatActivity() {
private val viewModel: StatelessSmartContractViewModel by viewModels()
private lateinit var binding: ActivityStatelessBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_stateless)
binding.compile.setOnClickListener {
viewModel.compileTealSource().observe(this, { response ->
binding.output.text = " response :$response\n hash: ${response.hash}\n result: ${response.result} "
})
}
binding.contractAccount.setOnClickListener {
viewModel.contractAccount().observe(this, { response ->
binding.output.text = " response :$response\n hash: ${response.hash}\n result: ${response.result} "
})
}
binding.accountDelegation.setOnClickListener {
viewModel.accountDelegation().observe(this, { response ->
binding.output.text = " response :$response\n hash: ${response.hash}\n result: ${response.result} "
})
}
}
}
Screenshots of log output from Stateless Smart Contract
Fig 0-6
Fig 0-7
Fig 0-8
11.Full code
The tutorial is already lengthy, so you can get the full code from the github repo
Here is the link to the AlgorandKotlinWallet
12.What’s next?
-
Creating an asset, Modifying, freezing, transferring and destroying
-
Teal, and more on smart contracts
-
Algorand Dapps
-
Connecting to AlgoSigner, Algo connect and Wallet connect
13.Resources
-
https://developer.algorand.org/docs/features/asc1/stateful/sdks/
-
https://developer.algorand.org/docs/features/asc1/stateless/sdks/
14.Conclusion
Most blockchain services, smart contracts, dapps will not be complete without thinking mobile first interactions. And building natively for the Android platform attention is gradually shifting from Java to Kotlin, good thing Java codes can be easily converted to Kotlin.
Most mobile Native Android apps will be done using Kotlin. There is therefore need to create adequate documentation that will meet the needs of Kotlin Native Android Developers.
In this tutorial we were able to cover, creating account, account recovery, getting account balance, transactions, stateful and stateless smart contracts. In subsequent, tutorials we will dig deep into more of the other services provided by the Algorand SDK, Purestake API and Algoexplorer.
Warning
This tutorial is intended for learning purposes only. It does not cover error checking and other edge cases. The smart contract(s) in this solution have NOT been audited. Therefore, it should not be used as a production application.