Tutorials
No Results
Tutorial Image

Intermediate · 30 minutes

Dynamic Fee Template with PyTeal

This example uses PyTeal to implement the Dynamic Fee Template which allows a third-party to pay a transaction fee for a transaction in which it is not involved.

Requirements

Background

PyTeal is a language binding for Algorand Smart Contracts (ASC). PyTeal allows Algorand developers to express their smart contract logic in Python. The PyTeal library then compiles the smart contract logic to TEAL source code for the developers. In this tutorial we are building a PyTeal sample that shows how to create a dynamic fee smart contract that is used as a delegated signature.

1. Define Template Variables

This tutorial creates a dynamic fee delegate signature using Pyteal. Suppose we have three accounts: A, B, C. This delegate logic allows account B to pay the fee of a transaction from account A to account C. This is done with an atomic transfer containing two transactions. One from account B to account A to add algos to account A to compensate for the transaction fee. The second transaction in the atomic transfer is the payment transaction from account A to account C for the payment. The TEAL logic only approves this payment if both transactions are in the atomic transfer and the fee is the proper amount.

Read the background information in the tutorial to get more insights into the dynamic fee template.

To create this template we first will create a python file that contains the parameters for the dynamic fee and the logic needed to create the TEAL program. These parameters are defined as follows:

  • TMPL_TO: the recipient of the payment from account A
  • TMPL_AMT: the amount to send from account A to TMPL_TO in microAlgos
  • TMPL_CLS: the account to close out the remainder of account A’s funds to after paying TMPL_AMT to TMPL_TO
  • TMPL_FV: the required first valid round of the payment from account A
  • TMPL_LV: the required last valid round of the payment from account A
  • TMPL_LEASE: the string to use for the transaction lease in the payment from account A (to avoid replay attacks)

Create a file named dynamic_fee.py. First, we define some configuration variables that will be used in the contract.

from pyteal import *

#template variables
tmpl_fee = Int(1000)
tmpl_lease = Bytes("base64", "y9OJ5MRLCHQj8GqbikAUKMBI7hom+SOj8dlopNdNHXI=")
tmpl_amt = Int(200000)
tmpl_rcv = Addr("ZZAF5ARA4MEC5PVDOP64JM5O5MQST63Q2KOY2FLYFLXXD3PFSNJJBYAFZM")
tmpl_fv = Int(55555)
tmpl_lv = Int(56555)
tmpl_cls = Addr("GD64YIY3TWGDMCNPP553DZPPR6LDUSFQOIJVFDPPXWEG3FVOJCCDBBHU5A")

These values are temporary and will be changed when the logic is used in a transaction. One thing to note is that the tmpl_cls value can be the Zero Address or any account if you want to close out account A after the payment is made. Later in the tutorial we will show using the Zero Address, which means we want to authorize this one payment but dont close out account A.

2. Define the Contract Logic

Add a function to the dynamic_fee.py file that implements the logic of contract. Additionally, you can add a function to print out the created TEAL for testing purposes.

def dynamic_fee(tmpl_cls=tmpl_cls,
                     tmpl_fv=tmpl_fv,
                     tmpl_lv=tmpl_lv,
                     tmpl_lease=tmpl_lease,
                     tmpl_amt=tmpl_amt,
                     tmpl_rcv=tmpl_rcv):

    dynamic_fee_core = And(Global.group_size() == Int(2), Gtxn.type_enum(0) == Int(1), 
                         Gtxn.receiver(0) == Txn.sender(),
                         Gtxn.amount(0) == Txn.fee(),
                         Txn.group_index() == Int(1),
                         Txn.type_enum() == Int(1),
                         Txn.receiver() == tmpl_rcv,
                         Txn.close_remainder_to() == tmpl_cls,
                         Txn.amount() == tmpl_amt,
                         Txn.first_valid() == tmpl_fv,
                         Txn.last_valid() == tmpl_lv,
                         Txn.lease() == tmpl_lease)           

    return dynamic_fee_core

print(dynamic_fee().teal())

This code has many AND conditions that check that we have two transactions, both transactions are payment transactions, the receiver of the first is transaction is the sender of the second transaction, the amount in the first transaction is the fee amount in the second transaction, the TEAL code is attached to the second of the two transactions, the receiver of the second transaction is the account set in the tmpl_rcv, the second transactions close remainder to parameter is set to the tmpl_cls, the amount in the second transaction is the amount specified in tmpl_amt, first and last valid rounds are equal to the values specified in the template parameters, and the lease parameter is equal to thevalue set in tmpl_lease.

You should be able to run this and get the following output.

