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

Wagering DApp with a serverless Vue + AlgoSigner Frontend

Overview

This solution provides a frontend to backend example of a distributed App (dApp) which includes the use of smart contracts, as well as a python CLI program that deploys, configures and deletes these smart contracts. It has a frontend built with Vue and Algosigner. The dApp described in this solution document is a wagering application through which users are able to bet on their favorite teams and receive Algo for making successful bets. The core logic will be managed by smart contracts running on the Algorand blockchain.

The full source of this application is available on github. You can also see it running and test it at https://lucasvanmol.github.io/algobets/.

This solution contains three parts:

  • A stateful and a stateless smart contract that are deployed to the Algorand blockchain
  • A python CLI program that will deploy, configure and delete these programs
  • A frontend using Vue and AlgoSigner to enable users to interact with the smart contracts

Table of Contents

TEAL Smart Contracts


Summary

The TEAL component of this application consists of a stateful smart contract (DApp) and a stateless smart contract (escrow account). The DApp will hold information about the wager in global state, and track user’s wagers in local state. The escrow account will hold the wagered Algo, and authorize payments from itself when users have won a wager.

DApp - Stateful Smart Contract

In the DApp the following global state variables are used:

  • Team1, the name of the 1st team.
  • Team2, the name of the 2nd team.
  • LimitDate, the date after which no more bets can be placed, and the winning team can be set by the creator.
  • EndDate, the date after which funds can be reclaimed if the creator did not set a winning team.
  • Winner, the winning team.
  • Escrow, the address of the escrow account that holds the funds.
  • Team1Total, the total amount wagered on team 1.
  • Team2Total, the total amount wagered on team 2.

We’ll also have two local state variables:

  • MyTeam, the team the user has wagered for.
  • MyBet, the amount wagered by the user.

Setting Up Global State Variables

Now we can get to writing the DApp. First, we need to define a couple of global state variables upon creation. This is done by checking if the ApplicationId is 0. We’ll want the creator of the DApp to provide the values for Team1, Team2, LimitDate, and EndDate as arguments upon the DApps creation. We’ll also initialize the rest of the global state variables to some default values. Once that’s done, we branch to a label named done which will return and approve the Application Create Transaction. If the application is not being created, we will skip past this part by jumping to the not_creation label.

// app.teal
#pragma version 3

txn ApplicationID
int 0
==
bz not_creation

txn NumAppArgs
int 4
==
assert

byte "Team1"
txna ApplicationArgs 0
app_global_put

byte "Team2"
txna ApplicationArgs 1
app_global_put

byte "LimitDate"
txna ApplicationArgs 2
btoi
app_global_put

byte "EndDate"
txna ApplicationArgs 3
btoi
app_global_put

byte "Winner"
byte ""
app_global_put

byte "Team1Total"
int 0
app_global_put

byte "Team2Total"
int 0
app_global_put

b done

not_creation:

Checking OnCompletion values

From here we check the OnCompletion value and jump around accordingly:

// app.teal

txn OnCompletion
int UpdateApplication
==
bnz handle_update

txn OnCompletion
int OptIn
==
bnz handle_optin

txn OnCompletion
int NoOp
==
bnz handle_noop

txn OnCompletion
int CloseOut
==
bnz handle_closeout


txn OnCompletion
int DeleteApplication
==
bnz handle_deleteapp

// Unexpected OnCompletion value. Should be unreachable
err

Opting In as a User

The handle_optin label will be how users wager their Algos for their team. We’ll need to do the following:

  • Check that their transaction is valid
  • Get their chosen team by checking ApplicationArgs 0
  • Updating local and global state accordingly

To make sure the user’s transaction is valid, we first want to make sure they’re opting in before the LimitDate:

// app.teal

handle_optin:

global LatestTimestamp
byte "LimitDate"
app_global_get
<=
assert

Next, we’ll check that their OptIn call is grouped with a payment transaction to the escrow address. We’ll also set a minimum amount of 10000 microAlgos.

// app.teal

global GroupSize
int 2
==
assert

gtxn 0 TypeEnum
int 1
==
assert

gtxn 0 Receiver
byte "Escrow"
app_global_get
==
assert

gtxn 0 Amount
int 10000
>=
assert

The user should also indicate who their wager is for by providing the team name as an argument to the OptIn transaction. We’ll have to check whether this team name is valid by checking it against the Team1 and Team2 global state variables.

// app.teal

txn NumAppArgs
int 1
==
assert

txna ApplicationArgs 0
byte "Team1"
app_global_get
==
txna ApplicationArgs 0
byte "Team2"
app_global_get
==
// Assuming the assert below passed, this value will be 0 if user voted for team 1 and 1 if user voted for team 2
// We'll store it for later to figure out which team's total to increment
dup
store 0 
||
assert

Now we can move on to setting the user’s local state accordingly:

// app.teal

int 0
byte "MyTeam"
txna ApplicationArgs 0
app_local_put

