Solutions
No Results

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

Creating a demo wallet using Flask

Overview

EditorImages/2021/07/07 21:33/home.png

In this project we’ll go through the steps of putting together a basic Algorand wallet website using Flask. This tutorial will feature:

  • Sending transactions
  • Logging in with a 25-word mnemonic
  • Creating an account
  • Viewing all sent/received transactions
  • Creating and displaying assets
  • Filtering displayed assets and transactions

As a warning, this project has not been audited and should not be used in a production environment. If you would like to see the final project, or if you’re unsure about the file structure, you can check out the GitHub repository.

Introduction and Setup

Flask is most well known for being able to quickly put together websites. However, it is capable of more sturdy features. We’ll achieve this by also using Flask-WTF and Flask-Login.

Along with the Python Algorand SDK, our requirements file should look like this:

flask
flask-login
flask-wtf
py-algorand-sdk

I also highly recommend using python-dotenv to store sensitive information and avoid committing them to a public repository. Flask also has inbuilt functionality with python-dotenv, allowing Flask environment variables to be automatically stored in any .env file. However, in this solution I’ll put the values in the code, which you can replace.

In order to access the Algorand network, you need to be running a node. You can do this yourself, but it might be easier to grab an API key from a third party, such as at https://developer.purestake.io/login.

You will need to use a virtual environment to correctly use Flask. This can be done as follows (these commands use Windows):

  • Create a directory for your project
  • Inside your project directory, type in py -m venv venv
  • Activate your virtual environment with .\venv\Scripts\activate

Creating the initial Flask application

To begin, first create a file structure as shown:

.
└── your_project_name/
    ├── venv/
    ├── your_application_name/
    │    ├── __init__.py
    │    └── views.py
    └── requirements.txt
    └── wsgi.py

requirements.txt should be filled with the requirements mentioned earlier. We can then install this with pip install -r requirements.txt. Make sure your virtual environment is activated when doing so.

The remaining files should be filled out as such:

your_application_name/__init__.py:

from flask import Flask

from . import views

def create_app():
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "put any long random string here"
    app.register_blueprint(views.main_bp)
    return app

your_application_name/views.py:

from flask import Blueprint

main_bp = Blueprint(
    'main_bp', __name__,
    template_folder='templates',
    static_folder='static'
)

@main_bp.route('/')
def index():
    return "Algorand Balance"

wsgi.py:

from your_application_name import create_app

app = create_app()

if __name__ == "__main__":
    app.run(host='0.0.0.0')

This can be run with flask run. It is important to note that flask run will by default look for a file called wsgi.py so don’t rename it.

If you haven’t used Flask before, this may seem intimidating, and if you have used Flask, you probably know that the same outcome can be achieved with a single app.py, written as:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return "Algorand Balance"

The reason we’ve structured it like this is for a lot more flexibility. As we start adding more features, a single app.py will quickly become overcrowded and hard to use. What we’ve done is used a Flask Blueprint to separate components.

Now you might be wondering about the static and template folders mentioned in views.py. These are to store the project’s CSS and HTML, respectively. We can add these folders as so:

.
├── venv/
├── your_application_name/
│   ├── static/
│   │   └── css/
│   ├── templates/
│   ├── __init__.py
│   └── views.py
├── requirements.txt
└── wsgi.py

Now create style.css inside css and layout.html inside templates.

Path: your_application_name/static/css/style.css

body {
    margin: 0;
    font-family: helvetica;
    color: black;
}

.container {
    display: flex;
    justify-content: center;
}
.content {
    text-align: center;
    padding: 15px;
    margin: 40px;
    border-radius: 24px;
    background-color: whitesmoke;
    width: 60%;
    position: absolute;
    padding-bottom: 48px;
}

Path: your_application_name/templates/layout.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
    <title>Wallet</title>
</head>
<body>
<div class="container">
    <div class="content">
        {% block content %} {% endblock %}
    </div>
</div>
</body>
</html>

Flask contains Jinja2, which we can use to add logic to HTML using the curly braces. We can create the index.html file, which extends from this layout.

Path: your_application_name/templates/index.html

{% extends 'layout.html' %}

{% block content %}
<h1>Algorand Balance</h1>
{% endblock %}

How this works is that by extending from layout.html, the blocks in index.html replace the blocks in layout.html.

Now in order to display the new index page, we need to render it with Flask.
Replace views.py with:

from flask import Blueprint, render_template

main_bp = Blueprint(
    'main_bp', __name__,
    template_folder='templates',
    static_folder='static'
)

@main_bp.route('/')
def index():
    return render_template('index.html')

Running flask run again should now give a page looking like this:

EditorImages/2021/07/28 15:58/initial.png

Interacting with the Algod client

Now we should start thinking about getting account information. To do this, we can use the algod client.

Create a new file called algod.py inside the your_application_name directory.

Path: your_application_name/algod.py

from algosdk import account, mnemonic
from algosdk.constants import microalgos_to_algos_ratio
from algosdk.v2client import algod

def algod_client():
    algod_address = "https://testnet-algorand.api.purestake.io/ps2"
    algod_token = "YOUR API KEY GOES HERE"
    headers = {
        "X-API-Key": algod_token,
    }

    return algod.AlgodClient(algod_token, algod_address, headers)

def create_account():
    private_key, address = account.generate_account()
    return mnemonic.from_private_key(private_key)

def get_balance(address):
    account_info = algod_client().account_info(address)
    balance = account_info.get('amount') / microalgos_to_algos_ratio

    return balance

This method uses Purestake.io to connect to the Algod client. After putting in your API key, run passphrase = create_account() to generate a TestNet account and save the passphrase. Make sure you store this somewhere because we’ll be using it a lot before implementing a login system.

Note that we made a helper function to return the Algod client whenever. From now on, we’ll be calling this function to use the client, so be sure to use algod_client() and not algod_client.

Now we can get the balance of our new account by running balance = get_balance(mnemonic.to_public_key(passphrase)). We need to convert the account passphrase into its public key - the address - in order to check the balance.

Note that by default, balance is stored in microAlgos, which is equivalent to 0.000001 Algos, the unit a wallet would normally display.

You can add more algorand to your account on the TestNet using the Algorand Dispenser.

Let’s now bring this value onto our Flask app.

Add the following to views.py:

...
from .algod import get_balance
...

def index():
    balance = get_balance("INPUT YOUR TESTNET ADDRESS HERE")
    return render_template('index.html', balance=balance)

And to index.html:

{% extends 'layout.html' %}

{% block content %}
<h1>Algorand Balance</h1>
<h1>{{ balance }}</h1>
{% endblock %}

Now using flask run will display the user’s balance.

Just to be sure, so far our file structure should look like this:

.
├── venv/
├── your_application_name/
│   ├── static/
│   │   └── css/
│   │       └── style.css
│   ├── templates/
│   │   ├── layout.html
│   │   └── index.html
│   ├── __init__.py
│   ├── algod.py
│   └── views.py
├── requirements.txt
└── wsgi.py

Sending transactions with the Algod client

Now that we have a working account balance, the next step should be to implement a way to send ALGO tokens. Here, we’ll also be working with Flask-WTF to create forms for these transactions.

We can start by creating a new file forms.py inside of your_application_name:

Path: your_application_name/forms.py

from algosdk.constants import address_len, note_max_length
from flask_wtf import FlaskForm
from wtforms import DecimalField, StringField, SubmitField
from wtforms.validators import InputRequired, Optional, Length, NumberRange

class SendForm(FlaskForm):
    """Form for creating a transaction"""
    quantity = DecimalField(
        'Quantity',
        validators=[InputRequired(), NumberRange(min=0)],
        render_kw={"placeholder": "Quantity to Send"}
    )
    receiver = StringField(
        'Receiver',
        validators=[InputRequired(), Length(min=address_len, max=address_len)],
        render_kw={"placeholder": "Receiver Address"}
    )
    note = StringField(
        'Note',
        validators=[Optional(), Length(max=note_max_length)],
        render_kw={"placeholder": "Note"})
    submit = SubmitField('Send')

This provides some of the parameters necessary for sending a transaction. WTForms, which is included with Flask-WTF, provides several validators to make our lives easier. This means we can prevent errors we know will fail before calling the Algod client. Meanwhile, algosdk.constants gives us the values we need to validate our form.

Under views.py, we can add a new route:

from .forms import SendForm

@main_bp.route('/send', methods=['GET', 'POST'])
def send():
    """Provides a form to create and send a transaction"""
    form = SendForm()
    address = "INPUT YOUR TESTNET ADDRESS HERE" # use mnemonic.to_public_key(passphrase)
    sk = "INPUT YOUR PRIVATE KEY HERE" # use mnemonic.to_private_key(passphrase)
    if form.validate_on_submit():
        success = send_txn(address, form.quantity.data, form.receiver.data, form.note.data, sk)
        return render_template('success.html', success=success)

    # show the form, it wasn't submitted
    return render_template('send.html', form=form, address=address)

We can add two new HTML files now. Both make use of our previous layout.html.

Path: your_application_name/templates/send.html

{% extends 'layout.html' %}

