Create Publication

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Tutorial Thumbnail
Intermediate · 1 hour

Bottle shooter smart contract using PyTeal and TypeScript

DISCLAIMER: The pseudo randomness introduced in this article is not secure and should not be used in a production environment. This is only for demo purposes of Algorand technology.

We will write a contract that implements a linear congruential recurrence relation, and use algosdk to interact with it through a web page. For wallet signing I’ll show how to use the MyAlgo wallet library

Requirements

Background

We’ll use the number generated by the LCG (Linear congruential generator) to decide whether or not the we’ve hit or missed the bottle. Everything sits in global space. There’s no knowledge of mathematics required. The generator is a simple recurrence relation of the following form (a*x + c) % m

LCG

There will be snippets of a continuation of this short story I made earlier. The full code repository of this tutorial can be found here

I had originally wrote the game as a form of Russian Roulette where I check that the bullet location matched our generated number. However, I have since adapted this to use this chance as a bottle hit or miss.

Steps

motel

I found a cheap motel room not far from the border. Cost about 1000 microalgos a night. I can hear them fixing the heater next door. Place smells like nobody opened the windows for days. I light up a cigarette to mask it a bit and open all of them. Need to pick up some air freshener. I’ll lay on this

1. Writing the contract

contract.py

I use the following template to build Stateful contracts. It contains python functions for the approval and clear parts of our Algorand Stateful contract. The approval part by itself has a constructor, a check whether the caller is admin and one Stateful contract function somefunc. I have yet to find a use for the clear part of the Stateful Contract so we just return true. The template also includes a main program for compilation of both our programs into TEAL.

from pyteal import *

def approval():
    on_init = Seq([
        App.globalPut(Bytes("admin"), Txn.sender()),
        Return(Int(1))
    ])

    is_admin = Txn.sender() == App.globalGet(Bytes("admin"))

    somefunc = Seq([
        Assert(is_admin),
        Return(Int(1))
    ])

    return Cond(
        [Txn.application_id() == Int(0), on_init],
        [Txn.on_completion() == OnComplete.UpdateApplication, Return(is_admin)],
        [Txn.on_completion() == OnComplete.DeleteApplication, Return(is_admin)],
        [Txn.application_args[0] == Bytes("somefunc"), somefunc],
    )

def clear():
  return Seq([Return(Int(1))])

if __name__ == '__main__':
  with open('game.teal', 'w') as f:
    compiled = compileTeal(approval(), mode=Mode.Application, version=2)
    f.write(compiled)
  with open('clear.teal', 'w') as f:
    compiled = compileTeal(clear(), mode=Mode.Application, version=2)
    f.write(compiled)

The constructor stores the initial global values that we use to generate new numbers. Here I’ve chosen to use initial values similar to those ZX Spectrum computer. x will store our generated number and we start off storing a big number so as to speed up usable results.

   on_init = Seq([
    # initial values for pseudo random generator
    App.globalPut(Bytes("a"), Int(75)),
    App.globalPut(Bytes("c"), Int(74)),
    App.globalPut(Bytes("m"), Int(65537)), #(1<<16)+1 or 2^16+1
    App.globalPut(Bytes("x"), Int(28652)),

We also have variables related to our main game. The hit and miss integers will start off at 0. It’s too bad that we can’t use the generator to set a number for the bullet location here.

    #
    App.globalPut(Bytes("hit"), Int(0)),
    App.globalPut(Bytes("bullet_loc"), Int(4)),
    App.globalPut(Bytes("miss"), Int(0)),
    App.globalPut(Bytes("admin"), Txn.sender()),
    #
    Return(Int(1)),
  ])

cornchippa 8

Looking around I found an old box with some antique weapon named cornchippa 8. Says its got three lead and three laser bullet. I open it and load up its ammo. looks like its running a congruential system under its mainframe. That’s pretty old school. I hope still works. When last I seen a safety lock.

Here we generate a pseudo random number using a simple linear congruential recurrence relation. TEAL has operators that we can use to do mathematics such as multiply and modulus. The are also some cryptographic primitives that are available like sha and keccak.

# x = (a*x + c) % m
gen_number = ((App.globalGet(Bytes("a")) * App.globalGet(Bytes("x"))) + App.globalGet(Bytes("c"))) % App.globalGet(Bytes("m"))

With no gravity you gotta have a little bit of extra finesse.

