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.

Tutorial Thumbnail
Intermediate · 1 hour +

Milestone Dapp Built with Pyteal

The goal of this tutorial is to teach current Algorand developers some of the more intermediate concepts needed to build robust decentralized applications on Algorand network.

PyTeal is a Python library for generating TEAL programs.

TEAL is a Transaction Execution Approval Language used for writing Smart Contract that will be deployed on the Algorand blockchain.

In this tutorial, we take a deep dive into the following concepts:

  • Inner Transactions
  • Transaction fee pooling
  • Handling runtime errors

RESOURCES
Github Repository

Requirements

Background

Note: This tutorial assumes the reader is familiar with the basics of writing Algorand smart contracts.

About the Project

This tutorial attempts to mirror what happens on traditional freelance platforms like upwork and fiverr but using the Algorand blockchain to handle the payment security.

How It Works

  1. A freelancer (employee) and a client (employer) agrees on terms of work such as
    • Cost of work
    • Start and end date of work
  2. The client makes an atomic transaction (group transaction) to the smart contract in which the first call in the group transaction sets the start and end date of the work/milestone.
    The second call is a payment call to the smart contract escrow address.
  3. When the freelancer is ready with their work submission, they make a submission call to the contract.
  4. The following are the possible use cases to be aware of:
    • Freelancer submits and client accepts submission -> Funds are atomatically sent to freelancer.
    • Freelancer submits and client does not accept/decline by a 7 day ultimatum after the submission. The freelancer can make a withdrawal call and the funds are atomatically transferred to the freelancer.
    • If the freelancer does not submit within the set timeframe of the agreement, the client can make a refund call and the funds are automatically sent back to client address.

I will be using the terms freelancer/employee interchangeably as well as employer/client.

Algorand Smart Contract Tutorials

PyTeal is used to write smart contracts on the Algorand blockchain. The PyTeal code will be deployed to the algorand network using the py-algorand-sdk.

The goal of the milestone based contract is to allow freelancers and clients agree on terms of work and be assured of secured payments via the smart contract escrow.

Funds sent by the client are not released until the work is approved by the client, upon which the locked funds are released to the freelancer via the inner transaction.

Steps

Note: Some utility functions used are not described here, so check the source code for more context.

Take a look at the Github Repository for their implementations.

Note:

  • I use client and employer interchangeably. They are the same.
  • I use freelancer and employee interchangeably. They are the same.

1. Application Global State Initialization.

The py-algorand-sdk provides a set of functions and utilities to interact with the Algorand network.

The py-algorand-sdk function algosdk.future.transaction.ApplicationCreateTxn creates the application. This is a specific application transaction to the blockchain and will return an application ID.

Several parameters are passed to the creation method such as:

  • Sender of contract address.
  • Suggested transaction parameters.
  • Approval program.
  • Clear program.
  • Arguments.
  • Global and local schema (allocated storage space).

This application uses eleven global variables (six byte slices and five integers).

Smart Contract Interaction Code

The following code snippet shows how to make an application creation call to a smart contract using py-algorand-sdk.

deploy.py

class Interface:

    @staticmethod
    def create_call():
        # The space that should be reserved for storage local (if using local storage) and globally
        global_schema = contract_schema(6, 5)
        local_schema = contract_schema(0, 0)

        algod_client = WebService.algod_client()

        compiled_approval_program = compile_to_bytes(
            client=algod_client, code=approval_program())

        compiled_clear_program = compile_to_bytes(
            client=algod_client, code=clear_program())

        # arguments
        args = [
            WebService.address_to_bytes(client_address),
            WebService.address_to_bytes(freelancer_address),
            1_500_000_00,
        ]

        app_id = transaction_instance.create_contract(
            approval_program=compiled_approval_program,
            clear_program=compiled_clear_program,
            global_schema=global_schema,
            local_schema=local_schema,
            args=args
        )

        # Read global state of contract

        global_state = transaction_instance.read_global_state(app_id)

        print("================================")
        print(" Smart Contract Global State ...")
        print("=================================")

        print(global_state)

        return app_id

The above code is an abstraction layer for deploying the smart contract to the blockchain network.

We essentially set the required global schemas i.e. space that should be allocated for the contract. We also pass the complied approval and clear PyTeal programs alongside with any arguments the contract call takes in.

Below, you will find the class TransactionService which calls transaction.ApplicationCreateTxn using the py-agorand-sdk. Check the original source code for greater context.

services.py