{% block content %}
<h1>Send Algorand</h1>
<form action="{{ url_for('main_bp.send') }}" method="post">
    {{ form.csrf_token }}

    {{ form.quantity.label }}
    <br>
    {{ form.quantity }}
    <br>
    {% if form.quantity.errors %}
        <h3>{{ form.quantity.errors[0] }}</h3>
    {% endif %}
    <br>
    {{ form.receiver.label }}
    <br>
    {{ form.receiver }}
    <br>
    {% if form.receiver.errors %}
        <h3>{{ form.receiver.errors[0] }}</h3>
    {% endif %}
    <br>
    {{ form.note.label }}
    <br>
    {{ form.note }}
    <br>
    {% if form.note.errors %}
        <h3>{{ form.note.errors[0] }}</h3>
    {% endif %}
    <br>
    {{ form.submit }}
</form>
<h1>Receive Algorand</h1>
<h2>Your address is:</h2>
<h4>{{ address }}</h4>
{% endblock %}

Path: your_application_name/templates/success.html

{% extends 'layout.html' %}

{% block content %}
{% if success %}
<h1>Transaction Success</h1>
{% else %}
<h1>Transaction Failed</h1>
{% endif %}
{% endblock %}

Here is the CSS you can use for forms:

input {
    width: 280px;
    padding: 12px;
    margin-bottom: 12px;
    border-radius: 6px;
    border: 2px solid grey;
    box-sizing: content-box;
}

input[type=submit] {
    font-size: 16px;
    font-weight: bold;
    border: 3px solid black;
    border-radius: 6px;
    width: 120px;
    background-color: white;
    color: black;
}

input[type=submit]:hover {
    background-color: darkslategrey;
    color: white;
    cursor: pointer;
}

Now if you do flask run and add /send to the address bar, your page should look like this:

EditorImages/2021/07/28 15:59/form.png

You can also add a link from your index with <h2><a href="{{ url_for('main_bp.send') }}">Send/Receive</a></h2>.

Notice if you try to click submit you would just get an error. We still need to implement actually sending the algorand using the algod client.

We can add to algod.py

from algosdk.future.transaction import PaymentTxn

...

def send_transaction(sender, quantity, receiver, note, sk):
    """Create and sign a transaction. Quantity is assumed to be in algorands, not microalgos"""

    quantity = int(quantity * microalgos_to_algos_ratio)
    params = algod_client().suggested_params()
    note = note.encode()
    try:
        unsigned_txn = PaymentTxn(sender, params, receiver, quantity, None, note)
    except Exception as err:
        print(err)
        return False
    signed_txn = unsigned_txn.sign(sk)
    try:
        txid = algod_client().send_transaction(signed_txn)
    except Exception as err:
        print(err)
        return False

    # wait for confirmation
    try:
        wait_for_confirmation(txid, 4)
        return True
    except Exception as err:
        print(err)
        return False