int 0 
byte "MyBet"
gtxn 0 Amount
app_local_put

And finally, we’ll update the global state for Team1Total or Team2Total by utilizing the store call from earlier.

// app.teal

load 0
bnz Team2Bet

// User voted for team1
byte "Team1Total"
b skip0

Team2Bet:
// User voted for team2
byte "Team2Total"
skip0:

// Increment the state
dup
app_global_get
gtxn 0 Amount
+
app_global_put

b done

NoOp calls

NoOp calls to the DApp will be used by the creator to update the escrow address and set the winner, and it’ll be used by the user to claim or reclaim their wager. The first thing we’ll have to do, therefore, is to check whether the NoOp caller is the creator or not, and branch accordingly.

// app.teal

handle_noop:

txn Sender
global CreatorAddress
==
bz client_noop

txn NumAppArgs
int 2
==
assert

Creator NoOp - Updating Escrow and Winner

First, let’s check out how the creator can interact with the application. They can call the application with either escrow addr to set the escrow address to addr or winner teamname to set the winner to teamname. First, parse these arguments to see what the creator wants to do:

// app.teal

txna ApplicationArgs 0
byte "escrow"
==
bnz escrow
txna ApplicationArgs 0
byte "winner"
==
bnz winner
err

Changing the escrow account is as easy as setting the corresponding global state variable to the second application argument:

// app.teal

escrow:

byte "Escrow"
txna ApplicationArgs 1
app_global_put
b done

Setting the winner is a bit more complicated. As outlined in the program functionality, the winner must only be set in-between LimitDate and EndDate.

// app.teal

winner:

global LatestTimestamp
byte "LimitDate"
app_global_get
>
assert

global LatestTimestamp
byte "EndDate"
app_global_get
<=
assert

The winner must also be either the value of Team1 or Team2, which we can verify in the same way as the user’s OptIn call:

// app.teal

txna ApplicationArgs 1
byte "Team1"
app_global_get
==
txna ApplicationArgs 1
byte "Team2"
app_global_get
==
||
assert

Once these checks are complete we can set the winner.

// app.teal

byte "Winner"
txna ApplicationArgs 1
app_global_put

b done

User NoOp - Claiming and Reclaiming

For the users, we’ll allow them to claim their winnings if the winner has been set, or reclaim it if no winner was set after EndDate.

// app.teal

client_noop:

txna ApplicationArgs 0
byte "claim" 
==
bnz claim
txna ApplicationArgs 0
byte "reclaim" 
==
bnz reclaim
err

For claim, we’ll need to do some calculations to calculate how much the user has won. We also want the user to pay any fees for the escrow account so that all winning users get their fair share. The calculation for the amount won is equal to the ratio of their wager over the total amount wager for their team, multiplied by the total amount wagered for both teams. Or as an equation:

winnings = MyBet / MyTeamTotal * (Team1Total + Team2Total)

We’ll rewrite this so that the user has to pay fees and also so that we don’t have any divisions in our equality.

winnings = MyBet / MyTeamTotal * (Team1Total + Team2Total)
amount + fee = MyBet / MyTeamTotal * (Team1Total + Team2Total)
(amount + fee) * MyTeamTotal = (Team1Total + Team2Total) * MyBet

Because this equation can result in amount being a decimal number, we want to see what the maximum value of amount can be without going over the amount a user is entitled to. This is because amount is in microAlgos and has to be a whole number.

Therefore we need to check that:

// Equation 1
(amount + fee) * MyTeamTotal <= (Team1Total + Team2Total) * MyBet

And to ensure that this is the strict maximum value for amount we need to check that:

// Equation 2
(amount + fee + 1) * MyTeamTotal > (Team1Total + Team2Total) * MyBet
// This distributes to:
(amount + fee) * MyTeamTotal + MyTeamTotal > (Team1Total + Team2Total) * MyBet

To optimize this, we will separate some of the terms into variables. Note that the right-hand side of both equations are equal and that the left-hand side of equation 2 is the left-hand side of equation 1 + MyTeamTotal. First, however, we’ll check that the claim is grouped with a payment transaction from the escrow address to the user, and that the user has chosen the right winner.

// app.teal

claim:

global GroupSize
int 2
==
assert

gtxn 0 TypeEnum
int 1
==
assert

gtxn 0 Sender
byte "Escrow"
app_global_get
==
assert

gtxn 0 Receiver
gtxn 1 Sender
==
assert

int 0
byte "MyTeam"
app_local_get
dup             // we'll use this later
byte "Winner"
app_global_get
==
assert

Now that that’s done, we can start calculating the left-hand side (LHS) of equation 1, i.e. (amount + fee) * MyTeamTotal:

// app.teal

// Get my team total (thanks to dup call earlier)
byte "Team2"
app_global_get
==
bnz Team2Total

byte "Team1Total"
b skip1

Team2Total:
byte "Team2Total"
skip1:

