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 · 30 minutes

Algo Builder Tutorial Part 2: Stateless Smart Contracts

This is the second tutorial from the Algo Builder series:

In this tutorial we present how our algob helps the developers to wield the power of Smart Contracts (both stateful and stateless) with TEAL and PyTEAL.

Requirements

  • Good knowledge about Blockchain and Algorand.
  • Detailed introduction to algob
  • Detailed knowledge about assets, accounts, transactions and signatures.
  • Brief introduction to Smart Contracts

Steps

1. Introduction to algobDeployer

By now it must be clear how our package algob provides so many wonderful interfaces which make the need of writing lengthy complex code perish. One of these interfaces is AlgobDeployer.

AlgobDeployer makes the task of deploying (creation) and running (execution) transactions easier. Deployer as a single entity provides the functionality of handling several accounts, performing different types of transactions, storing mapping of several checkpoints in the network and the full functionality of Smart Contracts (Stateful or Logic Signature Account.

Deployer is also efficient in handling PyTEAL files. During the process of funding a contract account, deploying Stateful Smart Contracts (SSC) or making delegated logic signatures, a deployer will browse smart contract code and use it when interacting with the blockchain and smart contracts.

You can learn more about AlgobDeployer in more detail in our documentation.

In this tutorial we will focus on stateless smart contract (also called logic signatures).

2. Creating TEAL files

In the algob project structure, the TEAL and PyTEAL code files are always placed in the /assets directory:

algob project:
├── assets
   ├── TEAL files
   ├── PyTEAL files
├── scripts
   ├── deploy.ts
   ├── run.ts
├── package.json

Check a simple tutorial on writing simple smart contracts to learn more about TEAL.

Simple Example

We will create a simple stateless smart contract which will check if a fee is smaller than 10k micro Algo. Put the code below in /assets/feecheck.teal:

txn Fee
int 10000
<=

To deploy the contract use the following code in one of your scripts (in ./scripts):

let lsig = await deployer.loadLogic('feecheck.teal', []); 

The first argument is the smart contract file with the source code. Algob will search for it in ./assets directory. The second argument is list of arguments used when making a logic signature transaction. In this example we don’t provide any transaction so the list is empty.

Note

Each argument must be either a number or byte array.

Now you can use lsig whenever a LogicSig object is expected. Example:

// opt in lsig to ASA
await deployer.optInLsigToASA('asa-name-or-id', lsig, { totalFee: 1000 });

// execute transaction:
const { executeTransaction } = require('@algo-builder/algob');
await executeTransaction(deployer, {
  type: types.TransactionType.TransferAsset,
  sign: types.SignType.SecretKey,
  fromAccount: ownerAccount,
  toAccountAddr: lsig.address(),
  amount: 1e5,
  assetID: goldAssetID,
  payFlags: { totalFee: 1000 }
});

The when running $ algob deploy ./scripts/yourscript.js, the algob will automatically compile and cache the TEAL program for you. The compilation will be run only when the TEAL code has been modified. You can find the compiled code in ./artifacts/cache.

You can also compile all TEAL code with:

$ algob compile

3. Creating PyTEAL

PyTEAL acts as an interface between high level programming and assembly level code. Developers can use PyTEAL to design the logic behind smart contracts purely in Python.

algob supports PyTEAL as well. All you need is Python3 and pipenv. The latter is a tool that aims to bring the best of all packaging worlds and dependency management. To use pipenv:

  • Clone and enter to our repository
    $ cd ~/ && git clone https://github.com/scale-it/algo-builder && cd algo-builder
  • Create a virtualenv and setup all dependencies
    $ pipenv sync
  • Activate the virtual environment
    $ pipenv shell

Alternatively you can create your own setup (using virtualenv or global installation of PyTEAL). The only thing we require is Python3 with PyTEAL.

Now you can put your PyTEAL code in assets directory and use it as it any other TEAL code. The algob will automatically detect that it is a PyTEAL code and it will first compile it to TEAL and then compile the TEAL code.

4. Full Example

We will implement the Hash Time Lock contract in PyTEAL based on the developer.algorand.org templates. Read the linked document - it’s necessary to understand the next section.

Smart Contract Code

First, let’s declare template variables:

john = Addr("2UBZKFR6RCZL7R24ZG327VKPTPJUPFM6WTG7PJG2ZJLU234F5RGXFLTAKA")
master = Addr("WWYNX3TKQYVEREVSW6QQP3SXSFOCE3SKUSEIVJ7YAGUPEACNI5UGI4DZCE")
fee = 10000
hash_image = "QzYhq9JlYbn2QdOMrhyxVlNtNjeyvyJc/I8d8VAGfGc="
timeout = 2000

master is our default account we use in algob private net. The secret, which will protect the escrow funds is "hero wisdom green split loop element vote belt". It will be provided as an argument to the stateless smart contract, which will check if it’s hash equal to the stored one (hash_image).

Here is the PyTEAL code. The TMPL_* are the placeholder parameters, which can are provided as a Python function arguments:

from pyteal import *

john = Addr("2UBZKFR6RCZL7R24ZG327VKPTPJUPFM6WTG7PJG2ZJLU234F5RGXFLTAKA")
master = Addr("WWYNX3TKQYVEREVSW6QQP3SXSFOCE3SKUSEIVJ7YAGUPEACNI5UGI4DZCE")
fee = 10000
hash_image = "QzYhq9JlYbn2QdOMrhyxVlNtNjeyvyJc/I8d8VAGfGc="
timeout = 2000

def htlc(TMPL_RCV,
        TMPL_OWN,
        TMPL_FEE,
        TMPL_HASHIMG,
        TMPL_HASHFN,
        TMPL_TIMEOUT):

    # First, check that the fee of this transaction is less than or equal to TMPL_FEE
    fee_check = Txn.fee() < Int(TMPL_FEE)

    # Next, check that this is a payment transaction.
    pay_check = Txn.type_enum() == TxnType.Payment

    # Next, check that the Receiver field for this transaction is empty
    # Because this contract can approve transactions that close out its entire balance,
    # it should never have a receiver.
    rec_field_check = Txn.receiver() == Global.zero_address()

    # Next, check that the Amount of algos transferred is 0. This is for the same reason as
    # above: we only allow transactions that close out this account completely, which
    # having a non-zero-address CloseRemainderTo will handle for us.
    amount_check = Txn.amount() == Int(0)

    # Always verify that the RekeyTo property of any transaction is set to the ZeroAddress
    # unless the contract is specifically involved ina rekeying operation.
    rekey_check = Txn.rekey_to() == Global.zero_address()

    # fold all the above checks into a single boolean.
    common_checks = And(
        fee_check,
        pay_check,
        rec_field_check,
        amount_check,
        rekey_check
    )

    # Payout scenarios : At this point in the execution, there is one boolean variable on the
    # stack that must be true in order for the transaction to be valid. The checks we have done
    # above apply to any transaction that may be approved by this script.We will now check if we
    # are in one of the two payment scenarios described in the functionality section."""

    # Scenario 1: Hash preimage has been revealed
    # First, check that the CloseRemainderTo field is set to be the TMPL_RCV address.
    recv_field_check = Txn.close_remainder_to() == TMPL_RCV

    # Next, we will check that arg_0 is the correct preimage for TMPL_HASHIMG under TMPL_HASHFN.
    preimage_check = TMPL_HASHFN(Arg(0)) == Bytes("base64", TMPL_HASHIMG)

    #Fold the "Scenario 1" checks into a single boolean.
    scenario_1 = And(recv_field_check, preimage_check)


    # Scenario 2: Contract has timed out
    # First, check that the CloseRemainderTo field is set to be the TMPL_OWN address
    # (presumably initialized to be the original owner of the funds).
    owner_field_check = Txn.close_remainder_to() == TMPL_OWN

    # Next, check that this transaction has only occurred after the TMPL_TIMEOUT round.
    timeout_check = Txn.first_valid() > Int(TMPL_TIMEOUT)

    #Fold the "Scenario 2" checks into a single boolean.
    scenario_2 = And(owner_field_check, timeout_check)

    # At this point in the program's execution, the stack has three values. At the base of the
    # stack is a boolean holding the results of the initial transaction validity checks.
    # This is followed by two booleans indicating the results of the scenario 1 and 2 checks.

    # We want to approve this transaction if we are in scenario 1 or 2.
    # So we logically OR the results of those checks together.
    # Finally, we logically AND the scenario checks with the initial checks.
    # At this point, the stack contains just one value: a boolean indicating
    # whether or not it has been approved by this contract.
    return And(Or(scenario_1, scenario_2), common_checks)


# print the compiled TEAL to the stdout:    
if __name__ == "__main__":
    print(compileTeal(htlc(john, master, fee, hash_image, Sha256, timeout), Mode.Signature))

We save this file in assets/htlc.py

Run the Contract

Let’s create the contract account and fund it with some ALGOs. The stateless smart contracts program code is not installed into the blockchain. Instead, the compiled code has to be provided on every call and used executed to validate certain conditions. That’s why we also call them Logic Signatures (short lsig)- it’s a program (logic) providing a signature. Address of an account referenced by a lsig is a hash of it’s compiled code.

Let’s create our script.

Create user accounts

const masterAccount = deployer.accountsByName.get('master-account');
const john = deployer.accountsByName.get('john');

// let's make sure john account is active and it has enough balance
const txnParams = mkTxnParams(masterAccount, john.addr, 4e6, {}, { note: 'funding account' });
txnParams.sign = types.SignType.SecretKey;
await executeTransaction(deployer, txnParams);

In the code above we declare user accounts and fund john account. masterAccount is the default account used in algob private net.

Interact with the contract

First we need to put some money into the contract. master account will fund it:

// setup a contract account and send 1 ALGO from master
await deployer.fundLsig('htlc.py', {
  funder: masterAccount,
  fundingMicroAlgo: 1e6 // 1 Algo
},
{ closeRemainderTo: john.addr }, []);

We can create a checkpoint to store the transaction log:

await deployer.addCheckpointKV('User Checkpoint', 'Fund Contract Account');

Now, we will use john account to withdraw from the smart contract. First we will test it with a wrong secret. We will also reuse and update the transaction parameters created earlier (txnParams) to instrument algob to create an lsig transaction:

let wrongSecret = "wrong";
let contract = await deployer.loadLogic('htlc.py', [stringToBytes(wrongSecret)]);
let contractAddress = contract.address();

txnParams.fromAccount = { addr: contractAddress };
txnParams.sign = types.SignType.LogicSignature;
txnParams.toAccountAddr = globalZeroAddress;
txnParams.amountMicroAlgos = 0;
txnParams.lsig = contract;
txnParams.payFlags = { totalFee: 1000, closeRemainderTo: john.addr };
await executeTransaction(deployer, txnParams);

The last statement will fail and we will see an appropriate log in the screen, as well as in the artifacts:

smart-contract source "htlc.py" didn't change, skipping.
Transaction Failed Error: cannot POST /v2/transactions (400)
    at Response.toError (/home/robert/projects/algorand/algo-builder/node_modules/superagent/src/node/response.js:95:15)
    at Response._setStatusProperties (/home/robert/projects/algorand/algo-builder/node_modules/superagent/src/response-base.js:126:48)
    at new Response (/home/robert/projects/algorand/algo-builder/node_modules/superagent/src/node/response.js:41:8)
    at Request._emitResponse (/home/robert/projects/algorand/algo-builder/node_modules/superagent/src/node/index.js:928:20)
    at fn (/home/robert/projects/algorand/algo-builder/node_modules/superagent/src/node/index.js:1130:38)
    at IncomingMessage.<anonymous> (/home/robert/projects/algorand/algo-builder/node_modules/superagent/src/node/parsers/json.js:19:7)
    at IncomingMessage.emit (events.js:327:22)
    at IncomingMessage.EventEmitter.emit (domain.js:486:12)
    at endReadableNT (_stream_readable.js:1327:12)
    at processTicksAndRejections (internal/process/task_queues.js:80:21) {
  status: 400,
...
transaction 7ZDSTDC6Q2CWEWTDNETF2RYBPMXEDRMTOPZIC5GISKQRJRJQWSGQ: rejected by logic"}

As we can see the transaction is not validated because the lsig verification didn’t pass (we provided the wrong secret). The first line (“smart-contract source “htlc.py” didn’t change, skipping.”) says that we don’t need to recompile the smart contract and algob will use it’s cached version.

Now, let’s provide the right secret:

const secret = 'hero wisdom green split loop element vote belt';
contract = await deployer.loadLogic('htlc.py', [stringToBytes(secret)]);
contractAddress = contract.address();

txnParams.lsig = contract;
await executeTransaction(deployer, txnParams);

This time we will see a success message:

{
  'closing-amount': 999000,
  'confirmed-round': 24,
  'pool-error': '',
  txn: {
    lsig: { arg: [Array], l: [Uint8Array] },
    txn: {
      close: [Uint8Array],
      fee: 1000,
      fv: 22,
      gen: 'private-v1',
      gh: [Uint8Array],
      lv: 1022,
      snd: [Uint8Array],
      type: 'pay'
    }
  }
}

Code Repository

You can see and run the example above directly from our examples repository.

5. Guides

Check our PyTEAL guide that explains more about PyTEAL integration with algob.
It’s worth to mention a functionality we drafted to pass template parameters from our JavaScript scripts into a PyTEAL code. Check the External Parameters Support section to see how you can use it to interface with PyTEAL code, rather than changing the values in the PyTEAL code everytime.

Next

In the next tutorial we will learn how to build stateful smart contracts.