# utility for waiting on a transaction confirmation
def wait_for_confirmation(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 = algod_client().status()["last-round"] + 1
    current_round = start_round

    while current_round < start_round + timeout:
        try:
            pending_txn = algod_client().pending_transaction_info(transaction_id)
        except Exception as err:
            print(err)
            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"]))
        algod_client().status_after_block(current_round)
        current_round += 1
    raise Exception(
        'pending tx not found in timeout rounds, timeout value = : {}'.format(timeout))

Now add from .algod import send_txn to views.py.

You can now test out sending algorand back to the dispenser with the address GD64YIY3TWGDMCNPP553DZPPR6LDUSFQOIJVFDPPXWEG3FVOJCCDBBHU5A.

Make sure you have enough funds, or the transaction will fail.

Implementing a Login System with Flask-Login

Before we get too far, we need to structure our code to allow some kind of Login System. Flask-Login can handle the finer details, and we can start by first creating a User class.

Create a new file models.py inside of your_application_name:

Path: your_application_name/models.py

from algosdk import mnemonic
from flask_login import UserMixin

from .algod import get_balance, send_txn

class User(UserMixin):
    """User account model"""

    def __init__(self, passphrase):
        """Creates a user using the 25-word mnemonic"""
        self.passphrase = passphrase

    @property
    def id(self):
        """Returns private key from mnemonic"""
        return mnemonic.to_private_key(self.passphrase)

    @property
    def public_key(self):
        """Returns public key from mnemonic. This is the same as the user's address"""
        return mnemonic.to_public_key(self.passphrase)

    def get_balance(self):
        """Returns user balance, in algos"""
        return get_balance(self.public_key)

    def send(self, quantity, receiver, note):
        """Returns True for a successful transaction. Quantity is given in algos"""
        return send_txn(self.public_key, quantity, receiver, note, self.id)

Normally, when creating a User class, we would also use a database to store account details. However, this isn’t as important when working with blockchain technology.

After inputting a passphrase, we can find the user’s public key (address) and private key. We called the private key the user ID so that it will work with some Flask-Login features later.

Notice the functions get_balance and send, we’ll soon be calling these from views.py so we no longer need to pass in our address every time.

Next let’s quickly create the form we will be using to sign in.

Path: your_application_name/forms.py

class LoginForm(FlaskForm):
    """Form for logging into an account"""
    passphrase = StringField('25-word Passphrase', validators=[InputRequired()])
    submit = SubmitField('Login')

Along with the corresponding HTML in a new login.html page.

Path: your_application_name/templates/login.html

{% extends 'layout.html' %}

{% block content %}
<h1>Login</h1>
<form action="{{ url_for('auth_bp.login') }}" method="post">
    {{ form.csrf_token }}

    {{ form.passphrase.label }}
    <br>
    {{ form.passphrase }}
    <br><br>
    {{ form.submit }}
</form>
{% with messages = get_flashed_messages() %}
{% if messages %}
<h2>Passphrase not found</h2>
{% else %}
<br><br><br>
{% endif %}
{% endwith %}
<br>
<h2><a href="{{ url_for('auth_bp.signup') }}">Create a Wallet</a></h2>
{% endblock %}

Now we can create a new file called auth.py. This file will function similarly to views.py but it will only handle routes related to user authentication.

Path: your_application_name/auth.py

from algosdk import mnemonic
from flask import Blueprint, render_template, redirect, url_for, flash
from flask_login import LoginManager, current_user, login_user

from .algod import create_account
from .forms import LoginForm
from .models import User

login_manager = LoginManager()

auth_bp = Blueprint(
    'auth_bp', __name__,
    template_folder='templates',
    static_folder='static'
)


@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
    """Default login page"""
    if current_user.is_authenticated:
        return redirect(url_for('main_bp.index'))

    form = LoginForm()
    if form.validate_on_submit():
        try:
            user = User(passphrase=form.passphrase.data)
            login_user(user)
            return redirect(url_for('main_bp.index'))
        except Exception as err:
            flash(err)
            return render_template('login.html', form=form)
    return render_template('login.html', form=form)

We are now using a new Flask Blueprint, auth_bp. This will need to be added to __init__.py.

from flask import Flask

from . import auth
from . import views


def create_app():
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "put any long random string here"

    auth.login_manager.init_app(app)

    app.register_blueprint(views.main_bp)
    app.register_blueprint(auth.auth_bp)

    return app

The secret key is used to encrypt browser cookies, making it especially important for storing user information.

Now, we can use Flask-Login to set routes as login_required, meaning they can’t be accessed without an account. Also, the current_user instance is used to retrieve information about the signed-in user. Let’s now add these into our views.py.

from flask_login import login_required, current_user

...

@main_bp.route('/')
@login_required
def index():
    """Main page, displays balance"""
    balance = current_user.get_balance()
    return render_template('index.html', balance=balance)


@main_bp.route('/send', methods=['GET', 'POST'])
@login_required
def send():
    """Provides a form to create and send a transaction"""
    form = SendForm()
    address = current_user.public_key
    if form.validate_on_submit():
        success = current_user.send(form.quantity.data, form.receiver.data, form.note.data)
        return render_template('success.html', success=success)

    # show the form, it wasn't submitted
    return render_template('send.html', form=form, address=address)

Before any of this code can run, we need to add two functions to auth.py.

@login_manager.user_loader
def load_user(user_id):
    """User load logic"""
    return User(mnemonic.from_private_key(user_id))


@login_manager.unauthorized_handler
def unauthorized():
    """Redirect unauthorized users to login page"""
    return redirect(url_for('auth_bp.login'))

  • load_user gathers a user from a given ID. We named the private key our User ID, so here we get a User from a given private key.

  • unauthorized redirects you to the login page if you haven’t signed in.

Now finally, try running the code. You should see a login page - put in your passphrase to sign in.

EditorImages/2021/07/28 15:59/login.png

Now that we can sign in, we should provide an easy way to sign out. Here I provide the CSS and HTML I used for a navigation bar.

Path: your_application_name/templates/nav.html

<nav class="topnav">
    <a href="{{ url_for('main_bp.index') }}" {% if request.path==url_for('main_bp.index') %} class="active"{% endif %}>Balance</a>
    <a href="{{ url_for('main_bp.send') }}" {% if request.path==url_for('main_bp.send') %} class="active"{% endif %}>Send/Receive</a>
    {% if current_user.is_authenticated %}
    <a class="logout" href="{{ url_for('main_bp.logout') }}"> Logout </a>
    {% endif %}
</nav>

CSS:

.topnav {
    background-color: whitesmoke;
    padding: 20px;
    text-align: center;
}

/* Style the links inside the navigation bar */
.topnav a {
    color: black;
    padding: 10px;
    padding-left: 20px;
    padding-right: 20px;
    text-decoration: none;
    border-radius: 6px;
    font-size: 18px;
}

/* Change the color of links on hover */
.topnav a:hover {
    background-color: lightgrey;
    color: black;
}

/* Add a color to the active/current link */
.topnav a.active {
    background-color: darkslategrey;
    color: white;
}

.topnav a.logout {
    background-color: lightcoral;
    color: white;
}

You will need to include the nav bar in layout.html. This can be done by {% include 'nav.html' %}.

The logout route also should be defined in views.py. This route uses redirect() to go back to the login page.

from flask import redirect, url_for
from flask_login import logout_user

@main_bp.route('/logout')
@login_required
def logout():
    """User log-out logic."""
    logout_user()
    return redirect(url_for('auth_bp.login'))

EditorImages/2021/07/28 15:59/navbar.png

We can now remove our send/receive link from the index.

Account Creation

We can also add a sign-up button, and generate an account in the website. In auth.py, add the following:

@auth_bp.route('/signup', methods=['GET', 'POST'])
def signup():
    """Generates a user account and shows its passphrase"""
    passphrase = create_account()
    user = User(passphrase=passphrase)
    login_user(user)
    return render_template('mnemonic.html', passphrase=passphrase)

Here, we use our earlier function from algod.py to generate an account and its passphrase.
Add this link <h2><a href="{{ url_for('auth_bp.signup') }}">Create a Wallet</a></h2> to login.html to access this route.

We also need to create mnemonic.html - this page will display the mnemonic of the created account.

Path: your_application_name/templates/mnemonic.html

{% extends 'layout.html' %}

{% block content %}
<h1>Your Account Recovery Phrase:</h1>
<br>
<h2>{{ passphrase }}</h2>
<h2>Note this down and keep it secret!</h2>
{% endblock %}

I also added a separate route to mnemonic.html in views.py. This route uses the current account instead of creating a new one and will be used so users can view their mnemonic again if they missed it.

@main_bp.route('/mnemonic')
@login_required
def mnemonic():
    """Displays the recovery passphrase"""
    passphrase = current_user.passphrase
    return render_template('mnemonic.html', passphrase=passphrase)

This can be accessed with <h2><a href="{{ url_for('main_bp.mnemonic') }}">View Recovery Passphrase</a></h2> on index.html

We’ve now finished our login system for the website. You can log in using a passphrase or create an account, and then you can then log out when finished. Our file structure should look like this:

.
├── venv/
├── your_application_name/
│   ├── static/
│   │   └── css/
│   │       └── style.css
│   ├── templates/
│   │   ├── index.html
│   │   ├── layout.html
│   │   ├── login.html
│   │   ├── mnemonic.html
│   │   ├── nav.html
│   │   ├── send.html
│   │   └── success.html
│   ├── __init__.py
│   ├── algod.py
│   ├── auth.py
│   ├── forms.py
│   ├── models.py
│   └── views.py
├── requirements.txt
└── wsgi.py

Displaying Transactions using the Indexer

The indexer is used to retrieve all transactions on the blockchain. We can then filter this to all transactions related to the current account.

Let’s start by creating a new file indexer.py to add all indexer processes.

Path: your_application_name/indexer.py

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

def myindexer():
    algod_address = "https://testnet-algorand.api.purestake.io/idx2"
    algod_token = "YOUR API KEY GOES HERE"
    headers = {
        "X-API-Key": algod_token,
    }
    return indexer.IndexerClient("", algod_address, headers)

def get_transactions(address):
    """Returns a list of transactions related to the given address"""

    response = myindexer().search_transactions(address=address, txn_type="pay")
    txns = []
    for txn in response["transactions"]:
        sender = txn["sender"]
        fee = txn["fee"]

        amount = txn["payment-transaction"]["amount"]
        if sender == address:

            # if the current account is the sender, add fee and display transaction as negative
            amount += fee
            amount *= -1
            other_address = txn["payment-transaction"]["receiver"]
        else:
            other_address = sender

        amount /= microalgos_to_algos_ratio

        txns.append({"amount": amount, "address": other_address})
    return txns

myindexer() is a helper function just like algod_client().

get_transactions works by looping through each payment transaction related to the address. If the given address (the current user) is also the sender, we want to also add the fee the sender paid and display the total cost as negative.

We return a list of dictionaries that includes an "amount" and an "address". "amount" will show the money entering/leaving the user’s account and "address" will show the other address the transaction was completed with.

Now, as usual, we can add a route in views.py to call this function.

@main_bp.route('/transactions')
@login_required
def transactions():
    """Displays all transactions from the user"""
    txns = current_user.get_transactions()
    return render_template('transactions.html', txns=txns)

The new file transactions.html should look like this:

Path: your_application_name/transactions.html

{% extends 'layout.html' %}

{% block content %}
<h1>Transactions</h1>
<table>
    <tr>
        <th>Sender/Receiver</th>
        <th>Quantity</th>
    </tr>
    {% for txn in txns %}
    <tr>
        <td>{{ txn.address }}</td>
        <td>{{ txn.amount }}</td>
        </div>
    </tr>
    {% endfor %}
</table>
{% endblock %}

With the following CSS:

table {
    text-align: left;
    margin-left: auto;
    margin-right: auto;
}

th, td {
    border-bottom: 1px solid lightgrey;
    padding: 4px;
    padding-right: 16px;
}

Don’t forget to add transaction.html to the navbar!

If you tried running now you would still get error. We need to add get_transactions to the user model so that we can add the current user’s address to our indexer function.

Path: your_application_name/models.py

from .indexer import get_transactions

class User(UserMixin):

    ...

    def get_transactions(self):
        """Returns a list of the user's transactions"""
        return get_transactions(self.public_key)

Now we can run, and view all transactions.

EditorImages/2021/07/28 15:59/transactions.png

If you want to, you could also try displaying negative transactions as red, and positive transactions as green.

At this point, your file structure should look like this:

.
├── venv/
├── your_application_name/
│   ├── static/
│   │   └── css/
│   │       └── style.css
│   ├── templates/
│   │   ├── index.html
│   │   ├── layout.html
│   │   ├── login.html
│   │   ├── mnemonic.html
│   │   ├── nav.html
│   │   ├── send.html
│   │   ├── success.html
│   │   └── transactions.html
│   ├── __init__.py
│   ├── algod.py
│   ├── auth.py
│   ├── forms.py
│   ├── indexer.py
│   ├── models.py
│   └── views.py
├── requirements.txt
└── wsgi.py

Working With Assets

Assets can be used for a variety of purposes within the Algorand network. We’re only going to look at creating assets and displaying the created assets.

For more information on assets and what you can do with them, I recommend reading about them on the Developer Portal.

Asset Creation Form

In order to create assets, we’ll first need a new form class.

Path: your_application_name/forms.py

from algosdk.constants import max_asset_decimals
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, IntegerField, BooleanField
from wtforms.validators import InputRequired, Optional, NumberRange

class AssetForm(FlaskForm):
    """Form for creating an asset"""
    asset_name = StringField(
        'Asset name',
        validators=[InputRequired()]
    )
    unit_name = StringField(
        'Unit name',
        validators=[InputRequired()]
    )
    total = IntegerField(
        'Total number',
        validators=[InputRequired(), NumberRange(min=1)]
    )
    decimals = IntegerField(
        'Number of decimals',
        validators=[InputRequired(), NumberRange(min=0, max=max_asset_decimals)]
    )
    default_frozen = BooleanField(
        'Frozen',
        validators=[Optional()]
    )
    url = StringField(
        'URL',
        validators=[Optional()]
    )
    submit = SubmitField('Create')

There is one more necessary parameter for creating an asset - the creator address. We will automatically put the current user’s address as the creator.

There are also four other addresses an asset can use:
- Manager address
- Freeze address
- Reserve address
- Clawback address

These addresses are granted special permissions related to the asset. For simplicity’s sake, we’ll set also set these addresses to the current user’s address.

Now create an assets.html page in templates.

Path: your_application_name/templates/assets.html

{% extends 'layout.html' %}

{% block content %}
<h1>Assets</h1>
<a href="{{ url_for('main_bp.create') }}"><h2>Create an Asset</h2></a>
{% endblock %}

We’ll later also display our created assets here.

Now let’s add the same routes into views.py.

from .forms import AssetForm

@main_bp.route('/assets')
@login_required
def assets():
    return render_template('assets.html')

@main_bp.route('/create', methods=['GET', 'POST'])
@login_required
def create():
    """Provides a form to create an asset"""
    form = AssetForm()
    if form.validate_on_submit():
        asset_id = current_user.create(
            form.asset_name.data,
            form.unit_name.data,
            form.total.data,
            form.decimals.data,
            form.default_frozen.data,
            form.url.data
        )

        return redirect(url_for('main_bp.assets'))

    # show the form, it wasn't submitted
    return render_template('create_asset.html', form=form)

We’ll need to create the HTML page for the form now.

Path: your_application_name/templates/create_asset.html

{% extends 'layout.html' %}

{% block content %}
<h1>Create an Asset</h1>
<form action="{{ url_for('main_bp.create') }}" method="post">
    {{ form.csrf_token }}

    {{ form.asset_name.label }}
    <br>
    {{ form.asset_name }}
    <br><br>
    {{ form.unit_name.label }}
    <br>
    {{ form.unit_name }}
    <br><br>
    {{ form.total.label }}
    <br>
    {{ form.total }}
    <br><br>
    {{ form.decimals.label }}
    <br>
    {{ form.decimals }}
    <br><br>
    {{ form.default_frozen.label }}
    <br>
    {{ form.default_frozen }}
    <br><br>
    {{ form.url.label }}
    <br>
    {{ form.url }}
    <br><br>
    {{ form.submit }}
    <br>
    {% if form.errors %}
        <h2>Invalid submission</h2>
    {% endif %}
</form>
{% endblock %}

After adding “Assets” to the navbar, we should be able to get a form looking like this:

EditorImages/2021/07/28 16:00/create.png

Of course, this won’t work until we implement the backend of creating an asset.

Creating Assets with the Algod client

Under models.py, we can create a new method. This method will add the current user’s public and private keys to the parameters needed to create an asset.

from .algod import create_asset

class User(UserMixin):

    ...

    def create(
            self,
            asset_name,
            unit_name,
            total,
            decimals,
            default_frozen,
            url
    ):
        """Creates an asset, with the user as the creator"""
        return create_asset(
            self.public_key,
            asset_name,
            unit_name,
            total,
            decimals,
            default_frozen,
            url,
            self.id
        )    

Now let’s implement the create_asset function in algod.py.

from algosdk.future.transaction import AssetConfigTxn

def create_asset(
        creator,
        asset_name,
        unit_name,
        total,
        decimals,
        default_frozen,
        url,
        sk
):
    """Creates an asset, returns the newly created asset ID"""
    params = algod_client().suggested_params()

    txn = AssetConfigTxn(
        sender=creator,
        sp=params,
        total=total,
        default_frozen=default_frozen,
        unit_name=unit_name,
        asset_name=asset_name,
        manager=creator,
        reserve=creator,
        freeze=creator,
        clawback=creator,
        url=url,
        decimals=decimals)

    # Sign with secret key of creator
    stxn = txn.sign(sk)

    # Send the transaction to the network and retrieve the txid.
    txid = algod_client().send_transaction(stxn)

    try:
        wait_for_confirmation(txid, 4)
    except Exception as err:
        print(err)
        return None

    try:
        ptx = algod_client().pending_transaction_info(txid)
        asset_id = ptx["asset-index"]
        return asset_id
    except Exception as err:
        print(err)
        return None    

You can now test out creating an asset, but you won’t see any feedback just yet. Next we will display all the created assets under the Asset page.

Displaying Created Assets

Created assets can be displayed using the indexer. We can query assets using the current account’s address as the creator parameter.

As always, we use our User model to input the current user address.

Path: your_application_name/models.py

from .indexer import get_assets

class User(UserMixin):

    ...

    def get_assets(self):
        """Returns a list of the user's assets"""
        return get_assets(self.public_key)

Now we can use the indexer to find all created assets from the given address.

Path: your_application_name/indexer.py

def get_assets(address):
    """Returns a list of assets that have been created by the given address"""

    response = myindexer().search_assets(creator=address)
    assets = response["assets"]
    return assets

Now let’s update views.py and assets.html to display the given list.

Path: your_application_name/views.py

@main_bp.route('/assets')
@login_required
def assets():
    """Displays all assets owned by the user"""
    assets_list = current_user.get_assets()
    return render_template('assets.html', assets=assets_list)

Path: your_application_name/templates/assets.html

{% extends 'layout.html' %}

{% block content %}
<h1>Assets</h1>
<a href="{{ url_for('main_bp.create') }}"><h2>Create an Asset</h2></a>
<table>
    <tr>
        <th>Asset Name</th>
        <th>Unit Name</th>
        <th>Total</th>
        <th>Decimals</th>
        <th>Frozen</th>
        <th>URL</th>
    </tr>
    {% for asset in assets %}
    <tr>
        <td>{{ asset.params.name }}</td>
        <td>{{ asset.params.get('unit-name') }}</td>
        <td>{{ asset.params.total }}</td>
        <td>{{ asset.params.decimals }}</td>
        <td>{{ asset.params.get('default-frozen') }}</td>
        <td>{{ asset.params.url }}</td>
    </tr>
    {% endfor %}
</table>
{% endblock %}

Now we can finally view our created assets.

EditorImages/2021/07/28 16:00/assets.png

As I mentioned before, there is a lot to do with assets. You could also add options to distribute or revoke assets, as well as view asset holdings.

We now have our final file structure:

.
├── venv/
├── your_application_name/
│   ├── static/
│   │   └── css/
│   │       └── style.css
│   ├── templates/
│   │   ├── assets.html
│   │   ├── create_asset.html
│   │   ├── index.html
│   │   ├── layout.html
│   │   ├── login.html
│   │   ├── mnemonic.html
│   │   ├── nav.html
│   │   ├── send.html
│   │   ├── success.html
│   │   └── transactions.html
│   ├── __init__.py
│   ├── algod.py
│   ├── auth.py
│   ├── forms.py
│   ├── indexer.py
│   ├── models.py
│   └── views.py
├── requirements.txt
└── wsgi.py

Filtering Transactions and Assets

The final feature we will add is the ability to filter a given list of transactions or assets. This can be easily achieved by adding to our existing indexer functions.

To get started, we need a new form class which we will add to forms.py.

class FilterForm(FlaskForm):
    """Form for filtering transactions and assets"""
    substring = StringField(
        'Filter',
        validators=[Optional()],
        render_kw={"placeholder": "Filter list"}
    )
    submit = SubmitField('Search')

Now we should add the required HTML for the form to transactions.html and assets.html.

Path: your_application_name/templates/transactions.html

<form action="{{ url_for('main_bp.transactions') }}" method="post">
    {{ form.csrf_token }}

    {{ form.substring }}
    {{ form.submit }}
</form>

This can be reused in assets.html by replacing url_for('main_bp.transactions') with url_for('main_bp.assets').

Now we need to update indexer.py, models.py and views.py to allow filtering.

Path: your_application_name/indexer.py

def get_transactions(address, substring):
    """Returns a list of transactions related to the given address"""

    response = myindexer().search_transactions(address=address, txn_type="pay")
    txns = []
    for txn in response["transactions"]:
        sender = txn["sender"]
        fee = txn["fee"]

        amount = txn["payment-transaction"]["amount"]
        if sender == address:

            # if the current account is the sender, add fee and display transaction as negative
            amount += fee
            amount *= -1
            other_address = txn["payment-transaction"]["receiver"]
        else:
            other_address = sender

        amount /= microalgos_to_algos_ratio

        # check for searched address
        if substring not in other_address:
            continue

        txns.append({"amount": amount, "address": other_address})
    return txns


def get_assets(address, name):
    """Returns a list of assets that have been created by the given address"""

    response = myindexer().search_assets(creator=address, name=name)
    assets = response["assets"]
    return assets

For assets, we can simply use the inbuilt functionality to filter by name. However, for transactions we need to filter for the second address ourselves.

Path: your_application_name/models.py

def get_transactions(self, substring):
    """Returns a list of the user's transactions"""
    return get_transactions(self.public_key, substring)

def get_assets(self, name):
    """Returns a list of the user's assets"""
    return get_assets(self.public_key, name)

Path: your_application_name/views.py

@main_bp.route('/transactions', methods=['GET', 'POST'])
@login_required
def transactions():
    """Displays all transactions from the user"""
    form = FilterForm()

    if form.validate_on_submit():
        txns = current_user.get_transactions(form.substring.data)
    else:
        txns = current_user.get_transactions("")

    return render_template('transactions.html', txns=txns, form=form)


@main_bp.route('/assets', methods=['GET', 'POST'])
@login_required
def assets():
    """Displays all assets owned by the user"""
    form = FilterForm()

    if form.validate_on_submit():
        assets_list = current_user.get_assets(form.substring.data)
    else:
        assets_list = current_user.get_assets("")

    return render_template('assets.html', assets=assets_list, form=form)

It is important to add the POST method to these functions for the form to work.

This gives us a working transaction filter by address:

EditorImages/2021/07/28 16:00/transaction_filter.png

And an asset filter by name:

EditorImages/2021/07/28 16:00/asset_filter.png

Conclusion

Now we finally have our working demo wallet. Thanks for reading this far. I hope this project walkthrough has been useful!

From here there are a lot more possibilities. Another feature could be to store multiple accounts on one wallet, for example.

Finally, remember to never use the mainnet, unless you have robust security features, and never commit files with sensitive information.

August 18, 2021