Create Publication

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Solution Thumbnail

Building an Android Payroll Dapp using Algorand Smart Contract

Overview

  1. Background

  2. Create Android Project

  3. Setup and Configurations

  4. Account Creation

  5. Data Model and Recyclerview Adapter

  6. Android Views

  7. Create the Teal file

  8. Compile the Teal

  9. Fund the Contract Account

  10. Verify the Account has been Funded

  11. Atomic Transfer Signed By the Contract Account

  12. Atomic Transfer Signed by the Private Key of the Sender

  13. Full Code

  14. Execute and Check

  15. Resources

EditorImages/2021/10/08 14:15/BACKIMAGE.png
Fig 0-1 App Screens

1. Background

Smart Contracts and Atomic Transfers on the Algorand platform are very powerful and can be used to create many different applications and transfers. Writing a smart contract with Transaction Execution Approval Language(TEAL) can be difficult. This solution will walk you through on how to create write a simple smart contract for a payroll system and send the the transaction to the network with atomic transfer on an android application. The contract is then compiled and funded using the Algorand dispenser.

The sample project is a payroll system that enables an employer to pay the employees at once using atomic transfer.
In this solution you will learn how you can perform/implement atomic transfer one that is signed by a contract account and another signed by the private key of the sender.

The scenario here is that the employer has three employees and he can pay all three employees at once by just a click of the pay employee button and all three employees will be credited immediately with the amount of algos sent to their account. The details of the transactions can also be viewed using the group transaction id on the algo explorer.

The github repo for this project can be found here and the apk here.

For more information on TEAL refer to these links on specification, guidelines, boilerplate and example walkthrough of a TEAL program

2. Create Android Project

Start up android studio and create a new project

EditorImages/2021/09/18 00:33/Screenshot_2021-09-18_at_01.30.30.png
Fig 2-1 Create New Empty Project

EditorImages/2021/09/18 00:34/Screenshot_2021-09-18_at_01.31.15.png
Fig 2-2 Project Name

3. Setup and Configurations

To run the completed code, the Algorand java SDK, minimum version of Android Studio 4.1 or newer. And Java 7+ and Android minSdkVersion 16+ will be required.

Add the required dependencies to your android project. For this solution the dependencies used in this project can be found in fig 3-1 below:

  //Algorand-sdk
    implementation 'com.algorand:algosdk:1.10.0'

    //logging
    implementation "com.jakewharton.timber:timber:4.7.1"

    //Retrofit
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation("com.squareup.okhttp3:okhttp:4.9.0")

    //multidex
    implementation 'androidx.multidex:multidex:2.0.1'

    //Glide for loading image
    implementation 'com.github.bumptech.glide:glide:4.10.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'

    //coroutines
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8'

EditorImages/2021/10/08 14:24/Screenshot_2021-10-08_at_15.24.33.png
Fig 3-1 Dependencies

Brief on why we need the above dependencies

The Algorand Java SDK gives us access to the Algorand features for interacting with the Algorand blockchain.

Timber takes care of logging.

Adding multidex helps to prevent multidex related errors. To learn more about multidex visit this link.

Glide is used for loading images and coroutines for handling background/heavy task. This will be required to connect to the network.

EditorImages/2021/10/08 14:25/Screenshot_2021-10-08_at_15.24.24.png
Fig 3-2 Android Level Dependencies

The higlighted files should also be added as seen in the screenshot. Databinding is required to bind the views to avoid using findviewbyId.

Purestake API Configurations
To get the Algorand REST- API-KEY 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 and for connecting with the Algorand Java SDK client. You can visit this link to setup your Purestake API account.

4. Account Creation

To perform any transactions on the blockchain you first need an account. More so when doing atomic transfer, you will need 2 or more accounts. As you can do between 1-16 transactions at once with the Algorand Atomic Transfer feature.

