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

Getting started with Python Algorand SDK and Django

Overview

algodjango

Introduction

The purpose of this blog post is to introduce the reader to the Algorand SDK and its implementation in the Django framework.

All the source code for this blog is available in a public GitHub repository.

Requirements

This project uses a Python wrapper around Algorand SDK, so you should have Python 3 installed on your system. Also, this project uses python3-venv package for creating virtual environments and you have to install it if it’s not already installed in your system. For a Debian/Ubuntu based systems, you can do that by issuing the following command:

$ sudo apt-get install python3-venv

In order to clone the Algorand Sandbox (as opposed to just download its installation archive), you’ll also need Git distributed version control system.

Setup and run Algorand Sandbox

Let’s create the root directory named algorand where your Django project and Sandbox will reside.

cd ~
mkdir algorand
cd algorand

This project depends on Algorand Sandbox running in your computer. Use its README for the instructions on how to prepare its installation on your system. You may clone the Algorand Sandbox repository with the following command:

git clone https://github.com/algorand/sandbox.git

Start the Sandbox Docker containers by issuing the following command:

./sandbox/sandbox up

For the rest of this post, we’ll assume that Sandbox is up and running in your system.


Note

This project code implies that the Sandbox executable is in the sandbox directory which is a sibling to this project’s directory:

$ tree -L 1
.
├── algodjango
└── sandbox

If that’s not the case, then you should set SANDBOX_DIR environment variable holding sandbox directory before running this project’s Django development server:

export SANDBOX_DIR="/home/ipaleka/dev/algorand/sandbox


Create and activate Python virtual environment

Every Python-based project should run inside its own virtual environment. Create and activate one for this project with:

python3 -m venv algovenv
source algovenv/bin/activate

After successful activation, the environment name will be presented at your prompt and that indicates that all the Python package installations issued will reside only in that environment.

(algovenv) $

We’re ready now to install our project’s main dependencies: the Python Algorand SDK and Django.

(algovenv) $ pip install py-algorand-sdk Django

Version 1.5.0 of Python Algorand SDK and Django LTS version 3.2 are going to be installed by issuing that command at the time of writing this blog post.

Create Django project and the main application

Django ships with a helper shortcut utility django-admin that will help us in the creation of our project files. Create our project named algodjango with:

(algovenv) $ django-admin startproject algodjango

That command will create the root directory for our project together with the project’s directory having the same name:

(algovenv) $ tree
.
└── algodjango
    ├── algodjango
       ├── asgi.py
       ├── __init__.py
       ├── settings.py
       ├── urls.py
       └── wsgi.py
    └── manage.py

A Django project is made of applications. All the functionality of this project will reside in a single app. Change directory to the project’s root directory and use the startapp Django management command to create our app named mainapp:

(algovenv) $ cd algodjango
(algovenv) $ python manage.py startapp mainapp

That creates a new directory inside the project’s root directory with the following structure:

(algovenv) $ tree
.
├── algodjango
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── mainapp
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│      └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
└── manage.py

We need one more thing before we start writing our app code. Use your favorite editor and open the settings.py file placed in the project’s directory. Find the INSTALLED_APPS setting and prepend the list with our app’s configuration class. Afterward, that setting should look like this:

INSTALLED_APPS = [
    "mainapp.apps.MainappConfig",
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]

Now you may start the Django development server with:

(algovenv) $ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
June 20, 2021 - 20:42:22
Django version 3.2.4, using settings 'algodjango.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Ignore that warning about unapplied migrations for now and point your browser to the specified URL (http://127.0.0.1:8000/). You should see a page similar to the following one:

Django starting page

If all goes well then you’re ready to start writing code for your Algorand application.

Our first Django page

In the next few sections, we’re going to configure and create code for our first Django-based HTML page.

Application’s URL configuration

Django URL dispatcher routes predefined URLs to Django views and that URL configuration is placed in the urls.py file in the project directory. We’ll create a separate URL configuration file inside our main application directory and change the project’s URL dispatcher to include that newly created configuration:

Project’s URL dispatcher algodjango/urls.py:

from django.contrib import admin
from django.urls import include, path


urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("mainapp.urls")),
]

Create a new python module mainapp/urls.py with the following content:

from django.urls import path

from . import views


urlpatterns = [
    path("", views.index, name="index"),
]

Root page view

An empty string as a URL defines the root page of the site - in the case of our development server that would be http://127.0.0.1:8000/. From now on, every request in your browser to the root page of the site will execute code from the index function found inside mainapp/views.py module.


Note

You may implement Django views in two different ways: as function based views or as class-based views. Please take some time to familiarize yourself with the class-based views in Django because they can make your coding more powerful and more secure, but for the purpose of technology introduction to the readers, like in this blog post, the function-based views are probably easier to grasp on and so are the better choice.


Our root/index page will be used to display the list of all created standalone accounts in our application. Besides those standalone accounts, we’ll be creating and displaying accounts connected to the wallets in our app. There are other types of accounts that are entities in the Algorand blockchain, like special or multi-signature accounts, but those are out of scope for this project.

Update the views.py module with the code as follows:

from django.shortcuts import render

from .models import Account


def index(request):
    """Display all the created standalone accounts."""

    accounts = Account.objects.order_by("-created")
    context = {"accounts": accounts}
    return render(request, "mainapp/index.html", context)

So, we fetch all the account objects from the database (we’ll get to that code related to the database in a minute) and we pass them in the form of a Python dictionary as the context of the index page template rendered by Django framework (another code yet to be created).


Note

We may use the Algorand SDK for the same purpose of fetching created accounts, but keeping those records in the application database surely represents a real-world application.


Account model

Django model is a class that represents a table in your database. The default database engine of a newly created Django project is SQLite and we’re going to use it in this project. That engine creates a single file in the project’s root directory named db.sqlite3 which represents our database.

Edit the models.py module and add the following code:

from algosdk.constants import address_len
from django.db import models

from .helpers import passphrase_from_private_key


class Account(models.Model):
    """Base model class for Algorand accounts."""

    address = models.CharField(max_length=address_len)
    private_key = models.CharField(max_length=address_len + hash_len)
    created = models.DateTimeField(auto_now_add=True)

    def balance(self):
        """Return this instance's balance in microAlgos."""
        return 0

    @property
    def passphrase(self):
        """Return account's mnemonic."""
        return passphrase_from_private_key(self.private_key)