app_global_get
// We now have MyTeamTotal on top of the stack
// We'll also store it for the second assertion
dup
store 0 

// Now multiply by amount + fee
gtxn 0 Amount
gtxn 0 Fee
+
*

// store LHS
dup
store 1

Notice how we are storing some intermediate steps in our calculation which will be used for checking the second equation. This way we don’t have to do the same calculation twice. Now we can move on to calculating the right-hand side (RHS) (Team1Total + Team2Total) * MyBet:

// app.teal

byte "Team1Total"
app_global_get
byte "Team2Total"
app_global_get
+
int 0
byte "MyBet"
app_local_get
*

// store RHS
dup
store 2

At this point, we have the RHS (Team1Total + Team2Total) * MyBet on top of the stack, and the LHS (amount + fee) * MyTeamTotal under it. We’ve also saved MyTeamTotal in position 0 of the scratch space, LHS in position 1, and RHS in position 2. We’re now ready to assert both equations, and if they pass, update the user’s local state and approve the transaction.

// app.teal

// First equation assertion
<=
assert

// Second equation assertion
load 0
load 1
+
load 2
>
assert

// Bet has been claimed, reduce MyBet to 0 so that user cannot claim twice
int 0
byte "MyBet"
int 0
app_local_put

b done

For reclaiming, it’s a bit easier. We simply need to check that:

  • We’re past the EndDate
  • No winner has been set

The amount the user is allowed to reclaim is simply their bet amount minus the fee.

// app.teal

reclaim:

// Check we're past EndDate and no winner has been set
global LatestTimestamp
byte "EndDate"
app_global_get
>
assert

byte ""
byte "Winner"
app_global_get
==
assert

// Check that the reclaim is grouped with a payment transaction from the escrow address to the user
global GroupSize
int 2
==
assert

gtxn 0 TypeEnum
int 1
==
assert

gtxn 0 Sender
byte "Escrow"
app_global_get
==
assert

gtxn 0 Receiver
gtxn 1 Sender
==
assert

// Check that amount + fee is equal to user's bet, so that the user pays for transaction fees.
gtxn 0 Amount
gtxn 0 Fee
+
int 0
byte "MyBet"
app_local_get
==
assert

// Decrement sender's bet
int 0
byte "MyBet"
int 0
app_local_put

b done

Finally, we’ll just allow CloseOut calls and allow the creator to update or delete the app. We also have the done label which is used throughout the DApp indicating approval.

// app.teal

handle_update:
txn Sender
global CreatorAddress
==
return

handle_closeout:
b done

handle_deleteapp:
txn Sender
global CreatorAddress
==
return

done:
int 1
return

And that’s the stateful smart contract! We’ll also pair it with a simple clear program that approves the call.

// clear.teal
#pragma version 3

int 1

Escrow Account - Stateless Smart Contract

The escrow stateless smart contract will need to be compiled after the application has deployed, so that it can use the application ID to approve transactions. We’ll check if the transaction is grouped with a call to the application and that neither transaction contains a rekey. The transaction can also be grouped with a DeleteApplication call when the creator wants to delete the application and withdraw any remaining funds.

// escrow.teal
#pragma version 3

global GroupSize
int 2
==
gtxn 1 TypeEnum
int appl
==
&&

// The specific App ID must be called
// This should be changed after creation
gtxn 1 ApplicationID
int TMPL_APP_ID
==
&&

gtxn 1 OnCompletion
int NoOp
==
gtxn 1 OnCompletion
int DeleteApplication
==
||
&&

gtxn 0 RekeyTo
global ZeroAddress
==
&&
gtxn 1 RekeyTo
global ZeroAddress
==
&&

Debugging

Debugging the teal program can be doing using tools like tealdbg. Alternatively you can use dryrun debugging. As an example, dryrun debugging this DApp with goal could look something like this:

goal app create --creator ETMTHOY55NUJQ2IJG5LLAXIRNVUAI2SAQWEWSFLK7PZO4IIOATGPLREOLM \
    --app-arg 'str:Team 1' --app-arg 'str:Team 2' --app-arg 'int:1625684400' --app-arg 'int:1726116400' \
    --approval-prog app.teal --clear-prog clear.teal \
    --global-byteslices 4 --global-ints 4 \
    --local-byteslices 1 --local-ints 1 \
    --dryrun-dump --out=dump.dr

This will create a dryrun request file named dump.dr which can be used like so:

goal clerk dryrun-remote -D dump.dr -v

If you’re using goal in the sandbox, you’ll first have to copy over the necessary teal files by using ./sandbox copyTo app.teal and ./sandbox copyTo clear.teal. For testing admin app calls such as setting an escrow account, you can create a dryrun request of that as well (note that the DApp must already be created):