1.class TransactionService:
2.    algod_client = WebService.algod_client()
3.    deployer_private_key = WebService.get_deployer_private_key()
4.    deployer_address = WebService.get_address_from_pk(deployer_private_key)
5.
6.    def create_contract(
7.        self,
8.        approval_program,
9.        clear_program,
10.        global_schema,
11.        local_schema,
12.        args
13.    ):
14.        on_complete = transaction.OnComplete.NoOpOC.real
15.        suggested_params = self.algod_client.suggested_params()
16.
17.        txn = transaction.ApplicationCreateTxn(
18.            self.deployer_address,
19.            suggested_params,
20.            on_complete,
21.            approval_program,
22.            clear_program,
23.            global_schema,
24.            local_schema,
25.            args
26.        )
27.
28.        signed_txn = txn.sign(self.deployer_private_key)
29.        txn_id = signed_txn.transaction.get_txid()
30.        self.algod_client.send_transactions([signed_txn])
31.
32.        self.wait_for_confirmation(txn_id, 15)
33.
34.        txn_response = self.algod_client.pending_transaction_info(txn_id)
35.        print(txn_response)
36.        application_id = txn_response["application-index"]
37.        return application_id

A set of parameters are used in the create method to configure the smart contract.

Some of such parameters are;

  • Approval Program
  • Clear State Program
  • Global Schema
  • Local Schema
  • Application arguments as see on line 25 such as client address, freelancer address and milestone cost.

For more information on creating a stateful smart contract, see Smart Contract Creation.

PyTeal Code

The first call always made when creating Algorand Smart Contract is the applicaton initializaton script. During this application call, the states needed for the contract are initialized.

There are two types of state that could be used when building an Algorand smart contract, the global and the local state.

  • The application global state is visible to all accounts using the smart contract.
  • The local state belongs individually to each account opted-in to the application/smart contract.

Below you will find the code snippet for initializing the state of the contract.

contract.py

 1. from pyteal import * 
 2. def approval_program():
 3.      # Accepted Application States Call Arguments
 4.      op_set_state = Bytes("set_state")
 5.      op_accept = Bytes("accept")
 6.      op_decline = Bytes("decline")
 7.      op_submit = Bytes("submit")
 8.      op_withdraw = Bytes("withdraw")
 9.      op_refund = Bytes("refund")
 10.      
 11.     # App Global Schemas (byteslice | uint64)
 12.     
 13.     # byteslice : deployer address
 14.     global_creator_address = Bytes(global_creator)
 15.     # uint64 : start date of milestone in time stamp
 16.     global_start_date = Bytes("start_date") 
 17.     # uint64 : end date of milestone in time stamp
 18.     global_end_date = Bytes("end_date")
 19.     # uint64: cost of the milestone in algo
 20.     global_amount = Bytes("amount") 
 21.     # uint64: time left for client/employer to review milestone after submission
 22.     global_ultimatum = Bytes("ultimatum")
 23.     # byteslice: client/employer algorand address
 24.     global_client = Bytes("client")
 25.     # byteslice: freelancer/employee algorand address
 26.     global_freelancer = Bytes("freelancer")
 27.     # uint64: date of milestone submission by freelancer/employee as time stamp
 28.     global_submission_date = Bytes("submission_date") 
 29.     # byteslice: status of submission 
 30.     global_submitted = Bytes("submit")
 31.     # byteslice: status of payment (if it has been sent or not)
 32.     global_sent = Bytes("sent")

As seen above from line 4 through 9, we set the op operation arguments that can be passed for each application call such as set_state, accept etc.

They will make more sense as we move further through the tutorial.

Lines 14 through 32 declare the global schemas which is the amount of storage space allocated to a smart contract on Algorand to store either byteslices or uint64.

Once an application transaction.ApplicationCreateTxn call is made, the following code snippet is triggered:

Note: This code lives inside the approval_program function. Check the GitHub repository for context.