The accounts used in the tutorial are restored from the mnemonics for demonstration purposes. It is not recommended to have code with hard coded secret keys (mnemonics). You can use integration with a Wallets such as myAlgo, AlgoSigner or Algorand Wallet. See information on these wallets [here] (https://developer.algorand.org/ecosystem-projects/?from_query=wallet#algorand-wallet)

You can also create account using this simple code to generate accounts.

//Use this to generate accounts
        val account = Account()
        Timber.d("mnemonic1 ${account.toMnemonic()} ")
        Timber.d("address1 ${account.address} ")

To follow along, you will need to create 4 accounts. Three of the accounts will be for the employees and one for the employer. The employer will be sending fund to the three employees using the atomic transfer. The four accounts will be used later in the solution.

5. Data Model and Recyclerview Adapter

Employee Data Model -> Employee.kt

data class Employee(
    val name: String,
    val role : String,
    val salary : Int,
    val profilePics : Int,
    val accountAddress : String,
    )

Employee Data Source - EmployeeDataSource.kt

package com.africinnovate.algorandpayrollsmartcontract

import java.util.ArrayList

object EmployeeDataSource {
    fun createEmployeeDate(): ArrayList<Employee> {
        val employeeList: ArrayList<Employee> = ArrayList()

        employeeList.add(
            Employee(
                "John Doe",
                "Blockchain Engineer",
                2,
                R.drawable.pix1,
                Constants.ACCOUNT1_ADDRESS
            )
        )
        employeeList.add(
            Employee(
                "Mary Doe",
                "Frontend Engineer",
                1,
                R.drawable.pix2,
                Constants.ACCOUNT2_ADDRESS,
            )
        )
        employeeList.add(
            Employee(
                "Franklin Gold",
                "Backend Engineer",
                2,
                R.drawable.pix3,
                Constants.ACCOUNT3_ADDRESS,
            )
        )
        return employeeList
    }
}

Constants.kt

object Constants {
    const val HEADER_VALUE = "x-api-key: aaaaaaccccaaaaaaaaaaaaaaaaaaaaaah"
    var FUND_ACCOUNT = "https://dispenser.testnet.aws.algodev.network/"
    var EXLORER = "https://testnet.algoexplorer.io/"
    const val ALGOD_API_ADDR  = "https://testnet-algorand.api.purestake.io/ps2/"
    const val ALGOD_PORT = 443
    const val ALGOD_API_TOKEN_KEY = "aaaaaaccccaaaaaaaaaaaaaaaaaaaaaah"
    const val ALGOD_API_TOKEN = "aaaaaaaaaaaaaaaaaaaaaaaaaaaa"

    // Teal Generated Algorand Address
    const val LSIG_SENDER_ADDRESS = "KNJEENOA2Z5MYBQCJR43KWOGR2LNBPU3DJNMBEZFBX4TJ3CKR674F7EQ5E"

    //New Accounts
    const val ACCOUNT1_MNEMONIC = "create your own"
    const val ACCOUNT1_ADDRESS = "create your own"

    const val ACCOUNT2_MNEMONIC = "create your own"
    const val ACCOUNT2_ADDRESS = "create your own"

    const val ACCOUNT3_MNEMONIC = "create your own"
    const val ACCOUNT3_ADDRESS = "create your own"

    const val ACCOUNT_MNEMONIC = "create your own"
    const val ACCOUNT_ADDRESS = "create your own"
}

Recyclerview Adapter -> EmployeeAdapter.kt

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.africinnovate.algorandpayrollsmartcontract.Employee
import com.africinnovate.algorandpayrollsmartcontract.R
import com.bumptech.glide.Glide
import timber.log.Timber

class EmployeeAdapter internal constructor(
    private val context: Context
) : RecyclerView.Adapter<EmployeeAdapter.EmployeeViewHolder>() {

    private val inflater: LayoutInflater = LayoutInflater.from(context)
    private var employees = emptyList<Employee>()

    var onItemClick: ((Int) -> Unit)? = null


    inner class EmployeeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
        val name: TextView = itemView.findViewById(R.id.name)
        val profileImage: ImageView = itemView.findViewById(R.id.profile_image)
        val role: TextView = itemView.findViewById(R.id.role)
        val salary: TextView = itemView.findViewById(R.id.salary)

    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmployeeViewHolder {
        val itemView = inflater.inflate(R.layout.employee_list_item, parent, false)
        return EmployeeViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: EmployeeViewHolder, position: Int) {
        val current = employees[position]
        holder.name.text = current.name
        Glide.with(context)
            .load(current.profilePics).into(holder.profileImage)
        holder.role.text = current.role
        holder.salary.text = current.salary.toString()


        holder.itemView.setOnClickListener {
            onItemClick?.invoke(position)
        }

    }

    internal fun setData(employees: List<Employee>) {
        this.employees = employees
        notifyDataSetChanged()
    }

    override fun getItemCount() = employees.size

}

6. Android Views

EditorImages/2021/10/08 13:36/Screenshot_2021-10-08_at_14.33.32.png
Fig 6-1 Employee List Item Page

employee_list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.cardview.widget.CardView
        android:id="@+id/cardContainer"
        android:layout_width="match_parent"
        android:layout_height="150dp"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_margin="8dp"
        app:cardUseCompatPadding="true"
        >
        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <ImageView
                android:id="@+id/profile_image"
                android:layout_width="120dp"
                android:layout_height="120dp"
                android:scaleType="centerCrop"
                android:layout_margin="16dp"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                android:src="@drawable/pix1"
                />
            <TextView
                android:id="@+id/name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintLeft_toRightOf="@id/profile_image"
                android:text="John Doe"
                android:layout_margin="24dp"
                app:layout_constraintTop_toTopOf="parent"
                android:textSize="16sp"
                android:textStyle="bold"
                />
            <TextView
                android:id="@+id/role"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintLeft_toRightOf="@id/profile_image"
                android:text="Software Developer"
                android:layout_marginLeft="24dp"
                android:layout_marginTop="8dp"
                app:layout_constraintTop_toBottomOf="@id/name"
                android:textSize="16sp"
                />
            <TextView
                android:id="@+id/salary1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintLeft_toLeftOf="@id/role"
                android:text="Salary:"
                android:layout_marginTop="16dp"
                app:layout_constraintTop_toBottomOf="@id/role"
                android:textSize="18sp"
                />
            <ImageView
                android:id="@+id/algo_icon"
                android:layout_width="36dp"
                android:layout_height="36dp"
                android:src="@drawable/algo"
                android:layout_marginTop="8dp"
                app:layout_constraintTop_toBottomOf="@id/role"
                app:layout_constraintLeft_toRightOf="@id/salary1"
                />
            <TextView
                android:id="@+id/salary"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintLeft_toRightOf="@id/algo_icon"
                android:text="2"
                android:layout_marginTop="8dp"
                android:textColor="@color/purple_500"
                app:layout_constraintTop_toBottomOf="@id/role"
                android:textStyle="bold"
                android:textSize="24sp"
                />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.cardview.widget.CardView>

</androidx.constraintlayout.widget.ConstraintLayout>

EditorImages/2021/10/08 13:43/Screenshot_2021-10-08_at_14.43.03.png
Fig 6-2 Activity Main Page
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    android:id="@+id/swipe"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <TextView
                android:id="@+id/employer"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="16dp"
                android:layout_marginTop="36dp"
                android:text="@string/contract_account_balance"
                android:textSize="18sp"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <ImageView
                android:id="@+id/algo_icon"
                android:layout_width="36dp"
                android:layout_height="36dp"
                android:layout_marginLeft="16dp"
                android:src="@drawable/algo"
                android:layout_marginTop="16dp"
                app:layout_constraintLeft_toLeftOf="@id/employer"
                app:layout_constraintTop_toBottomOf="@id/employer" />

            <TextView
                android:id="@+id/accountBal"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:text="0"
                android:layout_marginTop="16dp"
                android:textSize="24sp"
                android:textStyle="bold"
                app:layout_constraintLeft_toRightOf="@id/algo_icon"
                app:layout_constraintTop_toBottomOf="@id/employer"/>

            <ProgressBar
                android:id="@+id/progress1"
                android:visibility="gone"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintTop_toBottomOf="@id/employer"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                />

            <com.google.android.material.floatingactionbutton.FloatingActionButton
                android:id="@+id/fund"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="24dp"
                android:layout_marginRight="24dp"
                android:src="@drawable/ic_receive_24"
                app:layout_constraintRight_toLeftOf="@id/copy"
                app:layout_constraintTop_toTopOf="parent"
                android:contentDescription="copy text" />

            <com.google.android.material.floatingactionbutton.FloatingActionButton
                android:id="@+id/copy"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="24dp"
                android:backgroundTint="@color/purple_200"
                app:layout_constraintRight_toRightOf="parent"
                android:src="@drawable/ic_baseline_content_copy_24"
                app:layout_constraintTop_toTopOf="parent"
                android:contentDescription="copy text" />

            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/recyclerview"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="100dp"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toBottomOf="@id/accountBal"
                app:layout_constraintTop_toTopOf="parent"
                tools:listitem="@layout/employee_list_item" />

            <ProgressBar
                android:id="@+id/progress"
                android:visibility="gone"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintTop_toBottomOf="@id/paybtn"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                />
            <Button
                android:id="@+id/compile"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="24dp"
                android:backgroundTint="@color/grey"
                android:padding="16dp"
                android:text="Compile"
                android:textColor="#000000"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toBottomOf="@id/recyclerview" />
            <Button
                android:id="@+id/explore"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="24dp"
                android:backgroundTint="@color/grey"
                android:padding="16dp"
                android:text="View Explorer"
                android:textColor="#000000"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toLeftOf="@id/paybtn"
                app:layout_constraintTop_toBottomOf="@id/compile" />

            <Button
                android:id="@+id/paybtn"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="24dp"
                android:padding="16dp"
                android:backgroundTint="@color/purple_500"
                android:text="Pay Employees"
                app:layout_constraintLeft_toRightOf="@id/explore"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toBottomOf="@id/compile" />

            <TextView
                android:id="@+id/result"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="24dp"
                android:text="Result"
                android:textIsSelectable="true"
                android:textSize="16sp"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toBottomOf="@id/paybtn" />

        </androidx.constraintlayout.widget.ConstraintLayout>
    </ScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

</layout>

EditorImages/2021/10/08 13:50/Screenshot_2021-10-08_at_14.47.10.png
Fig 6-3 Activity Detail Page
activity_detail.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <androidx.constraintlayout.widget.ConstraintLayout
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="parent">

            <ImageView
                android:id="@+id/profile_image"
                android:layout_width="match_parent"
                android:layout_height="300dp"
                android:layout_margin="16dp"
                android:scaleType="centerCrop"
                android:src="@drawable/pix1"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <TextView
                android:id="@+id/name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="24dp"
                android:text="John Doe"
                android:textSize="18sp"
                android:textStyle="bold"
                app:layout_constraintLeft_toLeftOf="@id/profile_image"
                app:layout_constraintTop_toBottomOf="@id/profile_image" />

            <com.google.android.material.floatingactionbutton.FloatingActionButton
                android:id="@+id/copy"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:layout_marginRight="24dp"
                android:backgroundTint="@color/purple_200"
                android:src="@drawable/ic_baseline_content_copy_24"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toBottomOf="@id/profile_image"
                android:contentDescription="copy text" />


            <TextView
                android:id="@+id/role"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="24dp"
                android:layout_marginTop="8dp"
                android:text="Software Developer"
                android:textSize="18sp"
                app:layout_constraintLeft_toLeftOf="@id/profile_image"
                app:layout_constraintTop_toBottomOf="@id/name" />

            <TextView
                android:id="@+id/account"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="24dp"
                android:layout_marginTop="10dp"
                android:text="Account Bal:"
                android:textSize="20sp"
                android:textStyle="bold"
                app:layout_constraintLeft_toLeftOf="@id/profile_image"
                app:layout_constraintTop_toBottomOf="@id/role" />
            <ImageView
                android:id="@+id/algo_icon"
                android:layout_width="36dp"
                android:layout_height="36dp"
                android:src="@drawable/algo"
                android:layout_marginTop="8dp"
                app:layout_constraintTop_toBottomOf="@id/role"
                app:layout_constraintLeft_toRightOf="@id/account"
                />
            <TextView
                android:id="@+id/salary"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:text="0"
                android:textSize="24sp"
                android:textStyle="bold"
                app:layout_constraintLeft_toRightOf="@id/algo_icon"
                app:layout_constraintTop_toBottomOf="@id/role" />
            <ProgressBar
                android:id="@+id/progress"
                android:visibility="gone"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintTop_toBottomOf="@id/salary"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                />
            <Button
                android:id="@+id/view_explorer"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:padding="16dp"
                android:layout_marginTop="48dp"
                android:text="View Explorer"
                android:backgroundTint="@color/purple_500"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toBottomOf="@id/salary"/>

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

7. Create the TEAL File

TEAL is Transaction Execution and Approval language. TEAL can analyze and approve or reject a transaction but cannot change or create a transaction. A transaction approved by TEAL is as good as cryptographically signed by a key in one of two conditions

The below TEAL program makes use of #pragma version 5, given the conditions stated in the contract to enable the Employer/contract owner perform group transactions, in this case making group payment to three employees. Here is a brief explanation of what the TEAL program does;

  • The first item in the stack which is the txn Fee check to be sure the transaction fee is less than or equal to 1,000. This is important to avoid unreasonable/excessive charge.

  • The next item in the stack which is gtxn 0 Amount check to ensure the first transactions in the group is equal to 2000000 micro algos. Same check applies to the next two group transactions in the stack.

  • txn Amount check to ensure the transaction amount is less than or equal to 5000000 micro algos.

  • global GroupSize check to ensure the size of the group transaction is not more than 3.
  • txn CloseRemainderTo whatever is left at the end of the transaction is transferred to the intended recipient. This leaves the sender account in zero balance, else this should be set to the global ZeroAccount.

  • txn RekeyTo This check prevents the transaction from been assigned to a new private key. It should be set to the intended auth address or global ZeroAccount.

If the conditions stated in the TEAL program are not met the transaction will not be successful.

source file - Constants.kt

val tealSource = """#pragma version 5        
                // Check the Fee is reasonable, In this case 1,000 micro algos
                txn Fee
                int 1000
                <=

                //Check that the first group transaction is equal to 2000000
                gtxn 0 Amount
                int 2000000
                ==
                assert

                //Check that the second group transaction is equal to 1000000
                gtxn 1 Amount
                int 1000000
                == 
                assert

                //Check that the third group transaction is equal to 2000000
                gtxn 2 Amount
                int 2000000
                ==
                assert

                //Check that the transaction amount is less than 5000000 or equal to 5000000
                txn Amount
                int 5000000
                <=
                assert

                //Check the number of transactions in this atomic transaction group
                global GroupSize
                int 3
                ==
                assert

                //CloseRemainderTo should be the intended recipient or equal to global ZeroAddress.
                txn CloseRemainderTo 
                global ZeroAddress
                ==
                assert

                //This check to prevent the transaction from been assigned to a new private key.
                txn RekeyTo
                global ZeroAddress
                ==
                assert     
            """.trimIndent()

8. Compile the TEAL

EditorImages/2021/10/15 20:03/Screenshot_2021-10-15_at_21.00.15.png
Fig 8-1 TEAL Compile

After compiling the TEAL program, you will see a generated hash and result. If you compile the above TEAL program, you should get this address generated for you KNJEENOA2Z5MYBQCJR43KWOGR2LNBPU3DJNMBEZFBX4TJ3CKR674F7EQ5E as shown in fig 8-1.
The generated hash is an Algorand address which you will fund following the next step.

    private fun compileTeal() = launch {
        runOnUiThread {
            binding.progress.visibility = View.VISIBLE
        }
        withContext(Dispatchers.Default) {
            try {
                response = client.TealCompile().source(source.toByteArray(charset("UTF-8"))).execute(
                    headers,
                    values
                )
                if (!response.isSuccessful) {
                    throw java.lang.Exception(response.message().toString())
                }
                Timber.d("compileResponse: hash ${response.body().hash}")
                Timber.d("compileResponse: result ${response.body().result}")
                runOnUiThread {
                    binding.progress.visibility = View.GONE
                    binding.result.text =
                        " Response:\n Hash: ${response.body().hash}\n Result:  ${response.body().result}"
                }

            } catch (e: Throwable) {
                e.printStackTrace()
                runOnUiThread {
                    binding.progress.visibility = View.GONE
                    binding.result.text =
                        "Error occured, check the TEAL program"
                }
            }

        }
    }

9. Fund the Contract Account

EditorImages/2021/10/15 20:04/Screenshot_2021-10-15_at_20.57.18.png
Fig 9-1 Fund Account
You can get fund for your testnet account using the Algorand Dispenser. To perform atomic transfer you need to fund the contract account to avoid errors.

10. Verify the Account has been Funded

EditorImages/2021/10/15 20:04/Screenshot_2021-10-15_at_20.58.25.png
Fig 10-1 Check Account Balance

To verify that the account has been funded, you can check the Algorand explorer

11. Atomic Transfer Signed By the Contract Account

EditorImages/2021/09/19 00:11/Screenshot_2021-09-19_at_01.04.31.png
Fig 11-1 Atomic Transfer

On Algorand, atomic transfers are implemented as irreducible batch operations, where a group of transactions are submitted as a unit and all transactions in the batch either pass or fail.

An atomic transfer on Algorand is confirmed in less than 5 seconds, just like any other transaction. Transactions can contain Algos or Algorand Standard Assets and may also be governed by Algorand Smart Contracts. Transactions between 2 to 16 can be done with atomic transfer.

One of the use case of atomic transfer is to perform distributed payments to multiple recipients. Which is what this tutorial will be focusing on.

The code snippet below shows, how you can use Algorand atomic transfer to pay the salaries of 3 employees at once using a generated contract account from the logic sig.

The below transaction is signed after compilation of the TEAL program and the hash generated is used to generate a contract account which is used to sign the transaction.

MainActivity.kt
wait for confirmation

   /**
     * 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
     */
    private 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
                        println("Transaction " + txID + " confirmed in round " + pendingInfo.confirmedRound)
                        runOnUiThread {
                            binding.result.text =
                                "Transaction $txID  confirmed in round ${pendingInfo.confirmedRound}"
                        }
                        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!")
    }