goal app call --app-id 2  --from ETMTHOY55NUJQ2IJG5LLAXIRNVUAI2SAQWEWSFLK7PZO4IIOATGPLREOLM \
    --app-arg "str:escrow" --app-arg "addr:CFZQ2LPQNZJDCDNAOUTCHKH3GNQAYJZ7FVVLJYMPCJAPOXVJRZDIKLM3UE" \
    --out=dump.dr --dryrun-dump

If you want to debug the client betting operation, you’ll have to create a dryrun-dump of an atomic transaction as outlined by the DApp logic.

Python CLI


The DApp can be deployed to the blockchain using the goal command-line tool or an SDK. In this solution, I use the Python SDK to create a command-line interface to manage the DApp.

Setup

To use, clone the github repository and navigate to the admin folder. You must first install the required dependencies with pip:

$ pip install -r requirements.txt

Then configure the .env file to your liking. For a local sandbox you can set it to:

ALGOD_ADDRESS="http://localhost:4001"
ALGOD_TOKEN="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
INDEXER_ADDRESS="http://localhost:8980"
API_KEY=""

If you wish to run on testnet or mainnet, you can use, for example, a third-party API service like purestake:

ALGOD_ADDRESS="https://testnet-algorand.api.purestake.io/ps2"
ALGOD_TOKEN=""
INDEXER_ADDRESS="https://testnet-algorand.api.purestake.io/idx2"
API_KEY="your-api-key-here"

Usage

Help for any command can be used with the --help flag:

$ python .\admin.py --help
usage: admin.py [-h] {list,create,delete,setwinner,info} ...

positional arguments:
  {list,create,delete,setwinner,info}
    list                list active dapps for account
    create              create a new dapp with account
    delete              delete a dapp from account
    setwinner           set winner for a given dapp
    info                get dapp info

optional arguments:
  -h, --help            show this help message and exit

To deploy new DApps to the blockchain, you must first create a text file containing your mnemonic phrase. This file, which I’ve named my_private_key in this example, can then be used like so:

$ cat my_private_key
your twenty five word mnemonic goes here ...
$ python .\admin.py create my_private_key England Denmark 1625684400 1726116400
Deploying application with args: ['England', 'Denmark', 1625684400, 1726116400]
Waiting for confirmation...
...
All done!

We’ve just deployed our wagering DApp to the Algorand blockchain!

We can check an address’s deployed DApps with list:

$ python .\admin.py list PVFMNQ57IHIYVB5UCUF55AWC2GPTJ2TIOMNO4EOFDHSWT6ZFM672W63LRM
{
  "id": 32,
  "EndDate": 1726116400,
  "LimitDate": 1625684400,
  "Team1": "England",
  "Team1Total": 0,
  "Team2": "Denmark",
  "Team2Total": 0,
  "Winner": "",
  "Escrow": "NJRD6MZXQTHV6QMBIVTZG7CEGM6OEYMNSJVNKCHIZQ5YAS6WT4RJIYNQUA"
}

As you can see, the DApp was deployed with the arguments we provided. The command has also generated and set the escrow address corresponding to the application with this app ID.

We can also find this information by providing an app ID:

$ python .\admin.py info 32
{'id': '32', 'EndDate': 1726116400, 'LimitDate': 1625684400, 'Team1': 'England', 'Team1Total': 0, 'Team2': 'Denmark', 'Team2Total': 0, 'Winner': '', 'Escrow': 'NJRD6MZXQTHV6QMBIVTZG7CEGM6OEYMNSJVNKCHIZQ5YAS6WT4RJIYNQUA'}

We can also set a winner for the DApp with setwinner (as long as it’s approved by the applications logic, of course!):

$ python ./admin.py setwinner my_private_key 32 England

This component of the application is not the focus of this solution, so I won’t go into the details of how it works here. If you wish to have a look at the source code you can find it here.

Vue + AlgoSigner Frontend


This solution is using the following npm packages:

  • vue 3.0.0
  • vuex 4.0.2
  • algosdk 1.10.0
  • js-base64 3.6.1

Instructions on how to run the project locally can be found here. Start by cloning the github repository, then navigate to the vue-frontend directory and run the commands from there. You must have node installed in order to run the project.

The Vue project is organized like so:

vue-frontend
│   README.md
|   package.json
│   .env
│   ...

└───src
│   │   App.vue
|   |   types.ts
│   │   ...
│   │
│   └───api
│   │   │   index.ts
│   │   │   escrow-teal.ts
│   │
│   └───store
│   │   │   index.ts
│   │
│   └───components
│       |   ...

└───public
    │   favicon.ico
    │   index.html

  • The api folder is what is responsible for interacting with the Algorand blockchain, and will use AlgoSigner to do so.
  • The store folder will be our Vuex store folder for state management.
  • App.vue is the base component for our Vue application.

All components will be able to ask the store for information about the DApps, using what vuex calls actions. The store will in turn call the api to request this information and save it.

types.ts

Firstly, as this is a TypeScript application, we’ll define some types in types.ts that will be used by the rest of our project. These types will represent information about accounts and the DApp’s global and local states.