$ python3 dynamic_fee.py
global GroupSize
int 2
==
gtxn 0 TypeEnum
int 1
==
&&
gtxn 0 Receiver
txn Sender
==
&&
gtxn 0 Amount
txn Fee
==
&&
txn GroupIndex
int 1
==
&&
txn TypeEnum
int 1
==
&&
txn Receiver
addr ZZAF5ARA4MEC5PVDOP64JM5O5MQST63Q2KOY2FLYFLXXD3PFSNJJBYAFZM
==
&&
txn CloseRemainderTo
addr GD64YIY3TWGDMCNPP553DZPPR6LDUSFQOIJVFDPPXWEG3FVOJCCDBBHU5A
==
&&
txn Amount
int 200000
==
&&
txn FirstValid
int 55555
==
&&
txn LastValid
int 56555
==
&&
txn Lease
byte base64 y9OJ5MRLCHQj8GqbikAUKMBI7hom+SOj8dlopNdNHXI=
==
&&

3. Build Deploy Python Example

Now that the basic contract is implemented we need to build an example of instantiating the delegated signature. We will do this by creating a python file named dynamic_fee_deploy.py

import json
import time
import base64
import os
from pyteal import *
import uuid, base64
from algosdk import algod, transaction, account, mnemonic
from dynamic_fee import dynamic_fee


# utility to connect to node
def connect_to_network():
    algod_address = "http://<your-algod-node>:<your-algod-port>"
    client = algod.AlgodClient(algod_token, algod_address)
    algod_client = algod.AlgodClient(algod_token, algod_address)
    return algod_client

# utility for waiting on a transaction confirmation
def wait_for_confirmation( algod_client, txid ):
    while True:
        txinfo = algod_client.pending_transaction_info(txid)
        if txinfo.get('round') and txinfo.get('round') > 0:
            print("Transaction {} confirmed in round {}.".format(txid, txinfo.get('round')))
            break
        else:
            print("Waiting for confirmation...")
            algod_client.status_after_block(algod_client.status().get('lastRound') +1)

These are just utility functions for connecting to a node and confirming when transactions are complete.

4. Recover Accounts and Setup Transaction Parameters

In this step we add the following to dynamic_fee_deploy.py file. This code recovers account A, B, and C. It then connects to the network and sets some of the basic transaction parameters.

# group transactions           
def group_transactions() :

    # recover a account    
    passphrase = "champion weather blame curtain thing strike despair month pattern unaware feel congress carpet sniff palm predict olive talk mango toe teach jelly priority above squirrel"
    pk_account_a = mnemonic.to_private_key(passphrase)
    account_a = account.address_from_private_key(pk_account_a)

    # recover b account
    passphrase2 = "snake unveil hello input club barrel measure announce bring seed practice enact train camp enlist wear kick science now word horror month rather abstract course"
    pk_account_b = mnemonic.to_private_key(passphrase2)
    account_b = account.address_from_private_key(pk_account_b)

    # recover c account
    passphrase3 = "salmon about chair clap body sample chuckle inside adapt leg bonus afford grit floor pencil celery cherry armor solar sheriff loyal vessel stay absorb budget"
    pk_account_c = mnemonic.to_private_key(passphrase3)
    account_c = account.address_from_private_key(pk_account_c)

    # connect to node
    acl = connect_to_network()

    # get suggested parameters
    params = acl.suggested_params()
    gen = params["genesisID"]
    gh = params["genesishashb64"]
    last_round = params["lastRound"]
    fee = 1000
    amount = 200000

5. Create the Logic Signature

Next set the template variables and then call the dynamic_fee function we created in the previous steps. Next, we use python to call out to the command line to compile the TEAL program by saving the TEAL code to a file and using the execute function. Finally we read the file containing the compiled TEAL bytes back into a local variable.

    #template variables
    tmpl_fee = Int(1000)
    tmpl_lease = Bytes("base64", "y9OJ5MRLCHQj8GqbikAUKMBI7hom+SOj8dlopNdNHXI=")
    tmpl_amt = Int(200000)
    tmpl_rcv = Addr(account_c)
    tmpl_fv = Int(last_round)
    tmpl_lv = Int(last_round + 1000)
    # we are using delegation and do not want close out the account
    tmpl_cls = Addr("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ")

    teal_source = dynamic_fee(tmpl_cls,
                              tmpl_fv,
                              tmpl_lv,
                              tmpl_lease,
                              tmpl_amt,
                              tmpl_rcv).teal() 

    # compile teal
    teal_file = str(uuid.uuid4()) + ".teal"
    with open(teal_file, "w+") as f:
        f.write(teal_source)
    lsig_fname = str(uuid.uuid4()) + ".tealc"

    stdout, stderr = execute(["goal", "clerk", "compile", "-o", lsig_fname,
                            teal_file])

    if stderr != "":
        print(stderr)
        raise
    elif len(stdout) < 59:
        print("error in compile teal")
        raise

    with open(lsig_fname, "rb") as f:
        teal_bytes = f.read()
    lsig = transaction.LogicSig(teal_bytes)
    lsig.sign(pk_account_a)