MainActivity.kt

  // Atomic Transfer Signed with a smart Contract Logic Sig
    @RequiresApi(Build.VERSION_CODES.O)
    private fun atomicTransferWithSmartContract() = launch {
        runOnUiThread {
            binding.progress.visibility = View.VISIBLE
        }
        withContext(Dispatchers.Default) {
            try {
                val program = Base64.getDecoder().decode(response.body().result.toString())

                val lsig = LogicsigSignature(program, null)
                Timber.d("lsig add ${lsig.toAddress()}")

                val employee1_mnemonic = ACCOUNT1_MNEMONIC
                val employee2_mnemonic = ACCOUNT2_MNEMONIC
                val employee3_mnemonic = ACCOUNT3_MNEMONIC

                // recover account A, B, C
                val acctA = Account(employee1_mnemonic)
                val acctB = Account(employee2_mnemonic)
                val acctC = Account(employee3_mnemonic)

                // get node suggested parameters
                var resp = client.TransactionParams().execute(headers, values)
                if (!resp.isSuccessful) {
                    throw java.lang.Exception(resp.message())
                }
                val params = resp.body() ?: throw  Exception("Params retrieval error")
                // Create the first transaction
                val tx1: Transaction = Transaction.PaymentTransactionBuilder()
                    .sender(lsig.toAddress())
                    .amount(2000000)
                    .receiver(acctA.address)
                    .suggestedParams(params)
                    .note("employee1".toByteArray())
                    .build()

                // Create the second transaction
                val tx2: Transaction = Transaction.PaymentTransactionBuilder()
                    .sender(lsig.toAddress())
                    .amount(1000000)
                    .receiver(acctB.address)
                    .suggestedParams(params)
                    .note("employee2".toByteArray())
                    .build()

                // Create the third transaction
                val tx3: Transaction = Transaction.PaymentTransactionBuilder()
                    .sender(lsig.toAddress())
                    .amount(2000000)
                    .receiver(acctC.address)
                    .suggestedParams(params)
                    .note("employee3".toByteArray())
                    .build()

                // group transactions an assign ids
                val gid: Digest = TxGroup.computeGroupID(*arrayOf(tx1, tx2, tx3))
                tx1.assignGroupID(gid)
                tx2.assignGroupID(gid)
                tx3.assignGroupID(gid)


                // create the LogicSigTransaction with contract account LogicSig
                // sign individual transactions
                val signedTx1: SignedTransaction = Account.signLogicsigTransaction(lsig, tx1)
                val signedTx2: SignedTransaction = Account.signLogicsigTransaction(lsig, tx2)
                val signedTx3: SignedTransaction = Account.signLogicsigTransaction(lsig, tx3)

                // put all transactions in a byte array
                val byteOutputStream = ByteArrayOutputStream()
                val encodedTxBytes1: ByteArray = Encoder.encodeToMsgPack(signedTx1)
                val encodedTxBytes2: ByteArray = Encoder.encodeToMsgPack(signedTx2)
                val encodedTxBytes3: ByteArray = Encoder.encodeToMsgPack(signedTx3)
                byteOutputStream.write(encodedTxBytes1)
                byteOutputStream.write(encodedTxBytes2)
                byteOutputStream.write(encodedTxBytes3)
                val groupTransactionBytes: ByteArray = byteOutputStream.toByteArray()

                // Submit transaction group to the network
                val rawResponse = client.RawTransaction().rawtxn(groupTransactionBytes).execute(
                    txHeaders,
                    txValues
                )
                if (!rawResponse.isSuccessful()) {
                    throw  Exception(rawResponse.message());
                }
                val id = rawResponse.body().txId

                println("Successfully sent tx with ID: $id")
                runOnUiThread {
                    binding.progress.visibility = View.GONE
                    binding.result.text = "Successfully sent tx with ID: $id"
                }

                // Wait for transaction confirmation
                val pTrx = waitForConfirmation(client, id, 4)
                System.out.println("Transaction " + id.toString() + " confirmed in round " + pTrx!!.confirmedRound)
                // Read the transaction
                val jsonObj2 = JSONObject(pTrx.toString())
                println("Transaction information (with notes): " + jsonObj2.toString(2))
                println("Decoded note: " + String(pTrx.txn.tx.note))
                println("Transaction information (with notes): " + jsonObj2.toString(2))
                println("Decoded note: " + String(pTrx.txn.tx.note))
                println("Amount: " + pTrx.txn.tx.amount.toString())
                println("Fee: " + pTrx.txn.tx.fee.toString())
                if (pTrx.closingAmount != null) {
                    println("Closing Amount: " + pTrx.closingAmount.toString())
                }
            } catch (e: java.lang.Exception) {
                System.err.println("Exception when calling algod#transactionInformation: " + e.message);
                runOnUiThread {
                    binding.progress.visibility = View.GONE
                    binding.result.text =
                    "${e.message}"
                }
            }
        }
    }
}