Security warning

Together with the account address, we also store its private key in our database. The reasoning behind this is that a simple user flows better suits this demonstration app than an implementation of security best practices.


As you can see, we introduced a new module named helpers.py. That module will hold all the Algorand functionality code of our project. Create that module in the main app directory with the following content:

mainapp/helpers.py

from algosdk import mnemonic


def passphrase_from_private_key(private_key):
    """Return passphrase from provided private key."""
    return mnemonic.from_private_key(private_key)

This code that uses Algorand SDK should be straightforward: we used the provided private key to get the account’s passphrase.

In order to apply those changes to our database, run the following Django management commands:

(algovenv) $ python manage.py makemigrations
(algovenv) $ python manage.py migrate

If you start the development server now, you can see that the warning from the above (about unapplied migrations) has gone.

Index page template

In order to render the accounts data in the browser, we must create a Django template of our index page. The default placement for such template is in the mainapp subdirectory of the templates directory inside our application directory:

(algovenv) $ mkdir -p mainapp/templates/mainapp

Now create the following two files in that directory:

mainapp/templates/mainapp/base.html

{% load static %}<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{% block title %}Accounts{% endblock %}</title>
    <link rel="stylesheet" type="text/css" href="{% static 'mainapp/style.css' %}">
  </head>
  <body>
    <div class="topnav">
      <a href="/"{% if request.path == '/' %} class="active"{% endif %}>Standalone accounts</a>
    </div>
    <div class="body">{% block body %}{% endblock %}</div>
  </body>
</html>

mainapp/templates/mainapp/index.html

{% extends 'mainapp/base.html' %}
{% block title %}Standalone accounts{% endblock %}
{% block body %}
  <h1>Standalone accounts list</h1>
  {% if accounts %}
  <ul>
  {% for account in accounts %}
    <li><a href="/standalone-account/{{ account.address }}">{{ account.address }}</a> : {{ account.balance }} microAlgos</li>
  {% endfor %}
  </ul>
  {% else %}
  <p>There are no standalone accounts.</p>
  {% endif %}
  <br>
  <a href="/create-standalone/">Create standalone account</a>
{% endblock %}

We’re going to use this base.html in all of our templates in this project. In short, based on the DRY principle and with the help of the Django template system, our templates will share the same HTML page headers and the navigation bar. As you might have already guessed from these templates, we extend the base template and change the defined blocks of data in the derived templates.

In the index template, we loop through the context’s accounts - the value we provided to the template in our index view. For each account, we create an unordered list item with a link to that account’s detail page (a template we haven’t created yet).

CSS styling

We need to do one more thing before we point our browser to the index page - improve the aesthetic of our pages! This part is optional and it won’t change the project’s functionality, but maybe it will help you in configuring your future Django-based Algorand’s projects.

As you can see from the base.html template, the CSS for our site is located in the style.css file. The default placement for the static files in Django projects is similar to templates:

(algovenv) $ mkdir -p mainapp/static/mainapp

Now create the style.css file in that directory with the following content:

body {
    margin: 0;
    font-family: Arial, Helvetica, sans-serif;
}

.body {
    margin: 5px;
}

.topnav {
    overflow: hidden;
    background-color: #222;
}

.topnav a {
    float: left;
    color: #fbfbfb;
    text-align: center;
    padding: 12px 12px;
    text-decoration: none;
    font-size: 16px;
}

.topnav a:hover {
    background-color: #eee;
    color: black;
}

.topnav a.active {
    background-color: #5d6d7e;
    color: white;
}

