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.