Let’s work on the logic for hitting and missing a bottle. We just update these by adding to the globally stored integers. This information is relevant to the game attached to this random number generator.

  hit = Seq([
    App.globalPut(Bytes("hit"), App.globalGet(Bytes("hit")) + Int(1)),
    Return(Int(1))
  ])

  miss = Seq([
    App.globalPut(Bytes("miss"), App.globalGet(Bytes("miss")) + Int(1)),
    Return(Int(1))
  ])

parking lot

Lets take this baby out for a spin at the gravity less parking lot. There should be some bottles floating around. I’ll try to take a few of them out. Forget about it.

Here we use the scratch space to store a reduced version of our generated random number to the interval [0,6]. This ensures that the player has a 1/6 chance to hit a bottle.

The TEAL spec provides us with scratch space in addition to whats already on the stack. You can use this to store data that you make decisions with and never have a need to actually save it for later use.

If we still have targets to shoot at (ie hit count is smaller than maximum allowed), the logic will generate a number and check it against the corresponding bottle location. From this we decide whether to call the hit or miss logic. In pyteal, an If statement is of the form If(expr, then-expr, else-expr).

  rand = ScratchVar(TealType.uint64)
  take_shot = Seq([
    Assert(App.globalGet(Bytes("hit")) < Int(10)),
    App.globalPut(Bytes("x"), gen_number), # update random value
    rand.store(App.globalGet(Bytes("x")) % Int(6)), # reduce to six shots
    If(rand.load() == App.globalGet(Bytes("bullet_loc")),
      hit,
      miss
    ),
    Return(Int(1)) 
  ])

This is how I trap any adversary trying to take care of me in a parking lot. If they try to move around here one of these shattered glass will cut them in the wrong way. If you got some plerium quartz then you could probably blow all these cars up. I’m gonna head back into the motel before I get in trouble for this.

When resetting the game, we first check if the user is admin before resetting the relevant variables. We set the new bullet location to the the second argument of the reset call (converted into an integer via Btoi). The user must own the contract to do this.

  loc = Btoi(Txn.application_args[1])
  reset_game = Seq([
    Assert(is_admin),
    App.globalPut(Bytes("hit"), Int(0)),
    App.globalPut(Bytes("bullet_loc"), loc),
    App.globalPut(Bytes("miss"), Int(0)),
    Return(Int(1)),
  ])

Finally we put together the Algorand program via a Conditional that creates the app; update or delete the app depending on whether the user calling is admin; or handle some function calls requested by the user.

  return Cond(
    [Txn.application_id() == Int(0), on_init],
    [Txn.on_completion() == OnComplete.UpdateApplication, Return(is_admin)],
    [Txn.on_completion() == OnComplete.DeleteApplication, Return(is_admin)],
    [Txn.application_args[0] == Bytes("take_shot"), take_shot],
    [Txn.application_args[0] == Bytes("reset_game"), reset_game],
  )

2. Writing the typescript frontend

index.ts
On our Typescript frontend we’ll require the myalgo-connect and algosdk libraries.

The required packages are here

npm i @randlabs/myalgo-connect algosdk axios # dependencies
npm i -D @rollup/plugin-{commonjs,json,node-resolve,typescript} rollup-plugin-node-polyfills typescript # dev dependencies

For building Typescript applications I use the following boilerplate. The required compilation configuration files are included in the main repo. Namely, tsconfig.json and rollup.config.js

From an error handling perspective, it will be a bit tedious to setup because a lot of information about whats happening when you interact with the wallet will come back as an error. There isn’t an easy way to parse all this yet though.

import MyAlgoConnect, {CallApplTxn} from '@randlabs/myalgo-connect';
import algosdk from 'algosdk';

class App {
    // attributes
    appid: number;
    wallet: any;
    algodClient: any;
    wallet: any;
    accounts: any;
    addresses: any;
    elem:HTMLElement;

    constructor() {
        this.elem = document.createElement('div');
    }

    connect() {}
    callapp() {}
    readapp() {}

    render() {
        document.getElementById('root').appendChild(this.elem);
    }

    // helper functions
}

let app = new App();
app.render();

To instantiate the algodClient you need information about your node. This first parameter is the contents of algod.token file. The second and last parameters are the hostname and port of your running node. I’m using a private net at the moment from algo-builder sandbox. You don’t require extra information to instantiate the MyAlgoConnect wallet. This logic is in the constructor.