12. Atomic Transfer Signed by the Private Key of the Sender

The code snippet below shows, how you can use Algorand atomic transfer to transfer to pay the salaries of 3 employees at once using the signed by the private key of the sender/employer in this case.

MainActivity.kt

//Atomic Transfer signed by the Sender
    @RequiresApi(Build.VERSION_CODES.O)
    private fun atomicTransfer() = launch {
        runOnUiThread {
            binding.progress.visibility = View.VISIBLE
        }
        withContext(Dispatchers.Default) {
            val employer_mnemonic = ACCOUNT_MNEMONIC
            val employee1_mnemonic = ACCOUNT1_MNEMONIC
            val employee2_mnemonic = ACCOUNT2_MNEMONIC
            val employee3_mnemonic = ACCOUNT3_MNEMONIC

            // recover account A, B, C
            val acctA = Account(employer_mnemonic)
            val acctB = Account(employee1_mnemonic)
            val acctC = Account(employee2_mnemonic)
            val acctD = Account(employee3_mnemonic)

            var resp = client.TransactionParams().execute(headers, values)
            if (!resp.isSuccessful) {
                throw java.lang.Exception(resp.message())
            }
            val params = resp.body() ?: throw  Exception("Params retrieval error")
            // Create the first transaction
            val tx1: Transaction = Transaction.PaymentTransactionBuilder()
                .sender(acctA.address)
                .amount(2000000)
                .receiver(acctB.address)
                .suggestedParams(params)
                .build()

            // Create the second transaction
            val tx2: Transaction = Transaction.PaymentTransactionBuilder()
                .sender(acctA.address)
                .amount(1000000)
                .receiver(acctC.address)
                .suggestedParams(params)
                .build()


            // Create the third transaction
            val tx3: Transaction = Transaction.PaymentTransactionBuilder()
                .sender(acctA.address)
                .amount(2000000)
                .receiver(acctD.address)
                .suggestedParams(params)
                .build()

            // group transactions an assign ids
            val gid: Digest = TxGroup.computeGroupID(*arrayOf(tx1, tx2, tx3))
            tx1.assignGroupID(gid)
            tx2.assignGroupID(gid)
            tx3.assignGroupID(gid)

            // sign individual transactions
            val signedTx1: SignedTransaction = acctA.signTransaction(tx1)
            val signedTx2: SignedTransaction = acctA.signTransaction(tx2)
            val signedTx3: SignedTransaction = acctA.signTransaction(tx3)


            try {
                // put all transactions in a byte array
                val byteOutputStream = ByteArrayOutputStream()
                val encodedTxBytes1: ByteArray = Encoder.encodeToMsgPack(signedTx1)
                val encodedTxBytes2: ByteArray = Encoder.encodeToMsgPack(signedTx2)
                val encodedTxBytes3: ByteArray = Encoder.encodeToMsgPack(signedTx3)
                byteOutputStream.write(encodedTxBytes1)
                byteOutputStream.write(encodedTxBytes2)
                byteOutputStream.write(encodedTxBytes3)
                val groupTransactionBytes: ByteArray = byteOutputStream.toByteArray()

                // send transaction group to the network
                val rawResponse = client.RawTransaction().rawtxn(groupTransactionBytes).execute(
                    txHeaders,
                    txValues
                )
                if (!rawResponse.isSuccessful()) {
                    throw  Exception(rawResponse.message());
                }
                val id = rawResponse.body().txId

                println("Successfully sent tx with ID: $id")
                runOnUiThread {
                    binding.progress.visibility = View.GONE
                    binding.result.text = "Successfully sent tx with ID: $id"
                }

                // Wait for transaction confirmation
                val pTrx = waitForConfirmation(client, id, 4)
                System.out.println("Transaction " + id.toString() + " confirmed in round " + pTrx!!.confirmedRound)
                // Read the transaction
                val jsonObj2 = JSONObject(pTrx.toString())
                println("Transaction information (with notes): " + jsonObj2.toString(2))
                println("Decoded note: " + String(pTrx.txn.tx.note))
                println("Transaction information (with notes): " + jsonObj2.toString(2))
                println("Decoded note: " + String(pTrx.txn.tx.note))
                println("Amount: " + pTrx.txn.tx.amount.toString())
                println("Fee: " + pTrx.txn.tx.fee.toString())
                if (pTrx.closingAmount != null) {
                    println("Closing Amount: " + pTrx.closingAmount.toString())
                }
            } catch (e: java.lang.Exception) {
                println("Submit Exception: $e")
                System.err.println("Exception when calling algod#transactionInformation: " + e.message);

            }
        }
    }