Run the development server now (python manage.py runserver) and point your browser to the root page (http://127.0.0.1:8000/). If all goes well, you should see the index that looks like this:

Index page

Congratulations on your first Django page!

Standalone account creation

All the sections and code from previous sections can be used for the general purpose of creating web pages with Django. Those introduction steps were needed to get you familiarized with the basic Django principles and now we’re finally ready to start using Algorand SDK!

We created a link in the index page to the page used for the creation of a standalone account. Now, update the app’s urls.py and views.py modules with the related code.

mainapp/urls.py

urlpatterns = [
    #
    path("create-standalone/", views.create_standalone, name="create-standalone"),
]

mainapp/views.py

from .helpers import add_standalone_account


def create_standalone(request):
    """Create standalone account."""
    private_key, address = add_standalone_account()
    account = Account.objects.create(address=address, private_key=private_key)
    context = {"account": (address, account.passphrase)}
    return render(request, "mainapp/create_standalone.html", context)

A new account record in the database is created by calling the create method on the Account’s model objects manager. All the required model field values should be provided to the create method in order to successfully create an object - in our case that would be the account’s address and private key.

Add the following function to helpers module:

mainapp/helpers.py

from algosdk import account


def add_standalone_account():
    """Create standalone account and return two-tuple of its private key and address."""
    private_key, address = account.generate_account()
    return private_key, address

We created an account and returned private key and address in order to create an account in our database.

Now create the template that will be rendered upon account creation.

mainapp/templates/mainapp/create_standalone.html

{% extends 'mainapp/base.html' %}
{% block title %}Create standalone account{% endblock %}
{% block body %}
  <h1>New standalone account</h1>
  {% if account %}
  <p>Your standalone account has been created. Please write down the following data:</p>
  <p><strong>Address</strong>: {{ account.0 }}</p>
  <p><strong>Passphrase</strong>: {{ account.1 }}</p>
  <br>
  <a href="/initial-funds/{{ account.0 }}/">Add initial funds</a>
  {% endif %}
{% endblock %}

We provided a two-tuple (a tuple consisting of two items) as the context to this template and its values are retrieved by the related indexes.


Note

For the production - contrary to what was done in this application with storing the private keys in the database - you should emphasize the fact that this would be the only time the account’s passphrase is presented to the user.


Go to the index page, click the link entitled Create standalone account and you should see the page that looks like:

Create standalone account

Initial funds for the standalone accounts

Algorand Sandbox operates either in a real network mode (using one of the Algorand’s public networks) or in a private network mode. In the former case, you may use the Testnet and add funds to your account in the Algorand dispenser, but we use the private network mode in this project which has been the default setup for the Sandbox. Algorand Sandbox creates some test accounts filled with Algos and we’ll use one of those accounts to transfer initial funds to the accounts created by our application.

The following code in helpers.py module is responsible for retrieving the address of a test account (created by Sandbox) that has enough funds (funds to transfer plus the minimum requirement of 0.1 Algo) which can be transferred to our accounts:

from algosdk.constants import microalgos_to_algos_ratio
from algosdk.v2client import indexer

INITIAL_FUNDS = 1000000000  # in microAlgos


def _indexer_client():
    """Instantiate and return Indexer client object."""
    indexer_address = "http://localhost:8980"
    indexer_token = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    return indexer.IndexerClient(indexer_token, indexer_address)


def initial_funds_sender():
    """Get the address of initially created account having enough funds."""
    return next(
        (
            account.get("address")
            for account in _indexer_client().accounts().get("accounts", [])
            if account.get("created-at-round") == 0
            and account.get("amount") > INITIAL_FUNDS + microalgos_to_algos_ratio / 10
        ),
        None,
    )

Every transaction in the Algorand blockchain has to be signed by the sender account, so we also need the passphrase of the sender account. We use Python capabilities for communicating with command-line interfaces and the Sandbox process to export that passphrase using Algorand’s goal command:

mainapp\helpers.py

import io
import os
import subprocess
from pathlib import Path


def _call_sandbox_command(*args):
    """Call and return sandbox command composed from provided arguments."""
    return subprocess.Popen(
        [_sandbox_executable(), *args],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )


def _sandbox_executable():
    """Return full path to Algorand's sandbox executable.

    The location of sandbox directory is retrieved either from the SANDBOX_DIR
    environment variable or if it's not set then the location of sandbox directory
    is implied to be the sibling of this Django project in the directory tree.
    """
    sandbox_dir = os.environ.get("SANDBOX_DIR") or str(
        Path(__file__).resolve().parent.parent.parent / "sandbox"
    )
    return sandbox_dir + "/sandbox"


def cli_passphrase_for_account(address):
    """Return passphrase for provided address."""
    process = _call_sandbox_command("goal", "account", "export", "-a", address)
    passphrase = ""
    for line in io.TextIOWrapper(process.stdout):
        parts = line.split('"')
        if len(parts) > 1:
            passphrase = parts[1]
    return passphrase

Let’s create the related URL and view:

mainapp/urls.py

urlpatterns = [
    #
    path("initial-funds/<str:receiver>/", views.initial_funds, name="initial-funds"),
]

mainapp\views.py

from django.contrib import messages
from django.shortcuts import redirect

from .helpers import (
    INITIAL_FUNDS,
    add_transaction,
    cli_passphrase_for_account,
    initial_funds_sender,
)


def initial_funds(request, receiver):
    """Add initial funds to provided standalone receiver account."""
    sender = initial_funds_sender()
    if sender is None:
        message = "Initial funds weren't transferred!"
        messages.add_message(request, messages.ERROR, message)
    else:
        add_transaction(
            sender,
            receiver,
            cli_passphrase_for_account(sender),
            INITIAL_FUNDS,
            "Initial funds",
        )
    return redirect("standalone-account", receiver)

As you can see, the URL contains a receiver address and that address is supplied as a positional argument to the view.

We’ll get to the add_transaction function in a minute, let’s first create the template where this view redirects:

mainapp\templates\mainapp\standalone_account.html

{% extends 'mainapp/base_account.html' %}

We use the base template here because we’ll later code the functionality for wallet-based accounts that will use the same base template:

mainapp\templates\mainapp\base_account.html

{% extends 'mainapp/base.html' %}
{% block title %}Account page{% endblock %}
{% block body %}
  <h1>{% block prefix %}Standalone{% endblock prefix %} account page</h1>
  {% block start %}{% endblock start %}
  <p>Address: {{ account.address }}</p>
  <p>Created: {{ account.created }}</p>
  <p>Balance: {{ account.balance }} microAlgos</p>
  <br>
  {% if messages %}
    <ul class="messages">
      {% for message in messages %}
        <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
      {% endfor %}
    </ul>
  {% endif %}
{% endblock body %}

As you can see from the presented code if the transfer of the initial funds fails then the error message is rendered. Update our styling CSS too so the error text is displayed in red:

mainapp/static/mainapp/style.css

ul.messages li.error {
    color: red;
}

ul.messages li.success {
    color: green;
}

One more thing we should do before we get to the add_transaction SDK call - we need to update the account model with the proper balance method:

mainapp\models.py

from .helpers import account_balance

class Account(models.Model):
    #

    def balance(self):
        """Return this instance's balance in microAlgos."""
        return account_balance(self.address)

The code for that account_balance helper function should be straightforward: we use predefined (by Algorand Sandbox) tokens and API endpoints to instantiate Algod client and then we use the account_info method of that client to retrieve the funds amount in microAlgos.

mainapp\helpers.py

from algosdk.v2client import algod


def _algod_client():
    """Instantiate and return Algod client object."""
    algod_address = "http://localhost:4001"
    algod_token = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    return algod.AlgodClient(algod_token, algod_address)


def account_balance(address):
    """Return funds balance of the account having provided address."""
    account_info = _algod_client().account_info(address)
    return account_info.get("amount")

Create and sign a transaction on the Algorand blockchain

Algorand and Python Algorand SDK allow you to change various arguments in the transactions creating process according to your needs, We’ll be using the suggested parameters by Algorand SDK for the purpose of this project.

mainapp\helpers.py

from algosdk.future.transaction import PaymentTxn
from algosdk.error import WrongChecksumError


def add_transaction(sender, receiver, passphrase, amount, note):
    """Create and sign transaction from provided arguments."""

    client = _algod_client()
    params = client.suggested_params()
    unsigned_txn = PaymentTxn(sender, params, receiver, amount, None, note.encode())
    try:
        signed_txn = unsigned_txn.sign(mnemonic.to_private_key(passphrase))
    except WrongChecksumError:
        return "passphrase", "Checksum failed to validate"
    except ValueError:
        return "passphrase", "Unknown word in passphrase"

    try:
        transaction_id = client.send_transaction(signed_txn)
        _wait_for_confirmation(client, transaction_id, 4)
    except Exception as err:
        return None, err  # None implies non-field error
    return "", ""

The code for _wait_for_confirmation function is still our obligation for the version 1.5.0 of the py-algorand-sdk, but it will probably be a part of the Algorand’s Python SDK in the future.

mainapp\helpers.py

def _wait_for_confirmation(client, transaction_id, timeout):
    """
    Wait until the transaction is confirmed or rejected, or until 'timeout'
    number of rounds have passed.
    Args:
        transaction_id (str): the transaction to wait for
        timeout (int): maximum number of rounds to wait
    Returns:
        dict: pending transaction information, or throws an error if the transaction
            is not confirmed or rejected in the next timeout rounds
    """
    start_round = client.status()["last-round"] + 1
    current_round = start_round

    while current_round < start_round + timeout:
        try:
            pending_txn = client.pending_transaction_info(transaction_id)
        except Exception:
            return
        if pending_txn.get("confirmed-round", 0) > 0:
            return pending_txn
        elif pending_txn["pool-error"]:
            raise Exception("pool error: {}".format(pending_txn["pool-error"]))
        client.status_after_block(current_round)
        current_round += 1
    raise Exception(
        "pending tx not found in timeout rounds, timeout value = : {}".format(timeout)
    )

And now you’re ready to transfer the initial funds to your account. Point your browser to the index page, create a standalone account, click the Add initial funds link, wait no more than 5 seconds for the transaction confirmation on the blockchain, and - voilà! - you’ve got your Algos!

Display account’s transactions

The account page should display a list of all created transactions. Edit the account model and add the following method:

mainapp\models.py

from .helpers import account_transactions


class Account(models.Model):
    #
    def transactions(self):
        """Return all the transactions involving this account."""
        return account_transactions(self.address)

The code that will return the list of provided account’s transactions is placed in the account_transactions helper function:

mainapp\helpers.py

import base64


def account_transactions(address):
    """Return all transactions involving provided address."""
    transactions = (
        _indexer_client()
        .search_transactions_by_address(address)
        .get("transactions", [])
    )
    return [
        {
            "id": tr.get("id"),
            "round": tr.get("confirmed-round"),
            "type": tr.get("tx-type"),
            "sender": tr.get("sender"),
            "receiver": tr.get("payment-transaction", {}).get("receiver"),
            "amount": tr.get("payment-transaction", {}).get("amount"),
            "note": base64.b64decode(tr.get("note", "")).decode("utf-8"),
        }
        for tr in transactions
    ]

You can see that transaction notes in the Algorand blockchain are base64 encoded, so we have to decode them in order to present them in a human-readable way.

Update the base account template by adding the following code just before the {% endblock body %} tag:

mainapp\templates\mainapp\base_account.html

  <h2>Transactions</h2>
  <table class="full-width">
  <tr>
    <th>ID</th>
    <th>Round/Type</th>
    <th>Sender/Receiver</th>
    <th>Amount</th>
    <th>Note</th>
  </tr>
  {% for transaction in account.transactions %}
  <tr>
    <td rowspan="2">{{ transaction.id }}</td>
    <td>{{ transaction.round }}</td>
    <td>{{ transaction.sender }}</td>
    <td rowspan="2">{{ transaction.amount }}</td>
    <td rowspan="2">{{ transaction.note }}</td>
  </tr>
  <tr>
    <td>{{ transaction.type }}</td>
    <td>{{ transaction.receiver }}</td>
  </tr>
  {% endfor %}
</table>

Also, add the following rules to our CSS:

mainapp/static/mainapp/style.css

table.full-width {
    width: 100%;
}

th {
    text-align: left;
}

From now on, every account’s transaction will be displayed as a table row at the end of the account page.

Transfer funds functionality

In this section, we’ll use one of the most powerful Django beasts - its forms system!

For the start, edit the base account page template once again and add the the following link just before the transactions table:

mainapp\templates\mainapp\base_account.html

<a href="/transfer-funds/{{ account.address }}/">Transfer funds</a>

The URL configuration should look like this:

mainapp/urls.py

urlpatterns = [
    #
    path("transfer-funds/<str:sender>/", views.transfer_funds, name="transfer-funds"),
]

Now create a brand new module forms.py in the main application directory and add the following code into it:

mainapp/forms.py

from algosdk.constants import address_len, mnemonic_len, note_max_length
from algosdk.encoding import is_valid_address
from django import forms
from django.core.exceptions import ValidationError
from django.forms.fields import CharField


class TransferFundsForm(forms.Form):
    """Django form for transferring microAlgos between accounts."""

    passphrase = forms.CharField(required=False)
    receiver = forms.CharField(max_length=address_len)
    amount = forms.IntegerField(min_value=1)
    note = forms.CharField(max_length=note_max_length, required=False)

    def clean_passphrase(self):
        """Example validation for the passphrase field."""
        data = self.cleaned_data["passphrase"]
        words = data.split(" ")
        if len(words) != mnemonic_len:
            raise ValidationError(
                "Passphrase must have exactly %s words!" % (mnemonic_len,)
            )
        return data

    def clean_receiver(self):
        """Example validation for the receiver field."""
        data = self.cleaned_data["receiver"]
        if not is_valid_address(data):
            raise ValidationError("Provided value is not a valid Algorand address!")
        return data

When a validation error is raised during the form submission process inside the Django framework, then the form is returned with either the field error(s) or with a non-field error. Django forms system can take care of some predefined requirements on the client-side (like the receiver or note field length in this form) before validation takes place on the server-side. In this form, we validate two fields, passphrase and receiver, for some simple requirements.

The template responsible for rendering this form looks like this:

mainapp/templates/mainapp/transfer_funds.html

{% extends 'mainapp/base.html' %}
{% block title %}Transfer funds{% endblock %}
{% block body %}
  <h1>Transfer funds</h1>
  <p><strong>Sender</strong>: {{ sender }}</p>
  <form action="/transfer-funds/{{ sender }}/" method="post">
    {% csrf_token %}
    <table>{{ form.as_table }}</table>
    <input type="submit" name="submit" value="Submit">
    <br><br><hr>
    <p><span style="color:red">WARNING: don't do this in production</span></p>
    <input type="submit" name="retrieve_passphrase" value="Retrieve passphrase">
  </form>
{% endblock %}

As you can see, we instruct the Django template system to display the form as a table and Django takes care of rendering it properly. Also, every Django form needs the Cross Site Request Forgery protection token tag for security reasons.

Also, for the production, probably a user should manually enter the account’s passphrase.

In order to display the form errors text in red, update the CSS with:

mainapp/static/mainapp/style.css

ul.errorlist li {
    color: red;
}

Now add the following to the main application views module:

mainapp/views.py

from .forms import TransferFundsForm


def transfer_funds(request, sender):
    """Transfer funds from the provided sender account to the receiver from the form."""
    if request.method == "POST":

        if "retrieve_passphrase" in request.POST:
            sender_instance = Account.instance_from_address(sender)
            request.POST = request.POST.copy()
            request.POST.update({"passphrase": sender_instance.passphrase})
            form = TransferFundsForm(request.POST)
        else:

            form = TransferFundsForm(request.POST)

            if form.is_valid():

                error_field, error_description = add_transaction(
                    sender,
                    form.cleaned_data["receiver"],
                    form.cleaned_data["passphrase"],
                    form.cleaned_data["amount"],
                    form.cleaned_data["note"],
                )
                if error_field == "":
                    message = "Amount of {} microAlgos has been successfully transferred to account {}".format(
                        form.cleaned_data["amount"], form.cleaned_data["receiver"]
                    )
                    messages.add_message(request, messages.SUCCESS, message)
                    return redirect("standalone-account", sender)

                form.add_error(error_field, error_description)

    else:

        form = TransferFundsForm()

    context = {"form": form, "sender": sender}

    return render(request, "mainapp/transfer_funds.html", context)

This code uses the same template for the GET and the failed POST requests. For the GET it instantiates an empty form, while for the POST the form is instantiated with the user’s data together with the errors, and then it is forwarded to the same template as a context variable. For this example application not suitable for production, we use a different submit button just for retrieving the passphrase from our database and then we instantiate the form with the updated data.

Server side error

In the case of a successfully validated form, the entered data is sent to the helper function where Algorand SDK tries to create a transaction. That is the second step of the validation process and if Algorand SDK can’t create a transaction from the provided data then we manually add the supplied error to the form and it is forwarded to the same template.

SDK error

If Algorand SDK successfully manages to create a transaction, then a success message is created and the request redirects to the starting account’s page. Refresh the page after few seconds and both account’s transactions should be displayed:

Standalone account transactions

Wallets creation

We created Algorand’s standalone accounts in the previous sections that aren’t connected to any wallet. Now we’re going to create both the wallets and the accounts connected to them.

Update the app’s URL configuration with all the URL we+re going to manage in this section:

mainapp/urls.py

urlpatterns = [
    #
    path("wallets/", views.wallets, name="wallets"),
    path("create-wallet/", views.create_wallet, name="create-wallet"),
    path("wallet/<str:wallet_id>/", views.wallet, name="wallet"),
    path(
        "create-wallet-account/<str:wallet_id>/",
        views.create_wallet_account,
        name="create-wallet-account",
    ),
    path(
        "wallet-account/<str:wallet_id>/<str:address>/",
        views.wallet_account,
        name="wallet-account",
    ),
]

Our navigation bar located in the base template gets another item:

mainapp\templates\mainapp\base.html

    <div class="topnav">
      <a href="/"{% if request.path == '/' %} class="active"{% endif %}>Standalone accounts</a>
      <a href="/wallets/"{% if request.path == '/wallets/' %} class="active"{% endif %}>Wallets</a>
    </div>

The wallets list page is similar to our index page where all the standalone accounts are displayed:

mainapp\templates\mainapp\wallets.html

{% extends 'mainapp/base.html' %}
{% block title %}Wallets{% endblock %}
{% block body %}
  <h1>Wallets list</h1>
  {% if wallets %}
  <ul>
  {% for wallet in wallets %}
    <li><a href="/wallet/{{ wallet.wallet_id }}">{{ wallet.name }}</a></li>
  {% endfor %}
  </ul>
  {% else %}
  <p>There are no wallets.</p>
  {% endif %}
  <br>
  <a href="/create-wallet/">Create wallet</a>
{% endblock %}

Wallets page

The page used for creating a new wallet should look familiar too:

mainapp\templates\mainapp\create_wallet.html

{% extends 'mainapp/base.html' %}
{% block title %}Create wallet{% endblock %}
{% block body %}
  <h1>Create wallet</h1>
  <form action="/create-wallet/" method="post">
    {% csrf_token %}
    <table>{{ form.as_table }}</table>
    <input type="submit" value="Submit">
  </form>
{% endblock %}

A user defines the name and password of the wallet, while Algorand SDK creates its ID. Those are the wallet properties that we record in our database:

mainapp\models.html

from algosdk.constants import hash_len


class Wallet(models.Model):
    """Model class for wallets."""

    wallet_id = models.CharField(max_length=hash_len)
    name = models.CharField(max_length=50)
    password = models.CharField(max_length=50)
    created = models.DateTimeField(auto_now_add=True)

    @classmethod
    def instance_from_id(cls, wallet_id):
        """Return model instance from provided wallet's ID."""
        try:
            return cls.objects.get(wallet_id=wallet_id)
        except ObjectDoesNotExist:
            raise Http404

To retrieve a wallet object from the wallet ID string we use the class method instance_from_id. So, a call Wallet.instance_from_id(id_string) will either return the wallet instance or will raise the error if there’s no wallet with provided id_string as its ID.

For the purpose of this introduction project and validation example, we define that a password can’t consist just of numbers or just of letters:

mainapp\forms.html

class CreateWalletForm(forms.Form):
    """Django form for creating wallets."""

    name = forms.CharField(min_length=2)
    password = forms.CharField(min_length=2)

    def clean_password(self):
        """Example validation for the password field."""
        data = self.cleaned_data["password"]
        if data.isnumeric():
            raise ValidationError("Alphanumeric value for password is required!")
        if data.isalpha():
            raise ValidationError("Alphanumeric value for password is required!")

        return data

The most interesting thing in this section is the object-oriented capability of Django models that we’re going to use for the account connected to wallets:

mainapp\models.html

class WalletAccount(Account):
    """Model class for accounts belonging to wallets."""

    wallet = models.ForeignKey(Wallet, default=None, on_delete=models.CASCADE)

Our model is inherited from the old Account model and we just add a foreign key to the wallet it belongs to. We also use the Django template system to avoid redundancy:

mainapp/templates/mainapp/wallet_account.html

{% extends 'mainapp/base_account.html' %}
{% block prefix %}Wallet{% endblock prefix %}
{% block start %}<p>Wallet: {{ wallet.name }} (ID: {{ wallet.wallet_id }})</p>{% endblock start %}

The page for displaying a wallet has a familiar structure:

mainapp/templates/mainapp/wallet.html

{% extends 'mainapp/base.html' %}
{% block title %}Wallet page{% endblock %}
{% block body %}
  <h1>Wallet page</h1>
  <p>Name: {{ wallet.name }}</p>
  <p>ID: {{ wallet.wallet_id }}</p>
  <p>Created: {{ wallet.created }}</p>

  {% if messages %}
    <ul class="messages">
      {% for message in messages %}
        <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
      {% endfor %}
    </ul>
  {% endif %}

  <h2>Wallet accounts</h2>
  {% for account in wallet.walletaccount_set.all %}
    <li><a href="/wallet-account/{{ wallet.wallet_id }}/{{ account.address }}">{{ account.address }}</a> : {{ account.balance }} microAlgos</li>
  {% endfor %}
  <br>
  <a href="/create-wallet-account/{{ wallet.wallet_id }}/">Create wallet account</a>

{% endblock %}

The views and related code involved in the wallet creating and displaying processes follow the practice already presented in this blog post:

mainapp/views.py

from .forms import CreateWalletForm
from .helpers import add_wallet, get_wallet


def create_wallet(request):
    """Create wallet from the form data."""
    if request.method == "POST":

        form = CreateWalletForm(request.POST)

        if form.is_valid():

            wallet_id = add_wallet(
                form.cleaned_data["name"], form.cleaned_data["password"]
            )
            if wallet_id != "":
                Wallet.objects.create(
                    wallet_id=wallet_id,
                    name=form.cleaned_data["name"],
                    password=form.cleaned_data["password"],
                )
                message = "Wallet with name '{}' and ID '{}' has been created.".format(
                    form.cleaned_data["name"], wallet_id
                )
                messages.add_message(request, messages.SUCCESS, message)
                return redirect("wallet", wallet_id)

            form.add_error(None, "Wallet is not created!")

    else:
        form = CreateWalletForm()

    context = {"form": form}

    return render(request, "mainapp/create_wallet.html", context)


def create_wallet_account(request, wallet_id):
    """Create account in the wallet with provided ID."""
    model = Wallet.instance_from_id(wallet_id)
    wallet = get_wallet(model.name, model.password)
    address = wallet.generate_key()
    WalletAccount.objects.create(wallet=model, address=address)
    message = "Address '{}' has been created in the wallet.".format(address)
    messages.add_message(request, messages.SUCCESS, message)
    return redirect("wallet", wallet_id)


def wallet(request, wallet_id):
    """Display information of the wallet with provided ID."""
    context = {"wallet": Wallet.instance_from_id(wallet_id)}
    return render(request, "mainapp/wallet.html", context)


def wallet_account(request, wallet_id, address):
    """Display information of the wallet account with provided address."""
    context = {
        "wallet": Wallet.instance_from_id(wallet_id),
        "account": Account.instance_from_address(address),
    }
    return render(request, "mainapp/wallet_account.html", context)


def wallets(request):
    """Display all the created wallets."""
    wallets = Wallet.objects.order_by("name")
    context = {"wallets": wallets}
    return render(request, "mainapp/wallets.html", context)

We use the Key Management Daemon client for the purpose of creating and retrieving the wallets from the Algorand blockchain:

mainapp/helpers.py

from algosdk import kmd
from algosdk.wallet import Wallet


def _kmd_client():
    """Instantiate and return kmd client object."""
    kmd_address = "http://localhost:4002"
    kmd_token = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    return kmd.KMDClient(kmd_token, kmd_address)

def add_wallet(name, password):
    """Create wallet and return its ID."""
    try:
        wallet = Wallet(name, password, _kmd_client())
    except:
        return ""
    return wallet.id

def get_wallet(name, password):
    """Return wallet object from provided arguments."""
    return Wallet(name, password, _kmd_client())

A wallet object is returned if a user instantiates the Wallet class with a wallet name and a password. If such a wallet doesn’t exist then Algorand SDK simply creates it with that name and password.

Let’s do one more thing before we move to the next section. If we now visit project’s root/index page then both the standalone and wallet-based accounts will show up. Update the index view and use the Django object relational mapper to exclude the wallet-based accounts:

We use the base template as we’re going to code the wallet-based accounts functionality in this project that will use the same base template:

mainapp\templates\mainapp\base_account.html

{% extends 'mainapp/base.html' %}
{% block title %}Account page{% endblock %}
{% block body %}
  <h1>{% block prefix %}Standalone{% endblock prefix %} account page</h1>
  {% block start %}{% endblock start %}
  <p>Address: {{ account.address }}</p>
  <p>Created: {{ account.created }}</p>
  <p>Balance: {{ account.balance }} microAlgos</p>
  <br>
  {% if messages %}
    <ul class="messages">
      {% for message in messages %}
        <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
      {% endfor %}
    </ul>
  {% endif %}
{% endblock body %}

As you can see from the presented code, if the initial funds transfer fails then the error message is rendered. Update our styling CSS too so the error text is displayed in red:

mainapp/static/mainapp/style.css

ul.messages li.error {
    color: red;
}

ul.messages li.success {
    color: green;
}

One more thing we should do before we get to the add_transaction SDK call - we need to update the account model with the proper balance method:

mainapp\models.py

from .helpers import account_balance

class Account(models.Model):
    #

    def balance(self):
        """Return this instance's balance in microAlgos."""
        return account_balance(self.address)

The code for that account_balance helper function should be straightforward: we use predefined (by Algorand Sandbox) tokens and API endpoints to instantiate Algod client and then we use the account_info method of that client to retrieve the amount in microAlgos.

mainapp\helpers.py

from algosdk.v2client import algod


def _algod_client():
    """Instantiate and return Algod client object."""
    algod_address = "http://localhost:4001"
    algod_token = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    return algod.AlgodClient(algod_token, algod_address)


def account_balance(address):
    """Return funds balance of the account having provided address."""
    account_info = _algod_client().account_info(address)
    return account_info.get("amount")

Assets creation

In this section, we’re going to create the Algorand assets in our application.

The URL configuration for the assets-related pages looks like this:

mainapp/urls.py

urlpatterns = [
    #
    path("assets/", views.assets, name="assets"),
    path("create-asset/", views.create_asset, name="create-asset"),
]

Update the base template and add another item to the navigation bar:

mainapp\templates\mainapp\base.html

    <div class="topnav">
      <a href="/"{% if request.path == '/' %} class="active"{% endif %}>Standalone accounts</a>
      <a href="/wallets/"{% if request.path == '/wallets/' %} class="active"{% endif %}>Wallets</a>
      <a href="/assets/"{% if request.path == '/assets/' %} class="active"{% endif %}>Assets</a>
    </div>

The assets list page follows the same structure as the other Algorand entity pages we have created before. The assets are shown in the form of table where each row represents a single asset from our database:

mainapp\templates\mainapp\assets.html

{% extends 'mainapp/base.html' %}
{% block title %}Assets{% endblock %}
{% block body %}
  <h1>Assets list</h1>
  {% if assets %}
  <table class="full-width">
  <tr>
    <th>ID/Frozen</th>
    <th>Name/Unit</th>
    <th>Total/Decimals</th>
    <th>Url/Metadata</th>
    <th>Creator</th>
    <th>Manager/Reserve</th>
    <th>Freeze/Clawback</th>
  </tr>
  {% for asset in assets %}
  <tr>
    <td>{{ asset.asset_id }}</td>
    <td>{{ asset.name }}</td>
    <td>{{ asset.total }}</td>
    <td>{{ asset.url }}</td>
    <td rowspan="2">{{ asset.creator }}</td>
    <td>{{ asset.manager }}</td>
    <td>{{ asset.freeze }}</td>
  </tr>
  <tr>
    <td>{{ asset.frozen }}</td>
    <td>{{ asset.unit }}</td>
    <td>{{ asset.decimals }}</td>
    <td>{{ asset.metadata }}</td>
    <td>{{ asset.reserve }}</td>
    <td>{{ asset.clawback }}</td>
  </tr>
  {% endfor %}
  </table>
  {% else %}
  <p>There are no assets.</p>
  {% endif %}
  <br>
  <a href="/create-asset/">Create asset</a>
{% endblock %}

The template for the asset creation uses a familiar structure with a form rendered as a table:

mainapp\templates\mainapp\create_asset.html

{% extends 'mainapp/base.html' %}
{% block title %}Create asset{% endblock %}
{% block body %}
  <h1>Create asset</h1>
  <form action="/create-asset/" method="post">
    {% csrf_token %}
    <table>{{ form.as_table }}</table>
    <input type="submit" name="submit" value="Submit">
    <br><br><hr>
    <p><span style="color:red">WARNING: don't do this in production</span></p>
    <input type="submit" name="retrieve_passphrase" value="Retrieve passphrase">
  </form>
{% endblock %}

The asset model is responsible for saving the same asset properties we already presented in the assets list page template:

mainapp\models.py

from algosdk.constants import max_asset_decimals, metadata_length


class Asset(models.Model):
    """Model class for Algorand assets."""

    asset_id = models.IntegerField(blank=False)
    creator = models.CharField(max_length=address_len, blank=False)
    name = models.CharField(max_length=hash_len, blank=True)
    unit = models.CharField(max_length=8, blank=True)
    total = models.IntegerField(
        blank=False,
        validators=[MinValueValidator(1)],
    )
    decimals = models.IntegerField(
        blank=False,
        validators=[MinValueValidator(0), MaxValueValidator(max_asset_decimals)],
    )
    frozen = models.BooleanField(blank=False, default=False)
    url = models.URLField(blank=True)
    metadata = models.CharField(max_length=metadata_length, blank=True)
    manager = models.CharField(max_length=address_len, blank=True)
    reserve = models.CharField(max_length=address_len, blank=True)
    freeze = models.CharField(max_length=address_len, blank=True)
    clawback = models.CharField(max_length=address_len, blank=True)
    created = models.DateTimeField(auto_now_add=True)

The most interesting part in this section is the Django model form that we use to display, validate and save entered data to the database:

mainapp\forms.py

class CreateAssetForm(forms.models.ModelForm):
    """Django model form for creating Algorand assets."""

    passphrase = CharField(required=False)

    class Meta:
        model = Asset
        fields = (
            "creator",
            "name",
            "unit",
            "total",
            "decimals",
            "frozen",
            "url",
            "metadata",
            "manager",
            "reserve",
            "freeze",
            "clawback",
        )

    def _clean_address(self, field):
        """Base method for validation of fields holding Algorand address."""
        data = self.cleaned_data[field]
        if not is_valid_address(data):
            raise ValidationError("Provided value is not a valid Algorand address!")
        return data

    def clean_creator(self):
        return self._clean_address("creator")

    def clean_manager(self):
        return self._clean_address("manager")

    def clean_reserve(self):
        return self._clean_address("reserve")

    def clean_freeze(self):
        return self._clean_address("freeze")

    def clean_clawback(self):
        return self._clean_address("clawback")

The additional field passphrase has not been a part of the asset’s database record and it is used only to sign the transaction after the form is validated. For validation of the fields based on the Algorand address, we pass a field name to the base _clean_address method that uses Algorand SDK to validate the address.

Asset creation

The majority of the code in the views related to assets displaying and creating should be quite familiar to you by now:

mainapp\views.py

from .forms import CreateAssetForm
from .helpers import add_asset
from .models import Asset


def assets(request):
    """Display all the created assets."""
    assets = Asset.objects.order_by("-created")
    context = {"assets": assets}
    return render(request, "mainapp/assets.html", context)


def create_asset(request):
    """Create Algorand asset from the form data."""
    if request.method == "POST":

        if "retrieve_passphrase" in request.POST:
            creator = Account.instance_from_address(request.POST.get("creator"))
            request.POST = request.POST.copy()
            request.POST.update({"passphrase": creator.passphrase})
            form = CreateAssetForm(request.POST)
        else:

            form = CreateAssetForm(request.POST)

            if form.is_valid():

                asset_id, error_description = add_asset(form.cleaned_data)
                if error_description == "":

                    asset = form.save(commit=False)
                    asset.asset_id = asset_id
                    asset.save()

                    message = "Asset {} has been successfully created!".format(
                        form.cleaned_data["name"]
                    )
                    messages.add_message(request, messages.SUCCESS, message)
                    return redirect("assets")

                form.add_error(None, error_description)

    else:
        form = CreateAssetForm()

    context = {"form": form}

    return render(request, "mainapp/create_asset.html", context)

The only part that probably needs further explanation is the form-saving routine without committing. We use the form’s save method for both the model-level validation purpose and to get back the wallet model instance. We then update that instance with the wallet ID provided to us by Algorand SDK and then we finally save the record in the database.

The add_asset helper function instantiates the AssetConfigTxn class with the user data provided by the form. The rest of the code is similar to the code we used to transfer the Algos between accounts:

mainapp\helpers.py

from algosdk.error import WrongMnemonicLengthError
from algosdk.future.transaction import AssetConfigTxn


def add_asset(data):
    """Create asset from provided data dictionary."""
    client = _algod_client()
    params = client.suggested_params()
    unsigned_txn = AssetConfigTxn(
        sp=params,
        sender=data.get("creator"),
        asset_name=data.get("name"),
        unit_name=data.get("unit"),
        total=data.get("total"),
        decimals=data.get("decimals"),
        default_frozen=data.get("frozen"),
        url=data.get("url"),
        manager=data.get("manager"),
        reserve=data.get("reserve"),
        freeze=data.get("freeze"),
        clawback=data.get("clawback"),
        strict_empty_address_check=False,
    )
    # Sign with secret key of creator
    try:
        signed_txn = unsigned_txn.sign(mnemonic.to_private_key(data.get("passphrase")))
    except WrongMnemonicLengthError as err:
        return None, err

    try:
        transaction_id = client.send_transaction(signed_txn)
        _wait_for_confirmation(client, transaction_id, 4)
    except Exception as err:
        return None, err  # None implies non-field error

    try:
        info = client.pending_transaction_info(transaction_id)
        asset_id = info.get("asset-index")
        return asset_id, ""

    except Exception as err:
        return None, err

Assets page

Search transactions

In the last section of this blog post, we’re going to create a basic form that uses the Algorand SDK indexer client to perform searching for the transactions on the blockchain.

For the start, update the navigation bar in the base template with the fourth item:

mainapp\templates\mainapp\base.html

    <div class="topnav">
      <a href="/"{% if request.path == '/' %} class="active"{% endif %}>Standalone accounts</a>
      <a href="/wallets/"{% if request.path == '/wallets/' %} class="active"{% endif %}>Wallets</a>
      <a href="/assets/"{% if request.path == '/assets/' %} class="active"{% endif %}>Assets</a>
      <a href="/search/"{% if request.path == '/search/' %} class="active"{% endif %}>Search transactions</a>
    </div>

The template renders a form the way we’ve already used in this project, but this time the same template uses transactions context variable to display the found transactions:

mainapp\templates\mainapp\base.html

{% extends 'mainapp/base.html' %}
{% block title %}Search transactions{% endblock %}
{% block body %}
  <h1>Search transactions</h1>
  <form action="/search/" method="post">
    {% csrf_token %}
    <table>{{ form.as_table }}</table>
    <input type="submit" value="Submit">
  </form>
  <br>
  {% for transaction in transactions %}
    {{ transaction }}
    <br><br>
  {% endfor %}
{% endblock %}

You should add one more entry in the main application’s URL configuration module:

mainapp/urls.py

urlpatterns = [
    #
    path("search/", views.search, name="search"),
]

The form used to accept users data brings some new elements we haven’t yet used in this blog post:

mainapp/forms.py

class SearchTransactionsForm(forms.Form):
    """Django form for searching Algorand transactions."""

    note_prefix = forms.CharField(required=False)
    address = forms.CharField(required=False)
    asset_id = forms.CharField(required=False, label="Asset ID")
    txid = forms.CharField(required=False, label="Transaction ID")
    block = forms.CharField(required=False, label="Round")
    txn_type = forms.ChoiceField(
        required=False,
        choices=[
            ("", "All types"),
            ("pay", "Payment"),
            ("keyreg", "Key registration"),
            ("acfg", "Asset configuration"),
            ("axfer", "Asset freeze"),
            ("afrz", "Asset transfer"),
        ],
        label="Transaction type",
    )

    def clean_note_prefix(self):
        """Algorand SDK needs bytes-like object for note prefix."""
        data = self.cleaned_data["note_prefix"]
        return data.encode("ascii") if data != "" else data

    def clean(self):
        """Ensure at least one field is non-empty."""
        cleaned_data = super().clean()
        if all(val == "" for val in cleaned_data.values()):
            raise ValidationError("You must fill at least one field!")

        return cleaned_data

As you’re going to see in the helper function connected to the search capability, we need the exact field names like in Algorand SDK for the search functionality of the application. As those field names aren’t always human-comprehensible, we added the label named argument for those fields that replace the generic label created from the field name.

Another new element is the use of choices for the txn_type field - we limit the input to a list of pre-defined types to reduce the possibility of error.

And finally, we introduce the non-field/form level of validation with the clean method. The validation error raises for a condition created not from just one field, but rather from multiple fields (or from all of them in this case).

mainapp/views.py

from .forms import SearchTransactionsForm
from .helpers import search_transactions


def search(request):
    """Search transactions based on criteria created from the form data."""
    transactions = []
    if request.method == "POST":

        form = SearchTransactionsForm(request.POST)

        if form.is_valid():

            transactions = search_transactions(form.cleaned_data)

    else:
        form = SearchTransactionsForm()

    context = {"form": form, "transactions": transactions}

    return render(request, "mainapp/search.html", context)

The view that renders our looks familiar. As you can see, we pass the empty list as the template context in the case of a GET request or an invalid POST request. The same happens when Algorand SDK can’t find any result based on our search criteria.

mainapp/helpers.py

def search_transactions(data):
    """Search transaction based on criteria from provided data."""
    criteria = {key: val for key, val in data.items() if val != ""}
    transactions = (
        _indexer_client().search_transactions(**criteria).get("transactions", [])
    )
    # Decode notes to human-readable strings before returning the list
    for tr in transactions:
        tr["note"] = base64.b64decode(tr.get("note", "")).decode("utf-8")
    return transactions

The Algorand’s Indexer search_transactions method accepts various named arguments used to search on the blockchain. We use that fact to create non-empty pairs from our form field names and corresponding values.

Search transactions

Conclusion

We introduced the reader to some basic concepts of Python Algorand SDK and the Django framework. Use the presented material and publicly available code to start working on your own projects based on the Algorand blockchain.

For any question or suggestion you may have, use the issues section of the algodjango project or use the Algorand Discord channel.

Enjoy your coding!