this.algodClient = new algosdk.Algodv2(
  'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 
  'http://127.0.0.1', 
  '4001'
);
this.wallet = new MyAlgoConnect();

This is how we connect to the MyAlgo wallet and store the user’s accounts. This logic sits in the connect app and will be activated when clicking the “Connect Wallet” button. This logic is in the connect function.

this.accounts = await this.wallet.connect();
this.addresses = this.accounts.map(account => account.address);

Connect Wallet

We read from the global state using algosdk. The data from the retrieved comes encoded in base64. We’ll need to convert the keys and produce a new dictionary. This logic is in the readapp function.

let app = await this.algodClient.getApplicationByID(this.appid).do();
var recentState = {};
for (var key in app.params['global-state']) {
    let r = app.params['global-state'][key];
    recentState[atob(r.key)] = r.value;
}

Before we can display this new dictionary we check for differences between the current game state and the most recent one just retrieved from Algorand. Similar to React's life cycle management. We can also check to see if the game is still running or otherwise we’ve shot all the bottles.

if (this.gamestate !== null) {
    if (this.gamestate.hit.uint >= this.maxhit) {
      console.log("dead: " + this.gamestate.hit.uint + " > 10");
      this.msg.innerText = "dead: " + this.gamestate.hit.uint + " > ";
      this.updateImage(dead);
      return ;
    }

    if (this.gamestate.hit.uint !== recentState.hit.uint) {
        console.log('got hit');
        this.msg.innerText = 'got hit';
    } 
    if (this.gamestate.miss.uint !== recentState.miss.uint) {
        console.log('missed');
        this.msg.innerText = 'missed';
    }
} else {
    if (recentState.hit.uint >= this.maxhit) {
        console.log("dead: " + recentState.hit.uint + " > 10");
        this.msg.innerText = "dead: " + recentState.hit.uint + " > 10";
        this.updateImage(dead);
    } else {
        this.msg.innerText = "nothing to see here";
    }
}

We can now provide a view for the user by wrapping the game state information in this div. You can locate the values based off the type of it in the contract ie uint or string.

this.gamestate = recentState;
let disp = document.getElementById('health');
disp.innerHTML = `<div class="health">
        <img src="https://i.imgur.com/orJnFPc.png" />
        <div style="clear:both"></div>
        <div class="desc">
          Bullet location: ${this.gamestate.bullet_loc.uint}<br/>
          Hits: ${this.gamestate.hit.uint} of ${this.maxhit}<br/>
          Misses: ${this.gamestate.miss.uint}<br/>
        </div>
      </div>`;

View game information

To create a transaction that will make a call to our Algorand application. We first retrieve default parameters like firstValidRound, genesisHash, etc from getTransactionParams() function. We populate the appIndex and appArgs according to the CallAppTxn type defined by the wallet library. This logic is in the callapp function.

let txnn = await this.algodClient.getTransactionParams().do();
let txn: CallApplTxn = {
    ...txnn,
    from: this.addresses[0],
    fee: 1000,
    flatFee: true,
    appIndex: this.appid,
    type: 'appl',
    appArgs: [btoa("take_shot")],
    appOnComplete: 0,
};

Here we sign the transaction and send it as a raw transaction. Upon completion we can update the display according to the new global state.

let signedTxn = await this.wallet.signTransaction(txn);
await this.algodClient.sendRawTransaction(signedTxn.blob).do();
this.readapp();

Call App

Now let’s instantiate the app, add a few buttons and display the view on the HTML page. This logic is outside the class.

let app: App = new App();

let connectWalletBtn = document.createElement('button');
connectWalletBtn.onclick = async function() {
  app.connect();
}

let callAppBtn = document.createElement('button');
callAppBtn.onclick = async function() {
  app.callapp();
}

app.addbtn(connectWalletBtn);
app.addbtn(callAppBtn);
app.render();

4. Compiling TEAL and deploying to a node.

To build and deploy the contract we use goal. We store one byteslice (contract creator address) and 7 integers related to the game in global space. We store nothing locally.

python contract.py
goal app create --creator $ME --global-byteslices 1 --global-ints 7 --local-byteslices 0 --local-ints 0 --approval-prog game.teal --clear-prog clear.teal

The app id provided on successful completion of the above command can be added to the appid variable of the class.

appid: number = 296143611;

5. Conclusion

I deployed a working version that you can try out. The full code is here