// src/types.ts

export type Team = {
    Name: string,
    Total: number,
}

export type Dapp = {
    Id: number,
    Team1: Team,
    Team2: Team,
    Winner: string,
    Escrow: string,
    LimitDate: number,
    EndDate: number,
}

export type Account = {
    address: string,
}

export type DappLocalState = {
    dapp: Dapp;
    Team: string,
    Bet: number,
    account: Account,
}

AlgoSigner API

Let’s look at the most important part of the Vue project, the api. The first thing we must do is call AlgoSigner.connect() to start making requests to the Algorand blockchain. Because we’re using TypeScript, we add declare const AlgoSigner: any; to circumvent any complaints from it. AlgoSigner works by injecting some javascript into the user’s webpage, allowing web pages such as these to use it by means of an object namedAlgoSigner.

// src/api/dapps.ts

import * as algosdk from 'algosdk'
import { Base64 } from 'js-base64';
declare const AlgoSigner: any;

export default  {
    async connectAlgoSigner() {
        await AlgoSigner.connect();
    },
    // ...
}

Then, we’ll have a function that gets a list of DApps made by some creator address. We can define the creator address and ledger name in a .env file at the root of our project. The ledger name for the testnet is 'TestNet'.

// src/api/dapps.ts

// ...
import { Dapp, Account, DappLocalState } from "@/types";
const CREATOR = process.env.VUE_APP_CREATOR_ADDRESS;
const LEDGER_NAME = process.env.VUE_APP_LEDGER_NAME;

export default {
    // ...

    /**
     * Use AlgoSigner to query the Algorand blockchain for a list of AlgoBet DApps made by CREATOR.
     * 
     * @returns List of DApps.
     */
    async getDapps(): Promise<Dapp[]> {

        // Query the indexer
        const r = await AlgoSigner.indexer({
            ledger: LEDGER_NAME,
            path: `/v2/accounts/${CREATOR}`
        });
        const apps = r['account']['created-apps'];

        const dapps: Dapp[] = []
        apps.forEach((app: any) => {

            // Initialise the Dapp object
            const dapp: Dapp = {
                Id: app['id'],
                Team1: {
                    Name: '',
                    Total: 0
                },
                Team2: {
                    Name: '',
                    Total: 0
                },
                Winner: '',
                Escrow: '',
                LimitDate: 0,
                EndDate: 0
            }

            // Get all global state variables and decode them
            app['params']['global-state'].forEach((item: any) => {
                const key = Buffer.from(item['key'], 'base64').toString('ascii');

                const val_str = Buffer.from(item['value']['bytes'], 'base64').toString('ascii');
                const val_uint = item['value']['uint'];
                switch (key) {
                    case "Team1":
                        dapp.Team1.Name = val_str;
                        break;

                    case "Team2":
                        dapp.Team2.Name = val_str;
                        break;

                    case "Team1Total":
                        dapp.Team1.Total = val_uint;
                        break;

                    case "Team2Total":
                        dapp.Team2.Total = val_uint;
                        break;

                    case "Winner":
                        dapp.Winner = val_str;
                        break;

                    case "LimitDate":
                    case "EndDate":
                        dapp[key] = val_uint;
                        break;

                    case "Escrow": {
                        const bytes = Base64.toUint8Array(item['value']['bytes']);
                        const addr = algosdk.encodeAddress(bytes);
                        if (!algosdk.isValidAddress(addr)) {
                            throw Error(`Escrow value for app with id ${dapp.Id} is not a valid address! (${addr})`);
                        }
                        dapp.Escrow = addr
                        break;
                    }

                    default:
                        console.warn(`Unexpected global variable "${key}" from app with id ${dapp.Id}`)
                        break;
                }
            });

            dapps.push(dapp as Dapp);
        });

        return dapps;
    },
}

We’ll also want to query AlgoSigner for a list of user accounts.

// src/api/dapps.ts

    async getUserAccounts(): Promise<Account[]> {
        const accountsRaw = await AlgoSigner.accounts({
            ledger: LEDGER_NAME,
        });

        const userAccounts: Account[] = [];

        accountsRaw.forEach((account: any) => {
            const acc: Account = {
                address: account.address,
            };
            userAccounts.push(acc);
        });

        return userAccounts;
    },

Next, we want a function that gets an account’s local state for a given list of DApps. This will allow us to show the user which teams they have voted for.