deploy.py

 1. def initialize_app():
 2.     # This function sets the values of the declared contract schemas in the previous code snippet
 3.     # we make certain assertions to check the validity of the call and approve or decline the app call accordingly
 4.     return Seq([
 5.         # we assert that the call was made passing three (3) arguments with it
 6.         Assert(Txn.application_args.length() == Int(3)),
 7.         # we initialize the application global schema states with the arguments passed
 8.
 9.         # we set the contract deployer address to the sender of this transaction
 10.      
 11.        App.globalPut(global_creator, Txn.sender()),
 12.        # we set the client/employer algorand address
 13.        App.globalPut(global_client, Txn.application_args[0]),
 14.        # we set the freelancer/employee algorand address
 15.        App.globalPut(global_freelancer, Txn.application_args[1]),
 16.        # we set the milestone cost in algo
 17.        App.globalPut(global_amount, Btoi(Txn.application_args[2]),
 18.        # we set the ultimatum to zero since we are just setting up the contract
 19.        App.globalPut(global_ultimatum, Int(0)),
 20.        # we set the payment status to false since we are just setting up the contract
 21.        App.globalPut(global_sent, Bytes("False"),
 22.        # we set the submission date to zero since we are just setting up the contract
 23.        App.globalPut(global_submission_date, Int(0)),
 24.        # we set the submission status to False since we are just setting up the contract
 25.        App.globalPut(global_submitted, Bytes("False")),
 26.        # we set the start date and end date to zero since we are just setting up the contract
 27.        App.globalPut(global_start_date, Int(0))
 28.        App.globalPut(global_end_date, Int(0)),
 29.        # After all initialization is done, we approve the transaction call
 30.        Approve()
 31.    ])

Line 7 is an assertion statement that checks that we have passed the correct number of arguments with the transaction call. In this case, we check if three arguments were passed in.

Lines 12 through 30 set some of the global schemas we defined earlier by indexing the passed in arguments.

On line 30, if all conditions are met in the Assert block, we approve the transaction application call.

2. Application Set State Call (Step A).

Using the py-algorand-sdk function algosdk.future.transaction.ApplicationCallTxn and passing the required parameters as shown in the code below, we make a call to the smart contract with three arguments:

  • First argument tells the smart contract it is a set_state call.
  • Second argument specifies the start date of the milestone.
  • Third argument specifies the end date of the milestone.

Note : We pass timestamps of the datetime for both start and end date of milestone.

Smart Contract Interaction Code

The code below is an abstraction layer for making a set_state call to the smart contract.

deploy.py

@staticmethod
def set_up_call(app_id, sender, sender_pk, receiver, amount):
    args = [
        "set_state",
        int(get_current_timestamp()),  # start milestone timestamp
        # end milestone timestamp (14 days)
        int(get_future_timestamp_in_days(days=14))
    ]

    return transaction_instance.set_up_call(
        app_id=app_id, app_args=args,
        receiver=receiver, sender=sender, 
        amount=amount, sender_pk=sender_pk
    )

The code below shows how to make a grouped transaction call.

Atomic transfers are a group of transactions to be submitted at the same time.

Once again for more context, check the source code.

We first create a normal application call called set_state, then we make a payment call and assign both transactions to the same group id.

We can calculate the group id by using the py-algorand-sdk transaction.calculate_group_id method.

services.py

def set_up_call(self, app_id, app_args, receiver, amount, sender, sender_pk):
    # This is an atomic transaction consisting of two groups of transactions

    app_call_txn = self.no_op_call(
        sender=sender,
        app_id=app_id,
        on_complete=transaction.OnComplete.NoOpOC,
        app_args=app_args,
        sender_pk=sender_pk,
        sign_txn=False,
    )

    payment_call_txn = self.payment_transaction(
        sender=sender, sender_pk=sender_pk, 
        receiver=receiver, amount=amount, sign_txn=False
    )

    group_id = transaction.calculate_group_id(
        [app_call_txn, payment_call_txn]
    )

    app_call_txn.group = group_id
    payment_call_txn.group = group_id

    app_call_txn_signed = app_call_txn.sign(sender_pk)
    payment_call_txn_signed = payment_call_txn.sign(sender_pk)

    signed_group = [app_call_txn_signed, payment_call_txn_signed]

    txn_id = self.algod_client.send_transactions(signed_group)

    self.wait_for_confirmation(txn_id, 15)

    print(f"Set up application call with transaction_id: {txn_id}")

    return txn_id

PyTeal Code

contract.py

@Subroutine(TealType.none)
1. def set_state():
2.    # This fucntion sets the parameters to get the contract started
3.   # It is a grouped transaction with the second being a payment transaction to the smart contract escrow address.
4.
5.    return Seq([
6.        # Run some checks
7.        Assert(
8.            And(
9.               # assert it a group transaction with a group size of 2
10.                Global.group_size() == Int(2),
11.                Txn.group_index() == Int(0),
12.                # assert it is the client making this call
13.                Txn.sender() == App.globalGet(global_client),
14.                # assert the length of the argumnets passed is 3
15.                Txn.application_args.length() == Int(3),
16.
17.                # assert the second transaction in the group is a payment transaction
18.                Gtxn[1].type_enum() == TxnType.Payment,
19.                # assert the right amount is sent
20.                Gtxn[1].amount() == App.globalGet(global_amount),
21.                # assert the payment is to the smart contract escrow address
22.                Gtxn[1].receiver() == Global.current_application_address(),
23.                Gtxn[1].close_remainder_to() == Global.zero_address(),
24.                App.globalGet(global_start_date) == App.globalGet(
25.                    global_end_date) == Int(0),  # assert the contract hasn't started
26.            )
27.        ),
28.
29.        #  set the start and end dates
30.        App.globalPut(global_start_date, Btoi(Txn.application_args[1])),
31.        App.globalPut(global_end_date, Btoi(Txn.application_args[2])),
32.        Approve()
33.    ])

The above function set_state is another call that can be made to the smart contract. The purpose of this function is so that the contract can be kick started. This particular application call consist of two grouped transaction in which the first transaction is a regular application call to the smart contract, while the second is a payment transaction call.

On line 7 through 27, we perform a set of checks,
1. Firstly, on line 10 we assert this call is a group transaction with a length of two.
2. We assert that the sender of the transaction is the client/employer address.
3. We also perform three checks: one to assert that the second transaction is of type payment, that it pays the correct amount as was declared when the contract was fist deployed and, finally, that the funds go to the correct smart contract address.

On line 30 and 31, we set the start and end dates of the milestone as timestamps.

3. Grouped Transactions (Step B).

As seen in step 2 above, this transaction is actually a grouped transaction in which:

  • The first call sets the global start and end dates of the contract.
  • The second call is a payment call to the smart contract escrow address done by the employer.

Below shows the full code implementation for grouped transactions:

Notice that on lines 4 - 11, we make an application call, on lines 13 - 16 we make a payment call and on line 30, we send it all together as a single transaction.

Smart Contract Interaction Code

services.py

1. def set_up_call(self, app_id, app_args, receiver, amount, sender, sender_pk):
2.    # This is an atomic transaction consisting of two groups of transactions
3.
4.    app_call_txn = self.no_op_call(
5.        sender=sender,
6.        app_id=app_id,
7.        on_complete=transaction.OnComplete.NoOpOC,
8.        app_args=app_args,
9.        sender_pk=sender_pk,
10.        sign_txn=False,
11.    )
12.
13.    payment_call_txn = self.payment_transaction(
14.        sender=sender, sender_pk=sender_pk, 
15.        receiver=receiver, amount=amount, sign_txn=False
16.    )
17.
18.    group_id = transaction.calculate_group_id(
19.        [app_call_txn, payment_call_txn]
20.    )
21.
22.    app_call_txn.group = group_id
23.    payment_call_txn.group = group_id
24.
25.    app_call_txn_signed = app_call_txn.sign(sender_pk)
26.    payment_call_txn_signed = payment_call_txn.sign(sender_pk)
27.
28.    signed_group = [app_call_txn_signed, payment_call_txn_signed]
29.
30.    txn_id = self.algod_client.send_transactions(signed_group)
31.
32.    self.wait_for_confirmation(txn_id, 15)
33.
34.    print(f"Set up application call with transaction_id: {txn_id}")
35.
36.    return txn_id

4. Application Submission Call.

The purpose of this call is to allow for the freelancer/employee to make a submission upon completion of the smart contract.

Smart Contract Interaction Code

Here we make a call to the smart contract with three arguments:

  • “submit” to tell the smart contract that this is a submission call
  • Submission status
  • Ultimatum timestamp

deploy.py

@staticmethod
def submit_call(app_id, sender_pk, sender):
    args = [
        "submit",
        "True",
        # timestamp for ultimatum of 7 days from submission
        int(get_future_timestamp_in_days(days=7))
        ]
    return transaction_instance.submit_call(app_id, sender_pk, sender=sender, args=args)

The code above is an abstraction layer that calls the submit_call function of class TransactionService.

services.py

def submit_call(self, app_id, sender, sender_pk, args):
    txn = self.no_op_call(
        sender=sender,
        app_id=app_id,
        on_complete=transaction.OnComplete.NoOpOC,
        app_args=args,
        sign_txn=False,
        sender_pk=sender_pk
    )

    signed_txn = txn.sign(sender_pk)
    txn_id = signed_txn.transaction.get_txid()
    self.wait_for_confirmation(txn_id, 15)

    self.algod_client.send_transactions([signed_txn])
    txn_response = self.algod_client.pending_transaction_info(txn_id)

    print(f"Application submit call with transaction resp: {txn_response}")

    return txn_id

From the code above, we make use of the py-algorand-sdk to make a submit call to the smart contract, and we also pass along the required arguments to the smart contract call.

PyTeal Code

contract.py

@Subroutine(TealType.none)
1. def submit():
2.    return Seq([
3.        Assert(
4.            And(
5.                # the transaction sender must be the freelancer associated with the contract
6.                Txn.sender() == App.globalGet(global_freelancer),
7.                # assert that the milestone has started and been set
8.                App.globalGet(global_start_date) != Int(0),
9.                App.globalGet(global_end_date) != Int(0),
10.                # assert that the submission has not beem previously made
11.                App.globalGet(global_submitted) == Bytes("False"),
12.                # assert that the payment hasn't beem made
13.                App.globalGet(global_sent) == Bytes("False"),
14.                # assert that the second argumnet equals True to set submission status
15.                Txn.application_args[1] == Bytes("True"),
16.                Txn.application_args.length() == Int(3),
17.
18.                # assert the start date is less than current date time
19.                App.globalGet(global_start_date) <= Global.latest_timestamp(),
20.                # assert that the enddate is greater than the current date time
21.                App.globalGet(global_end_date) > Global.latest_timestamp(),
22.
23.           )
24.        ),
25.        # we set the submission status to true
26.        App.globalPut(global_submitted, Txn.application_args[1]),
27.        # we set the submission date
28.        App.globalPut(global_submission_date, Global.latest_timestamp()),
29.        
30.        # we set the ultimatum date (7 days)
31.        App.globalPut(global_ultimatum, Btoi(Txn.application_args[2])),
32.        Approve()
33.    ])

We assert that the call is made by the freelancer/employee. We assert that three arguments are passed to the smart contract, i.e. the application call type, the submission status, and the ultimatum as a timestamp.

The ultimatum is essentially the grace period that the client/employer has in order to review a submission. If the submission is not reviewed i.e. approved or declined, the freelancer/employee is permitted to make a withdraw call.

On line 31, we set the global_ultimatum.
On line 28, we set the global_submission_date as the timestamp of the block during which the milestone was submitted.
On line 26, we set the status of submission to be true. This indicates that a submission has been made.

The ultimatum timestamp is the sum of the current submission timestamp plus 7 days.

5. Application Acceptance Call

This call is made by the employer/client to accept a submission made by the freelancer/employee.

Once this transaction call is made to the smart contract, the milestone funds are automatically released to the freelancer/employee address via an inner transaction.

An Inner Transaction is the result of a smart contract logic that is by an external transanction.

An external transaction could be as simple as an application call to the smart contract.

Smart Contract Interaction Code

services.py

def accept_call(self, sender, sender_pk, app_id, args, accounts):
    txn = self.no_op_call(
        fee=2,
        sender=sender,
        app_id=app_id,
        on_complete=transaction.OnComplete.NoOpOC,
        app_args=args,
        sign_txn=False,
        accounts=accounts,
        sender_pk=sender_pk

    )

    signed_txn = txn.sign(sender_pk)
    txn_id = signed_txn.transaction.get_txid()
    self.wait_for_confirmation(txn_id, 15)

    self.algod_client.send_transactions([signed_txn])
    txn_response = self.algod_client.pending_transaction_info(txn_id)

    print(f"Application accept call with transaction resp: {txn_response}")

    return txn_id

PyTeal Code

contract.py

@Subroutine(TealType.none)
def accept():
    # case of client/employer accpecting the milestone
    return Seq([
        Assert(
            And(
                App.globalGet(global_submitted) == Bytes(
                    "True"),  # the freelancer must have submitted
                # assert the call is made by the client
                Txn.sender() == App.globalGet(global_client),
                # assert that the payment hasn't been previously made
                App.globalGet(global_sent) == Bytes("False"),
                Txn.application_args.length() == Int(1),
                Txn.group_index() == Int(0),
                Txn.accounts[1] == App.globalGet(global_freelancer),
            )
        ),

        Assert(
            Txn.fee() >= Global.min_txn_fee() * Int(2)
        ),

        sendPayment(Txn.accounts[1], App.globalGet(
            global_amount), Txn.accounts[1]),  # send payments to freelancer
        # we set the status of payment to true
        App.globalPut(global_sent, Bytes("True")),
        Approve()
    ])

Here we accept one argument to the smart contract. Being “accept” which tells the smart contract how to handle the application call and an accounts array which tells the smart contract which accounts it is allowed to access.

Note, that we Assert the Txn.accounts[1] equals global_freelancer. This is to ensure we are sending the funds to the right address.

The Txn.accounts array allows additional accounts other than the Txn.sender to be passed to the contract call.

6. Inner Transactions

Inner transactions allow stateful applications/smart contract to have many of the effects of a true top-level transaction, programatically.

However, they are different in some significant ways. The most important differences are that they are not signed, duplicates are not rejected, and they do not appear in the block in the usual away.

Instead, their effects are noted in metadata associated with their top-level application call transaction.

Read more about Inner Transactions
PyTeal Code

deploy.py

@Subroutine(TealType.none)
def sendFund(receiver, amount, close_to_receiver):
    return Seq([
        InnerTxnBuilder.Begin(),
        InnerTxnBuilder.SetFields({
            # specifies the type of transaction been made (payment, application_call, etc)
            TxnField.type_enum: TxnType.Payment,
            # we subtract the cost for making the call (gas fee) and the minimum amount of algo that must be in an algorand account
            TxnField.amount: amount - (Global.min_balance() + Global.min_txn_fee()),
            # The sender of this payment is the smart contract escrow address
            TxnField.sender: Global.current_application_address(),
            TxnField.receiver: receiver,  # Funds receiver
            # address to send the remaining algo in the escrow account to,
            TxnField.close_remainder_to: close_to_receiver,
        }),
        InnerTxnBuilder.Submit()
    ])

  • InnerTxn.Begin() Indicates the start of an inner transaction
  • InnerTxn.SetFields() It takes in a dictionary with the necessary details of the inner transaction to be created.

Some possible fields that can be passed to InnerTxn.SetFields are listed below:

  • TxnField.type_enum: The type of transaction being performed. Some possible types are asset transfer, payment etc. see pyteal.TxnType for the supported types.
  • TxnField.sender: The address performing the transaction.
  • TxnField.close_remanider_to: When set, it indicates that the transaction is requesting that the Sender account should be closed, and all remaining funds be transferred to this address.

Check out pyteal docs for more details of other fields that could be passed in.

Just consider it as a normal call you would make to a smart contract (Application call, Payment call) but now it is called within a smart contract and not outside of the smart contract.

InnerTxn.Submit() This will submit the inner transaction to the network.

7. Transaction Fee Pooling.

Transaction fee pooling allows a single transaction in the current contract call to cover the fees for any of the other transactions that may be triggered such as inner transactions as we see in the PyTeal code.

That is, the fee for the application call can essentially cover the fee for any inner transaction call made during the call.

Inner transactions may have their fees covered by the outer transactions but they may not cover outer transaction fees.

This limitation that only outer transactions may cover inner transactions is true in the case of nested inner transactions as well.

Smart Contract Interaction Code

services.py

1.def no_op_call(self, sender, sender_pk, app_id, on_complete, app_args=None,         2.sign_txn=True, accounts=[], fee=0):
3.    suggested_params = self.algod_client.suggested_params()
4.
5.    if fee != 0:
6.        suggested_params.fee = fee * MIN_TXN_FEE
7.        suggested_params.flat_fee = True
8.
9.    txn = transaction.ApplicationCallTxn(
10.        sender=sender,
11.        sp=suggested_params,
12.        index=app_id,
13.        app_args=app_args,
14.        on_complete=on_complete,
15.        accounts=accounts
16.    )
17.    if sign_txn:
18.        txn = txn.sign(sender_pk)
19.
20.    return txn

To perform transaction fee pooling, we multiply the total amount of calls that would be made, by the minimum Algorand transaction fee as seen on line 6 and set the value in the suggested parameter.

We also set the flat_fee to True as seen on line 7 and set the value in the suggested parameters also.

Flat Fee: A fee that does not depend on the size of the transaction. All transactions on the Algorand blockchain require a fee, which must be greater than or equal to the minimum transaction fee (currently 1000 microAlgos). py-algorand-sdk provides us two ways to determine the fee for our transaction. One method uses the suggested fee-per-byte and the other allows you to specify a flat fee. we set the flat fee to true because we want to specify the transaction fees ourselves and not use the suggested fee-per-byte since we are performing transaction fee pooling.

PyTeal Code

contract.py

1.@Subroutine(TealType.none)
2.def sendPayment(receiver, amount_in_algo, close_to_receiver):
3.    # This demonstrates just a basic use case of inner transactions.
4.    # Accounts on algorand must maintain a minimum balace of 100,000 microAlgos
5.    return Seq([
6.        InnerTxnBuilder.Begin(),
7.        InnerTxnBuilder.SetFields({
8.            # specifies the type of transacion been made (paymnet, application_call, etc)
9.            TxnField.type_enum: TxnType.Payment,
10.            # we subtract the cost for making the call (gas fee) and the minimum amount of algo that must be in an algorand account
11.            TxnField.amount: amount_in_algo - Global.min_balance(),
12.            # The sender of this payment is the smart contract escrow address
13.            TxnField.sender: Global.current_application_address(),
14.            TxnField.receiver: receiver,  # Funds receiver
15.            # address to send the remaining algo in the escrow account to,
16.            TxnField.close_remainder_to: close_to_receiver,
17.            TxnField.fee: Int(0),  # It has already been paid for
18.        }),
19.        InnerTxnBuilder.Submit()
20.    ])

The code gets executed when an accept application call is made to the smart contract.

As we know already, this is also an application transaction call and hence, requires a transaction fee, but since we have covered the fee in the smart contract interaction call through fee pooling, we can set transaction fee to zero (0) as seen on line 17.

Read More About Fee pooling reference

8. Application Withdraw Call

This call can only be made by the employee/freelancer after a submission as been made and the the employer/client has not made an accept or decline call within the ultimatum.

If the ultimatum set for the contract has expired and the client/employer has not responded, the funds are released to the freelancer/employee automatically and the employee/freelancer pays the transaction fee for the inner transaction. We compare dates using the logical operators such as >=, <=, >, <. We check if the timestamp for the ultimatum set in the contract global state is less than the timestamp of the transaction.

The employee/freelancer in this case pays the fee for the inner transaction incured.

Smart Contract Interaction Code

services.py

def withdraw_call(self, app_id, sender, sender_pk, args, accounts=[]):
    txn = self.no_op_call(
        sender=sender,
        app_id=app_id,
        on_complete=transaction.OnComplete.NoOpOC,
        app_args=args,
        sign_txn=False,
        accounts=accounts,
        sender_pk=sender_pk
    )

    signed_txn = txn.sign(sender_pk)
    txn_id = signed_txn.transaction.get_txid()
    self.wait_for_confirmation(txn_id, 15)

    self.algod_client.send_transactions([signed_txn])
    txn_response = self.algod_client.pending_transaction_info(txn_id)

    print(
        f"Application withdarw call with transaction resp: {txn_response}")

    return txn_id

PyTeal Code

contract.py

@Subroutine(TealType.none)
def withdraw():
    # case client hasn't accepted or declined submission
    # ultimatum time must have been exceeded
    # freelancer must have submitted work
    # payment hasn't been made by client
    return Seq([
        Assert(
            And(
                Txn.sender() == App.globalGet(global_freelancer),
                App.globalGet(global_submitted) == Bytes("True"),
                # assert that the payment hasn't be made
                App.globalGet(global_sent) == Bytes("False"),
                Txn.application_args.length() == Int(1),
                Txn.group_index() == Int(0),
                App.globalGet(global_ultimatum) != Int(0),
                Global.latest_timestamp() >= App.globalGet(global_ultimatum)
            )
        ),
        Assert(
            # two because the first call to this handler and the second call which is a payment call
            Txn.fee() >= Global.min_txn_fee() * Int(2)

        ),
        sendPayment(Txn.sender(), App.globalGet(
            global_amount), Txn.sender()),
        App.globalPut(global_sent, Bytes("True")),
        Approve()
    ])

In the code above, we make the following assertions:
1. This transaction call is made by the freelancer/employee address.
2. The milestone work has been submitted.
3. That no payment has been previously made.
4. The ultimatum timestamp has been set.
5. We assert that, the timestamp of the transaction is greater than or equal to the set ultimatum timestamp.
6. We also check to ensure that the transaction fee for the inner transaction that would be trigged by this call has been payed for.

Finally, we make an inner transaction to send the payment to the employee/freelancer and mark the payment status to be true.

9. Application Delete Call

This call is used to delete an application/smart contract, remove the global state and other application parameters from the creators/deployer Algorand account.

Smart Contract Interaction Code

services.py

def delete_call(self, app_id: int, accounts:list) -> int:
    suggested_params = self.algod_client.suggested_params()
    suggested_params.fee = 2 * MIN_TXN_FEE
    suggested_params.flat_fee = True

    txn = transaction.ApplicationDeleteTxn(
        sender=self.deployer_address,
        index=app_id,
        sp=suggested_params,
        accounts=accounts,

    )

    signed_txn = txn.sign(self.deployer_private_key)

    txn_id = signed_txn.transaction.get_txid()
    self.wait_for_confirmation(txn_id, 15)

    self.algod_client.send_transactions([signed_txn])

    txn_response = self.algod_client.pending_transaction_info(txn_id)

    print(f"Application Deleted with transaction resp: {txn_response}")

    return txn_id

PyTeal Code

contract.py

def delete_app():

    return Seq([
        Assert(
            Or(
                Txn.sender() == App.globalGet(global_creator),
                Txn.sender() == App.globalGet(global_client),
            )
        ),
        If(
            And(
                Txn.sender() == App.globalGet(global_creator),
                App.globalGet(global_start_date) == Int(0)
            ),
            Approve()
        ),
        If(
            And(
                App.globalGet(global_start_date) == Int(0), App.globalGet(global_sent) == Bytes("True")
            ),
            Approve()
        ),


        If(And(Txn.sender() == App.globalGet(global_client), App.globalGet(
            global_start_date) != Int(0)), App.globalGet(global_sent) == Bytes("False"))  # we make sure that client has paid to escrow and has not released funds to freelancer
        .Then(
            sendFund(Txn.sender(), App.globalGet(
                global_amount), Txn.sender()),
        ),

        If(And(Txn.sender() == App.globalGet(global_creator),
           App.globalGet(global_start_date) != Int(0)), App.globalGet(global_sent) == Bytes("False"))
        .Then(
            sendFund(Txn.accounts[1], App.globalGet(
                global_amount), Txn.accounts[1]),
        ),
        Approve()
    ])

When writing a smart contract, before we delete the application, we need to empty the application account. If the application is deleted before emptying the application account there is no way to recover the assets.

We perform the process of emptying the account via an inner transaction.

This call can be made by the employer/client who created this contract.

In the code snippet above, we perform the following:
1. We assert the call is made by the client/employer
2. We check if the milestone has not started, we approve the transaction call.
3. If the client/employer has paid the milestone fee to the application address, we make an inner transaction to send back funds to the client/employer address.

10. Handling Runtime Errors

It is inevitable to come across bugs in our code. In this section, we will discuss how to handle some possible runtime errors.

Some of those cases are:

  • TealInputError, TealCompileError: These are errors associated with our PyTeal code such as using data types that are not provide by TEAL or wrongly using a PyTeal function e.g, calling Cond() without at least a single condition throws a TealInputError

  • Syntax Errors: This a normal python syntax error.

  • AlgodHTTPError: This error is thrown when our call to the smart contract is not approved.

A call to a smart contract is considered unapproved when it doesn’t fulfill/pass all the assertions required in the smart contract application call.

Below shows a simple way of handling such errors.

Note: The code in the try block is abbreviated, and you should check the source code for more context.

Smart Contract Interaction Code

deploy.py

def main():
    try:
        print("======================")
        print("making deployment call ...")
        print("======================")

        # deploy the smart contract
        application_id = Interface.create_call()

        print(f"Application id: {application_id}")

        # code is abbreviated
       ...

    except TealInputError as teal_error:
        _, value, _ = sys.exc_info()
        print(f"Error in teal code: {value}")

    except TypeError as type_error:
        _, value, _ = sys.exc_info()
        print(f"Invalid Teal type: {value}")

    except SyntaxError as syntax_error:
        _, value, _ = sys.exc_info()
        print(f"Syntx Error in code: {value}. Check your python code and teal code for possible syntax errors")


    except NameError as name_err:
        _, value, _ =sys.exc_info()
        print(f"Name Error in code: {value}. Check your python code and teal code for naming error")

    except TealCompileError:
        _, value, _ = sys.exc_info()
        print(f"Error compiling teal code to bytes: {value}")

    except AlgodHTTPError as http_err:
        _, value, _ = sys.exc_info()
        print(f"Smart Contract Call Not Approved: {value}")
        print(f"Error code: {http_err.code}")


    except Exception as e:
        print(e.__class__.__name__)
        exc_type, value, traceback = sys.exc_info()

        if e.__class__.__name__ == "SyntaxError":
            print(f"Syntx Error in code: {value}. Check your python code and teal code for possible syntax errors")
            return

        if e.__class__.__name__ == 'NameError':
            print(f"Name Error in code: {value}. Check your python code and teal code for naming error")
            return

        print(value, exc_type)

Conclusion

The milestone application illustrates the use of many of Algorand’s layer-1 features to implement a complete application such as inner transactions and transaction fee pooling.

The full source code for the application is available on Github.

Note: This code has not been audited and should not be used in a production environment.