The last line above signs the logic with account A’s private key. The logic signature now contains the logic and the signature. This will be used in the second of the two transactions later in this tutorial. We signed the logic because we are using a delegated signature and not a contract account. Also note that the tmpl_cls template parameter is set to the Zero Address. This means we do not want to close out account A after making the payment. If account A is created to just make this payment, then you can close it out and send all the funds to account C using a tmpl_cls value of account_c.


Learn More
- Logic Signatures
- Delegate Approval

6. Create Grouped Transactions

Next, we create two payment transactions, one for the transaction from account B to account A to cover the fee of the second transaction, and the second one for the payment from account A to account C.

    # create transaction1
    txn1 = transaction.PaymentTxn(account_b, fee, last_round, last_round+1000, gh, account_a, fee, flat_fee=True)

    tlease = base64.b64decode("y9OJ5MRLCHQj8GqbikAUKMBI7hom+SOj8dlopNdNHXI=")
    # create transaction2
    txn2 = transaction.PaymentTxn(account_a, fee, last_round, last_round+1000, gh, account_c, amount, flat_fee=True, lease=tlease)

    # get group id and assign it to transactions
    gid = transaction.calculate_group_id([txn1, txn2])
    txn1.group = gid
    txn2.group = gid

In this example we are using a hard-coded lease parameter that is verified in the contract, along with first and last valid rounds to prevent double submissions. This is critical if you are not closing out the account.

7. Sign, Group and Submit Transactions

The final step is to sign each of the two transactions. The first transaction from account B to A is signed with the private key for account B. The second transaction is signed with the logic signature created earlier in the tutorial. These two transactions are then grouped and sent to the network. The wait_for_confirmation function will print when the transactions are confirmed.

    # sign transaction1
    stxn1 = txn1.sign(pk_account_b)

    # sign transaction2
    stxn2 = transaction.LogicSigTransaction(txn2, lsig)
    #transaction.write_to_file([stxn1, stxn2], "simple.stxn")
    signedGroup =  []
    signedGroup.append(stxn1)
    signedGroup.append(stxn2)

    # send them over network
    sent = acl.send_transactions(signedGroup)
    # print txid
    print(sent)

    # wait for confirmation
    wait_for_confirmation( acl, sent) 

# Run    
group_transactions()


Learn More
- Atomic Transfers

8. Complete Examples

The complete code for dynamic_fee.py is shown below.

#!/usr/bin/env python3
# two transactions sent atomically
# Second transaction uses the teal script
from pyteal import *

#template variables
tmpl_fee = Int(1000)
tmpl_lease = Bytes("base64", "y9OJ5MRLCHQj8GqbikAUKMBI7hom+SOj8dlopNdNHXI=")
tmpl_amt = Int(200000)
tmpl_rcv = Addr("ZZAF5ARA4MEC5PVDOP64JM5O5MQST63Q2KOY2FLYFLXXD3PFSNJJBYAFZM")
tmpl_fv = Int(55555)
tmpl_lv = Int(56555)
tmpl_cls = Addr("GD64YIY3TWGDMCNPP553DZPPR6LDUSFQOIJVFDPPXWEG3FVOJCCDBBHU5A")


def dynamic_fee(tmpl_cls=tmpl_cls,
                     tmpl_fv=tmpl_fv,
                     tmpl_lv=tmpl_lv,
                     tmpl_lease=tmpl_lease,
                     tmpl_amt=tmpl_amt,
                     tmpl_rcv=tmpl_rcv):

    dynamic_fee_core = And(Global.group_size() == Int(2), Gtxn.type_enum(0) == Int(1), 
                         Gtxn.receiver(0) == Txn.sender(),
                         Gtxn.amount(0) == Txn.fee(),
                         Txn.group_index() == Int(1),
                         Txn.type_enum() == Int(1),
                         Txn.receiver() == tmpl_rcv,
                         Txn.close_remainder_to() == tmpl_cls,
                         Txn.amount() == tmpl_amt,
                         Txn.first_valid() == tmpl_fv,
                         Txn.last_valid() == tmpl_lv,
                         Txn.lease() == tmpl_lease)           

    return dynamic_fee_core

print(dynamic_fee().teal())

The complete code for dynamic_fee_deploy.py is shown below.