// src/api/dapps.ts

    /**
     * For a given list of app ids, check if an account has opted in to it or not. If it has, also provide information on its local state.
     * 
     * @param appIds List of app ids to filter for.
     * @param accounts List of accounts to check.
     * @returns List of objects that include information about the user account, the corresponding app id, and their local state for that app id.
     */
    async getActiveDapps(dapps: Dapp[], account: Account): Promise<DappLocalState[]> {
        const activeAccounts: DappLocalState[] = [];

        // Query the indexer for account information
        const info = await AlgoSigner.indexer({
            ledger: LEDGER_NAME,
            path: `/v2/accounts/${account.address}`
        });


        if ('account' in info && 'apps-local-state' in info['account']) {
            info['account']['apps-local-state'].forEach((app: any) => {
                // Check if this app is in our list of dapps
                const dapp = dapps.find(dapp => dapp.Id === app['id']);

                // If it is, add local state information to the list
                if (dapp !== undefined) {
                    const localState: DappLocalState = {
                        dapp: dapp,
                        Team: '',
                        Bet: 0,
                        account: account,
                    }

                    app['key-value'].forEach((item: any) => {
                        const key = Buffer.from(item['key'], 'base64').toString('ascii');
                        switch (key) {
                            case "MyTeam":
                                localState.Team = Buffer.from(item['value']['bytes'], 'base64').toString('ascii');
                                break;

                            case "MyBet":
                                localState.Bet = item['value']['uint']
                                break;

                            default:
                                console.warn(`Unexpected global variable "${key}" from app with id ${app['id']}`)
                                break;
                        }
                    });

                    activeAccounts.push(localState);
                }
            });
        }

        return activeAccounts;
    },

Finally, we’ll move on to interacting with the blockchain directly, by opting-in to DApps and calling them. We’ll introduce a helper function that will allow us to get the required transaction parameters with minimal fees:

// src/api/dapps.ts

    /**
     * Query the blockchain for suggested params, and set flat fee to True and the fee to the minimum.
     * 
     * @returns The paramaters.
     */
    async getMinParams(): Promise<algosdk.SuggestedParams> {
        const suggestedParams = await AlgoSigner.algod({
            ledger: LEDGER_NAME,
            path: '/v2/transactions/params'
        });

        const params: algosdk.SuggestedParams = {
            fee: suggestedParams["min-fee"],
            flatFee: true,
            firstRound: suggestedParams["last-round"],
            genesisHash: suggestedParams["genesis-hash"],
            genesisID: suggestedParams["genesis-id"],
            lastRound: suggestedParams["last-round"] + 1000,
        }

        return params
    },

First, we’ll have a function that allows players to wager on their favorite team. It’ll work by using the javascript SDK to construct an OptIn transaction to a given DApp, and group it with a payment transaction with the escrow address. This’ll then have to be signed by the user with AlgoSigner.

// src/api/dapps.ts

    /**
     * Bet on a team by opting in to the DApp
     * 
     * @param address       The address of the user.
     * @param dapp          The DApp in question.
     * @param amount        The amount to wager.
     * @param teamName      The team name to bet for. 
     */
    async optInToDapp(address: string, dapp: Dapp, amount: number, teamName: string) {
        const params = await this.getMinParams();

        // Construct the transaction
        const tx0 = new algosdk.Transaction({
            to: dapp.Escrow,
            from: address,
            amount: amount,
            ...params,
        });
        const myTeam = new TextEncoder().encode(teamName);
        const tx1 = algosdk.makeApplicationOptInTxn(
            address,
            params,
            dapp.Id,
            [myTeam]
        );

        // Sign and send
        this.combineAndSend(tx0, tx1);
    },

    /**
     * Helper function to combine two transactions, sign them with AlgoSigner, and send them to the blockchain
     * 
     * @param tx0 The first transaction
     * @param tx1 The second transaction
     */
    async combineAndSend(tx0: Transaction, tx1: Transaction) {
        algosdk.assignGroupID([tx0, tx1]);

        const binaryTxs = [tx0.toByte(), tx1.toByte()];
        const base64Txs = binaryTxs.map((binary) => AlgoSigner.encoding.msgpackToBase64(binary));

        const signedTxs = await AlgoSigner.signTxn([
            {
                txn: base64Txs[0],
            },
            {
                txn: base64Txs[1],
            },
        ]);

        const binarySignedTxs = signedTxs.map((tx: any) => AlgoSigner.encoding.base64ToMsgpack(tx.blob));
        const combinedBinaryTxns = new Uint8Array(binarySignedTxs[0].byteLength + binarySignedTxs[1].byteLength);
        combinedBinaryTxns.set(binarySignedTxs[0], 0);
        combinedBinaryTxns.set(binarySignedTxs[1], binarySignedTxs[0].byteLength);

        const combinedBase64Txns = AlgoSigner.encoding.msgpackToBase64(combinedBinaryTxns);

        await AlgoSigner.send({
            ledger: LEDGER_NAME,
            tx: combinedBase64Txns,
        });
    },

Finally, we want to allow the user to claim or reclaim their winnings. We first need some helper functions that will calculate the amount we’re able to request as defined by the DApp’s logic.

// src/api/dapps.ts

    calculateClaimAmount(myBet: number, myTeamTotal: number, otherTeamTotal: number, fee = 1000) {
        return Math.floor(myBet / myTeamTotal * (myTeamTotal + otherTeamTotal) - fee)
    },

    calculateReclaimAmount(myBet: number, fee = 1000) {
        return myBet - fee
    },