13. Full code

MainActivity.kt

package com.africinnovate.algorandpayrollsmartcontract

import EmployeeAdapter
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.africinnovate.algorandpayrollsmartcontract.Constants.ACCOUNT1_MNEMONIC
import com.africinnovate.algorandpayrollsmartcontract.Constants.ACCOUNT2_MNEMONIC
import com.africinnovate.algorandpayrollsmartcontract.Constants.ACCOUNT3_MNEMONIC
import com.africinnovate.algorandpayrollsmartcontract.Constants.ACCOUNT_MNEMONIC
import com.africinnovate.algorandpayrollsmartcontract.Constants.ALGOD_API_ADDR
import com.africinnovate.algorandpayrollsmartcontract.Constants.ALGOD_API_TOKEN
import com.africinnovate.algorandpayrollsmartcontract.Constants.ALGOD_API_TOKEN_KEY
import com.africinnovate.algorandpayrollsmartcontract.Constants.ALGOD_PORT
import com.africinnovate.algorandpayrollsmartcontract.databinding.ActivityMainBinding
import com.algorand.algosdk.account.Account
import com.algorand.algosdk.crypto.Address
import com.algorand.algosdk.crypto.Digest
import com.algorand.algosdk.crypto.LogicsigSignature
import com.algorand.algosdk.transaction.SignedTransaction
import com.algorand.algosdk.transaction.Transaction
import com.algorand.algosdk.transaction.TxGroup
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.CompileResponse
import com.algorand.algosdk.v2.client.model.PendingTransactionResponse
import kotlinx.coroutines.*
import org.apache.commons.lang3.ArrayUtils
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.json.JSONObject
import timber.log.Timber
import java.io.ByteArrayOutputStream
import java.security.Security
import java.util.*
import kotlin.Array as Array1