#/usr/bin/python3
import json
import time
import base64
import os
from pyteal import *
import uuid, base64
from algosdk import algod, transaction, account, mnemonic
from dynamic_fee import dynamic_fee


# utility to connect to node
def connect_to_network():
    algod_address = "http://<your-algod-node>:<your-algod-port>"
    client = algod.AlgodClient(algod_token, algod_address)
    algod_client = algod.AlgodClient(algod_token, algod_address)
    return algod_client

# utility for waiting on a transaction confirmation
def wait_for_confirmation( algod_client, txid ):
    while True:
        txinfo = algod_client.pending_transaction_info(txid)
        if txinfo.get('round') and txinfo.get('round') > 0:
            print("Transaction {} confirmed in round {}.".format(txid, txinfo.get('round')))
            break
        else:
            print("Waiting for confirmation...")
            algod_client.status_after_block(algod_client.status().get('lastRound') +1)

# group transactions           
def group_transactions() :

    # recover a account    
    passphrase = "champion weather blame curtain thing strike despair month pattern unaware feel congress carpet sniff palm predict olive talk mango toe teach jelly priority above squirrel"
    pk_account_a = mnemonic.to_private_key(passphrase)
    account_a = account.address_from_private_key(pk_account_a)

    # recover b account
    passphrase2 = "snake unveil hello input club barrel measure announce bring seed practice enact train camp enlist wear kick science now word horror month rather abstract course"
    pk_account_b = mnemonic.to_private_key(passphrase2)
    account_b = account.address_from_private_key(pk_account_b)

    # recover c account
    passphrase3 = "salmon about chair clap body sample chuckle inside adapt leg bonus afford grit floor pencil celery cherry armor solar sheriff loyal vessel stay absorb budget"
    pk_account_c = mnemonic.to_private_key(passphrase3)
    account_c = account.address_from_private_key(pk_account_c)

    # connect to node
    acl = connect_to_network()

    # get suggested parameters
    params = acl.suggested_params()
    gen = params["genesisID"]
    gh = params["genesishashb64"]
    last_round = params["lastRound"]
    fee = 1000
    amount = 200000


    #template variables
    tmpl_fee = Int(1000)
    tmpl_lease = Bytes("base64", "y9OJ5MRLCHQj8GqbikAUKMBI7hom+SOj8dlopNdNHXI=")
    tmpl_amt = Int(200000)
    tmpl_rcv = Addr(account_c)
    tmpl_fv = Int(last_round)
    tmpl_lv = Int(last_round + 1000)
    # we are using delegation and do not want close out the account
    tmpl_cls = Addr("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ")

    teal_source = dynamic_fee(tmpl_cls,
                              tmpl_fv,
                              tmpl_lv,
                              tmpl_lease,
                              tmpl_amt,
                              tmpl_rcv).teal() 

    # compile teal
    teal_file = str(uuid.uuid4()) + ".teal"
    with open(teal_file, "w+") as f:
        f.write(teal_source)
    lsig_fname = str(uuid.uuid4()) + ".tealc"

    stdout, stderr = execute(["goal", "clerk", "compile", "-o", lsig_fname,
                            teal_file])

    if stderr != "":
        print(stderr)
        raise
    elif len(stdout) < 59:
        print("error in compile teal")
        raise

    with open(lsig_fname, "rb") as f:
        teal_bytes = f.read()
    lsig = transaction.LogicSig(teal_bytes)
    lsig.sign(pk_account_a)


    # create transaction1
    txn1 = transaction.PaymentTxn(account_b, fee, last_round, last_round+1000, gh, account_a, fee, flat_fee=True)

    tlease = base64.b64decode("y9OJ5MRLCHQj8GqbikAUKMBI7hom+SOj8dlopNdNHXI=")
    # create transaction2
    txn2 = transaction.PaymentTxn(account_a, fee, last_round, last_round+1000, gh, account_c, amount, flat_fee=True, lease=tlease)

    # get group id and assign it to transactions
    gid = transaction.calculate_group_id([txn1, txn2])
    txn1.group = gid
    txn2.group = gid

    # sign transaction1
    stxn1 = txn1.sign(pk_account_b)

    # sign transaction2
    stxn2 = transaction.LogicSigTransaction(txn2, lsig)
    #transaction.write_to_file([stxn1, stxn2], "simple.stxn")
    signedGroup =  []
    signedGroup.append(stxn1)
    signedGroup.append(stxn2)

    # send them over network
    sent = acl.send_transactions(signedGroup)
    # print txid
    print(sent)

    # wait for confirmation
    wait_for_confirmation( acl, sent) 

# Run     
group_transactions()

python

fee

pyteal

dynamic fee

v1 api

teal

May 05, 2020