These claim and reclaim transactions need to be signed by a LogicSig, as the escrow account will be the one sending over the winnings. We’ll have another file, escrow-teal.ts that will construct the escrow teal program with the right app id:

// src/api/escrow-teal.ts

export function escrow(app_id: number) {
    return `#pragma version 3
    global GroupSize
    int 2
    ==
    gtxn 1 TypeEnum
    int appl
    ==
    &&
    gtxn 1 ApplicationID
    int ${app_id}
    ==
    &&
    gtxn 1 OnCompletion
    int NoOp
    ==
    gtxn 1 OnCompletion
    int DeleteApplication
    ==
    ||
    &&
    gtxn 0 RekeyTo
    global ZeroAddress
    ==
    &&
    gtxn 1 RekeyTo
    global ZeroAddress
    ==
    &&
    `
} 

We can then import this function in our api/index.ts file and use it to construct the LogicSig:

// src/api/dapps.ts

import { escrow } from './escrow-teal';

export default {
    //...
    async getLogicSig(dls: DappLocalState) {
        // Compile the escrow stateless smart contract in order to construct the LogicSig
        const escrow_src = escrow(dls.dapp.Id);
        const response = await AlgoSigner.algod({
            ledger: LEDGER_NAME,
            path: '/v2/teal/compile',
            body: escrow_src,
            method: 'POST',
            contentType: 'text/plain',
        });
        if (response['hash'] !== dls.dapp.Escrow) {
            throw Error(`Escrow program hash ${response['hash']} did not equal the dapps's escrow address ${dls.dapp.Escrow}`)
        }

        const program = new Uint8Array(Buffer.from(response['result'], 'base64'));
        return algosdk.makeLogicSig(program);
    },
}

Then, to claim a user’s winnings, we’ll have to group the escrow account LogicSig transaction with a NoOp call to the DApp (with the argument 'claim').

// src/api/dapps.ts

    /**
     * Claim winnings for a given user.
     * 
     * @param dls DappLocalState object.
     */
    async claimFromDapp(dls: DappLocalState) {
        const lsig = await this.getLogicSig(dls);

        const params = await this.getMinParams();

        // Calculate winnings
        let myTeamTotal = dls.dapp.Team1.Total;
        let otherTeamTotal = dls.dapp.Team2.Total;
        if (dls.Team !== dls.dapp.Team1.Name) {
            myTeamTotal = dls.dapp.Team2.Total;
            otherTeamTotal = dls.dapp.Team1.Total;
        }
        const amount = this.calculateClaimAmount(dls.Bet, myTeamTotal, otherTeamTotal);

        // Construct the transaction
        console.log("Claiming " + amount + " with account " + dls.account.address);
        const txn_1 = new algosdk.Transaction({
            to: dls.account.address,
            from: lsig.address(),
            amount: amount,
            ...params
        })

        const args: Uint8Array[] = [];
        args.push(new Uint8Array(Buffer.from('claim')))

        const txn_2 = algosdk.makeApplicationNoOpTxn(dls.account.address, params, dls.dapp.Id, args);

        algosdk.assignGroupID([txn_1, txn_2]);

        const binaryTxs = [txn_1.toByte(), txn_2.toByte()];
        const base64Txs = binaryTxs.map((binary) => AlgoSigner.encoding.msgpackToBase64(binary));

        // Sign the app call with the user's account
        const signedTxs = await AlgoSigner.signTxn([
            {
                txn: base64Txs[0],
                signers: []
            },
            {
                txn: base64Txs[1],
            },
        ]);

        // Sign the payment transaction with the LogicSig
        const stxn_1 = algosdk.signLogicSigTransactionObject(txn_1, lsig);
        const signedTx1Binary = stxn_1.blob;
        const signedTx2Binary = AlgoSigner.encoding.base64ToMsgpack(signedTxs[1].blob);

        const combinedBinaryTxns = new Uint8Array(signedTx1Binary.byteLength + signedTx2Binary.byteLength);
        combinedBinaryTxns.set(signedTx1Binary, 0);
        combinedBinaryTxns.set(signedTx2Binary, signedTx1Binary.byteLength);

        const combinedBase64Txns = AlgoSigner.encoding.msgpackToBase64(combinedBinaryTxns);

        await AlgoSigner.send({
            ledger: LEDGER_NAME,
            tx: combinedBase64Txns,
        });
    },

And we’re done! To do a reclaim, we’ll simply have to use this.calculateReclaimAmount() to get the amount, and use 'reclaim' as the argument as opposed to 'claim'.

Vuex Store

The application uses Vuex as a way to hold global data. We’ll first create the store and introduce some state variables. We’ve got a boolean indicating whether the user has the AlgoSigner extension installed in their browser, a list of accounts the user may have, a list of DApps on the Algorand blockchain that the user can opt in to, and a list of DApps that the user has already opted-in to. Vuex also requires some mutations that are needed to modify the application state and will keep the data it contains reactive.