class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
    var headers = arrayOf("X-API-Key")
    var values = arrayOf(ALGOD_API_TOKEN_KEY)

    val txHeaders: Array1<String> = ArrayUtils.add(headers, "Content-Type")
    val txValues: Array1<String> = ArrayUtils.add(values, "application/x-binary")
    lateinit var employeeAdapter: EmployeeAdapter
    lateinit var binding: ActivityMainBinding
    private var client: AlgodClient = AlgodClient(
        ALGOD_API_ADDR,
        ALGOD_PORT,
        ALGOD_API_TOKEN,
        ALGOD_API_TOKEN_KEY
    )
    val source = Constants.tealSource
    lateinit var response: Response<CompileResponse>

    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        Security.removeProvider("BC")
        Security.insertProviderAt(BouncyCastleProvider(), 0)

        val address = Address(Constants.LSIG_SENDER_ADDRESS)
        getAccountBalance(address)

        initializeRecyclerview()

        employeeAdapter.onItemClick = { position ->
            val intent = Intent(this, DetailActivity::class.java)
            intent.putExtra("employee", position)
            startActivity(intent)
        }

        binding.copy.setOnClickListener {
            val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
            val clip = ClipData.newPlainText("label", Constants.LSIG_SENDER_ADDRESS)
            clipboard.setPrimaryClip(clip)
            Toast.makeText(this, Constants.LSIG_SENDER_ADDRESS, Toast.LENGTH_LONG).show()
        }
        binding.fund.setOnClickListener { fundAccount() }
        binding.explore.setOnClickListener { viewExlorer() }

        binding.swipe.setOnRefreshListener {
            getAccountBalance(address)
            binding.swipe.isRefreshing = false
        }
        //Use this to generate accounts
        val account = Account()
        Timber.d("mnemonic1 ${account.toMnemonic()} ")
        Timber.d("address1 ${account.address} ")

        binding.paybtn.setOnClickListener {
            atomicTransferWithSmartContract()
//            atomicTransfer()
        }

        binding.compile.setOnClickListener {
            compileTeal()
        }
        binding.root

    }

    private fun getAccountBalance(address: Address?) = launch {
        runOnUiThread {
            binding.progress1.visibility = View.VISIBLE
        }
        withContext(Dispatchers.Default) {
            try {
                val respAcct = client.AccountInformation(address).execute(headers, values)
                if (!respAcct.isSuccessful) {
                    throw java.lang.Exception(respAcct.message())
                }
                val accountInfo = respAcct.body()
                println(String.format("Account Balance: %d microAlgos", accountInfo.amount))
                runOnUiThread {
                    binding.progress1.visibility = View.GONE
                    val amount = accountInfo.amount.toBigDecimal()
                    binding.accountBal.text = amount.toString()
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }

    private fun viewExlorer() {
        val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Constants.EXLORER))
        try {
            if (!Constants.EXLORER.startsWith("http://") && !Constants.EXLORER.startsWith(
                    "https://"
                )
            )
                Constants.EXLORER = "http://" + Constants.EXLORER;
            startActivity(browserIntent)
        } catch (e: Exception) {
            Timber.d("Host not available ${e.message}")
        }
    }

    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}")
        }
    }

    private fun initializeRecyclerview() {
        val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
        employeeAdapter = EmployeeAdapter(this)
        recyclerView.adapter = employeeAdapter
        recyclerView.layoutManager = LinearLayoutManager(this)
        employeeAdapter.setData(EmployeeDataSource.createEmployeeDate())
    }

    // utility function to connect to a node
    private fun connectToNetwork(): AlgodClient {
        return client
    }

    /**
     * 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
     */
    private 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
                        println("Transaction " + txID + " confirmed in round " + pendingInfo.confirmedRound)
                        runOnUiThread {
                            binding.result.text =
                                "Transaction $txID  confirmed in round ${pendingInfo.confirmedRound}"
                        }
                        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!")
    }


    //Atomic Transfer signed by the Sender
    @RequiresApi(Build.VERSION_CODES.O)
    private fun atomicTransfer() = launch {
        runOnUiThread {
            binding.progress.visibility = View.VISIBLE
        }
        withContext(Dispatchers.Default) {
            val employer_mnemonic = ACCOUNT_MNEMONIC
            val employee1_mnemonic = ACCOUNT1_MNEMONIC
            val employee2_mnemonic = ACCOUNT2_MNEMONIC
            val employee3_mnemonic = ACCOUNT3_MNEMONIC

            // recover account A, B, C
            val acctA = Account(employer_mnemonic)
            val acctB = Account(employee1_mnemonic)
            val acctC = Account(employee2_mnemonic)
            val acctD = Account(employee3_mnemonic)

            var resp = client.TransactionParams().execute(headers, values)
            if (!resp.isSuccessful) {
                throw java.lang.Exception(resp.message())
            }
            val params = resp.body() ?: throw  Exception("Params retrieval error")
            // Create the first transaction
            val tx1: Transaction = Transaction.PaymentTransactionBuilder()
                .sender(acctA.address)
                .amount(2000000)
                .receiver(acctB.address)
                .suggestedParams(params)
                .build()

            // Create the second transaction
            val tx2: Transaction = Transaction.PaymentTransactionBuilder()
                .sender(acctA.address)
                .amount(1000000)
                .receiver(acctC.address)
                .suggestedParams(params)
                .build()


            // Create the third transaction
            val tx3: Transaction = Transaction.PaymentTransactionBuilder()
                .sender(acctA.address)
                .amount(2000000)
                .receiver(acctD.address)
                .suggestedParams(params)
                .build()

            // group transactions an assign ids
            val gid: Digest = TxGroup.computeGroupID(*arrayOf(tx1, tx2, tx3))
            tx1.assignGroupID(gid)
            tx2.assignGroupID(gid)
            tx3.assignGroupID(gid)

            // sign individual transactions
            val signedTx1: SignedTransaction = acctA.signTransaction(tx1)
            val signedTx2: SignedTransaction = acctA.signTransaction(tx2)
            val signedTx3: SignedTransaction = acctA.signTransaction(tx3)


            try {
                // put all transactions in a byte array
                val byteOutputStream = ByteArrayOutputStream()
                val encodedTxBytes1: ByteArray = Encoder.encodeToMsgPack(signedTx1)
                val encodedTxBytes2: ByteArray = Encoder.encodeToMsgPack(signedTx2)
                val encodedTxBytes3: ByteArray = Encoder.encodeToMsgPack(signedTx3)
                byteOutputStream.write(encodedTxBytes1)
                byteOutputStream.write(encodedTxBytes2)
                byteOutputStream.write(encodedTxBytes3)
                val groupTransactionBytes: ByteArray = byteOutputStream.toByteArray()

                // send transaction group to the network
                val rawResponse = client.RawTransaction().rawtxn(groupTransactionBytes).execute(
                    txHeaders,
                    txValues
                )
                if (!rawResponse.isSuccessful()) {
                    throw  Exception(rawResponse.message());
                }
                val id = rawResponse.body().txId

                println("Successfully sent tx with ID: $id")
                runOnUiThread {
                    binding.progress.visibility = View.GONE
                    binding.result.text = "Successfully sent tx with ID: $id"
                }

                // Wait for transaction confirmation
                val pTrx = waitForConfirmation(client, id, 4)
                System.out.println("Transaction " + id.toString() + " confirmed in round " + pTrx!!.confirmedRound)
                // Read the transaction
                val jsonObj2 = JSONObject(pTrx.toString())
                println("Transaction information (with notes): " + jsonObj2.toString(2))
                println("Decoded note: " + String(pTrx.txn.tx.note))
                println("Transaction information (with notes): " + jsonObj2.toString(2))
                println("Decoded note: " + String(pTrx.txn.tx.note))
                println("Amount: " + pTrx.txn.tx.amount.toString())
                println("Fee: " + pTrx.txn.tx.fee.toString())
                if (pTrx.closingAmount != null) {
                    println("Closing Amount: " + pTrx.closingAmount.toString())
                }
            } catch (e: java.lang.Exception) {
                println("Submit Exception: $e")
                System.err.println("Exception when calling algod#transactionInformation: " + e.message);

            }
        }
    }

    private fun compileTeal() = launch {
        runOnUiThread {
            binding.progress.visibility = View.VISIBLE
        }
        withContext(Dispatchers.Default) {
            try {
                response = client.TealCompile().source(source.toByteArray(charset("UTF-8"))).execute(
                    headers,
                    values
                )
                if (!response.isSuccessful) {
                    throw java.lang.Exception(response.message().toString())
                }
                Timber.d("compileResponse: hash ${response.body().hash}")
                Timber.d("compileResponse: result ${response.body().result}")
                runOnUiThread {
                    binding.progress.visibility = View.GONE
                    binding.result.text =
                        " Response:\n Hash: ${response.body().hash}\n Result:  ${response.body().result}"
                }

            } catch (e: Throwable) {
                e.printStackTrace()
                runOnUiThread {
                    binding.progress.visibility = View.GONE
                    binding.result.text =
                        "Error occured, check the TEAL program"
                }
            }

        }
    }

    // Atomic Transfer Signed with a smart Contract Logic Sig
    @RequiresApi(Build.VERSION_CODES.O)
    private fun atomicTransferWithSmartContract() = launch {
        runOnUiThread {
            binding.progress.visibility = View.VISIBLE
        }
        withContext(Dispatchers.Default) {
            try {
                val program = Base64.getDecoder().decode(response.body().result.toString())

                val lsig = LogicsigSignature(program, null)
                Timber.d("lsig add ${lsig.toAddress()}")

                val employee1_mnemonic = ACCOUNT1_MNEMONIC
                val employee2_mnemonic = ACCOUNT2_MNEMONIC
                val employee3_mnemonic = ACCOUNT3_MNEMONIC

                // recover account A, B, C
                val acctA = Account(employee1_mnemonic)
                val acctB = Account(employee2_mnemonic)
                val acctC = Account(employee3_mnemonic)

                // get node suggested parameters
                var resp = client.TransactionParams().execute(headers, values)
                if (!resp.isSuccessful) {
                    throw java.lang.Exception(resp.message())
                }
                val params = resp.body() ?: throw  Exception("Params retrieval error")
                // Create the first transaction
                val tx1: Transaction = Transaction.PaymentTransactionBuilder()
                    .sender(lsig.toAddress())
                    .amount(2000000)
                    .receiver(acctA.address)
                    .suggestedParams(params)
                    .note("employee1".toByteArray())
                    .build()

                // Create the second transaction
                val tx2: Transaction = Transaction.PaymentTransactionBuilder()
                    .sender(lsig.toAddress())
                    .amount(1000000)
                    .receiver(acctB.address)
                    .suggestedParams(params)
                    .note("employee2".toByteArray())
                    .build()

                // Create the third transaction
                val tx3: Transaction = Transaction.PaymentTransactionBuilder()
                    .sender(lsig.toAddress())
                    .amount(2000000)
                    .receiver(acctC.address)
                    .suggestedParams(params)
                    .note("employee3".toByteArray())
                    .build()

                // group transactions an assign ids
                val gid: Digest = TxGroup.computeGroupID(*arrayOf(tx1, tx2, tx3))
                tx1.assignGroupID(gid)
                tx2.assignGroupID(gid)
                tx3.assignGroupID(gid)


                // create the LogicSigTransaction with contract account LogicSig
                // sign individual transactions
                val signedTx1: SignedTransaction = Account.signLogicsigTransaction(lsig, tx1)
                val signedTx2: SignedTransaction = Account.signLogicsigTransaction(lsig, tx2)
                val signedTx3: SignedTransaction = Account.signLogicsigTransaction(lsig, tx3)

                // put all transactions in a byte array
                val byteOutputStream = ByteArrayOutputStream()
                val encodedTxBytes1: ByteArray = Encoder.encodeToMsgPack(signedTx1)
                val encodedTxBytes2: ByteArray = Encoder.encodeToMsgPack(signedTx2)
                val encodedTxBytes3: ByteArray = Encoder.encodeToMsgPack(signedTx3)
                byteOutputStream.write(encodedTxBytes1)
                byteOutputStream.write(encodedTxBytes2)
                byteOutputStream.write(encodedTxBytes3)
                val groupTransactionBytes: ByteArray = byteOutputStream.toByteArray()

                // Submit transaction group to the network
                val rawResponse = client.RawTransaction().rawtxn(groupTransactionBytes).execute(
                    txHeaders,
                    txValues
                )
                if (!rawResponse.isSuccessful()) {
                    throw  Exception(rawResponse.message());
                }
                val id = rawResponse.body().txId

                println("Successfully sent tx with ID: $id")
                runOnUiThread {
                    binding.progress.visibility = View.GONE
                    binding.result.text = "Successfully sent tx with ID: $id"
                }

                // Wait for transaction confirmation
                val pTrx = waitForConfirmation(client, id, 4)
                System.out.println("Transaction " + id.toString() + " confirmed in round " + pTrx!!.confirmedRound)
                // Read the transaction
                val jsonObj2 = JSONObject(pTrx.toString())
                println("Transaction information (with notes): " + jsonObj2.toString(2))
                println("Decoded note: " + String(pTrx.txn.tx.note))
                println("Transaction information (with notes): " + jsonObj2.toString(2))
                println("Decoded note: " + String(pTrx.txn.tx.note))
                println("Amount: " + pTrx.txn.tx.amount.toString())
                println("Fee: " + pTrx.txn.tx.fee.toString())
                if (pTrx.closingAmount != null) {
                    println("Closing Amount: " + pTrx.closingAmount.toString())
                }
            } catch (e: java.lang.Exception) {
                System.err.println("Exception when calling algod#transactionInformation: " + e.message);
                runOnUiThread {
                    binding.progress.visibility = View.GONE
                    binding.result.text =
                         "${e.message}"
                }
            }
        }
    }
}

DetailActivity.kt

package com.africinnovate.algorandpayrollsmartcontract

import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.net.Uri
import android.opengl.Visibility
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.africinnovate.algorandpayrollsmartcontract.databinding.ActivityDetailBinding
import com.algorand.algosdk.account.Account
import com.algorand.algosdk.crypto.Address
import com.algorand.algosdk.v2.client.common.AlgodClient
import kotlinx.coroutines.*
import timber.log.Timber

class DetailActivity : AppCompatActivity(), CoroutineScope by MainScope() {
    private var employees = ArrayList<Employee>()

    //    private lateinit var employees: ArrayList<Employee>
    lateinit var binding: ActivityDetailBinding
    private var client: AlgodClient = AlgodClient(
        Constants.ALGOD_API_ADDR,
        Constants.ALGOD_PORT,
        Constants.ALGOD_API_TOKEN,
        Constants.ALGOD_API_TOKEN_KEY
    )
    var headers = arrayOf("X-API-Key")
    var values = arrayOf(Constants.ALGOD_API_TOKEN_KEY)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_detail)
        employees = EmployeeDataSource.createEmployeeDate()
        val data = intent.getIntExtra("employee", 1)
        val employee: Employee = employees[data]
        binding.name.text = employee.name
        binding.role.text = employee.role
        binding.profileImage.setImageResource(employee.profilePics)

        binding.copy.setOnClickListener {
            val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
            val clip = ClipData.newPlainText("label", employee.accountAddress)
            clipboard.setPrimaryClip(clip)
            Toast.makeText(this, employee.accountAddress, Toast.LENGTH_LONG).show()
        }
        binding.viewExplorer.setOnClickListener { viewExlorer() }

        val address = Address(employee.accountAddress)
        getAccountBalance(address)
    }

    private fun getAccountBalance(address: Address?) = launch {
        runOnUiThread {
            binding.progress.visibility = View.VISIBLE
        }
        withContext(Dispatchers.Default) {
            try {
                val respAcct = client.AccountInformation(address).execute(headers, values)
                if(!respAcct.isSuccessful){
                    throw java.lang.Exception(respAcct.message())
                }
                val accountInfo = respAcct.body()
                println(String.format("Account Balance: %d microAlgos", accountInfo.amount))
                runOnUiThread {
                    binding.progress.visibility = View.GONE
                    val amount = accountInfo.amount.toBigDecimal()
                    binding.salary.text = amount.toString()
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }


    private fun viewExlorer() {
        val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Constants.EXLORER))
        try {
            if (!Constants.EXLORER.startsWith("http://") && !Constants.EXLORER.startsWith(
                    "https://"
                )
            )
                Constants.EXLORER = "http://" + Constants.EXLORER;
            startActivity(browserIntent)
        } catch (e: Exception) {
            Timber.d("Host not available ${e.message}")
        }
    }
}

14. Execute and Check

EditorImages/2021/10/08 14:31/Screenshot_2021-10-08_at_15.27.15.png
Fig 14-1 Compile

EditorImages/2021/10/08 14:32/Screenshot_2021-10-08_at_15.29.26.png
Fig 14-2 Check Transaction Details

EditorImages/2021/10/08 14:32/Screenshot_2021-10-08_at_15.29.52.png
Fig 14-3 Group Transaction Info

15. Resources

These are helpful resources to learn more about atomic transfer, teal and smart contract;

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.