// src/store/index.ts

import { createStore, createLogger } from 'vuex'
import { Dapp, Account, DappLocalState } from '@/types'
import api from '@/api/dapps'


export default createStore({
    state: {
        hasAlgoSigner: false,
        userAccounts: [] as Account[],
        dapps: [] as Dapp[],
        activeDapps: [] as DappLocalState[]
    },
    mutations: {
        setHasAlgoSigner(state, value) {
            state.hasAlgoSigner = value
        },
        setDapps(state, value) {
            state.dapps = value
        },
        setUserAccounts(state, value) {
            state.userAccounts = value
        },
        setActiveDapps(state, value) {
            state.activeDapps = value
        }
    },
    // ...
})

Next, we’ll introduce some helpful getters that allow us to get a subset of ‘active’ DApps (their LimitDate has not yet passed) and ones that have expired (their LimitDate has passed).

// src/store/index.ts

    getters: {
        activeDapps(state) {
            const timestamp = Math.floor(Date.now() / 1000)
            return state.dapps.filter(dapp => dapp.LimitDate > timestamp)
        },
        expiredDapps(state) {
            const timestamp = Math.floor(Date.now() / 1000)
            return state.dapps.filter(dapp => dapp.LimitDate <= timestamp)
        }
    },

Finally, and most importantly, we’ll want a way to update the state, by using the api that we outlined previously. In Vuex this is done using actions. We’ll also have an action getAll to get all the required information, which we can use at the load time of our application.

// src/store/index.ts

    actions: {
        async getAll(context) {
            await context.dispatch('getDapps');
            await context.dispatch('getUserAccounts');
            await context.dispatch('getActiveDapps');
        },
        async getDapps(context) {
            const dapps = await api.getDapps();
            dapps.sort((a, b) => b.LimitDate - a.LimitDate);
            context.commit('setDapps', dapps);
        },
        async getUserAccounts(context) {
            const userAccounts = await api.getUserAccounts();
            context.commit('setUserAccounts', userAccounts);
        },
        async getActiveDapps(context) {
            context.commit('setActiveDapps', []);
            const activeDapps = [];

            for (const account of context.state.userAccounts) {
                const activeAccounts = await api.getActiveDapps(context.state.dapps, account);
                activeDapps.push(...activeAccounts)
                activeDapps.sort((a, b) => b.dapp.LimitDate -  a.dapp.LimitDate);

                context.commit('setActiveDapps', [...activeDapps]);
            }
        }
    },

App Components

Now that we have a good API and Vuex store setup, we can use them in the components of our application. In the main component App.vue, for example, we’ll check if the user has AlgoSigner installed, and if they do, call the getAll action so that the rest of our application can use the information in the store. We can do this by adding a method that gets called as the component gets mounted. Note that AlgoSigner may take a moment to inject itself into the web page, so we’ll call this method periodically using setTimeout if it is not detected.

// src/App.vue
import { defineComponent } from 'vue';
declare var AlgoSigner: any;

export default defineComponent({
    // ...
    computed: {
        hasAlgoSigner() {
            return this.$store.state.hasAlgoSigner;
        },
    },
    mounted() {
        this.checkAlgoSigner();
    },
    methods: {
        async checkAlgoSigner() {
            if (typeof AlgoSigner !== 'undefined') {
                this.$store.commit('setHasAlgoSigner', true);
                await AlgoSigner.connect();
                this.$store.dispatch('getAll');
            } else {
                setTimeout(() => this.checkAlgoSigner(), 10);
            }
        }
    }
    // ...
});

Our other applications can now use the information in the store by calling, for example, this.$store.state.hasAlgoSigner. In this solution, I will not go into detail on how I’ve used this information to display components on the page, as it is very subjective. If you are familiar with Vue then you will be able to create your own implementation easily. You can also view this application’s source, or see it running at https://lucasvanmol.github.io/algobets/.

Limitations and Conclusion


If you’ve read this far than I thank you! I hope it has helped with whatever project you may be working on. Without any further ado, here are some limitations to the solution outlined in this document:

  • AlgoSigner calls are blocking. To speed up the application, it may be smoother to make calls to the Algorand blockchain using a third-party service or by running your own node.
  • At the time of writing, Algorand accounts are limited to 10 active stateful applications at a time. This can be easily fixed by tweaking the code to allow multiple creator addresses.
  • The application in its current implementation is not completely trustless. This can be partially solved by not allowing the creator to delete or update the application before the expiry date. In the future, it may also be possible to have a trusted entity (known as a blockchain oracle) decide the winners. If these steps are met then the DApp would be completely trustless.

Warning

Please note that this project has not been audited for security, and is intended for instructional use only. It should not be used in a production environment.

August 12, 2021