Tutorials
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.

Tutorial

Beginner · 1 hour

Simple NPC game interactions using a stateful contract and atomic transfers

Using a stateful contract and atomic transfers we’ll keep track of a health variable and let users decide whether to help or deceive the NPC character for a small amount of algos. Once the health has reached full or has diminished to nothing, the game ends.

From a DevOps perspective, I’ll show how to use Scale-It’s private net sandbox that you can get up and running in a matter of seconds.

Requirements

  1. The python3 abstraction of TEAL called pyteal
  2. goal command line tool

Background

This tutorial will only read/write from/to the global state. However, Algorand’s Smart Contract’s are powerful in that you can also store state locally on the user’s account (for example to check if they voted or not). To store some data on a user’s local account, they have to opt-in to use the application and upon exiting the contract they invoke a CloseOut procedure, whence the Stateful Contract can modify their participation data (or not).

We will not cover local state as its not required for the game to be functional. If you’re interested in local storage too, have a look at this voting contract. It touches the account making it only possible to vote once.

Steps

1. Anatomy of the contract program

We’ll use the following stateful contract template. It contains a function approval that will return our approval part of the stateful contract. It also contains a function clear_state that will return the CloseOut part. Here you can delete the user’s participation data or do some other “opt-out” logic. Finally, the main function compiles both programs to produce the two .teal source files.

# contract.py
from pyteal import *

def approval():
    # create program here
    return program

def clear_state():
    program = Seq([Return(Int(1))])
    return program

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

    with open('clear_state.teal', 'w') as f:
        compiled = compileTeal(clear_state(), mode=Mode.Application, version=2)
        f.write(compiled)

For the constructor, we’ll use the address of the contract creator as the address for our NPC, George. We’ll also set the health variable of our NPC to 5. To store and read a variable to/from the state we use App.globalPut(name, value) and App.globalGet(name), respectively.

on_init = Seq([
    App.globalPut(Bytes("george"), Txn.sender()),   # save the creator's address
    App.globalPut(Bytes("health"), Int(5)),         # set an integer to 5
    Return(Int(1)),
])

Next, there are two conditions which need to be reused. Let’s create two booleans that will store these values. The first condition checks if the atomic transaction will eventually send the required amount to George’s wallet (Contract Creator). The second will check that the health variable is still within our desired threshold.

We use a built-in pyteal variable called Gtxn which stores grouped transactions in a list. The second transaction should be on indice 1. We will request the user to send at least 100 microalgos, so as to alleviate congestion when working within public networks. From a gaming perspective, its much more efficient to minimize the number of transactions your application uses.

# check if 2nd tx amount is correct and that george receives the payment
correct_amt_for_george = If(Or(Gtxn[1].amount() < Int(100), # Check for at least 100 microalgos
                               Gtxn[1].receiver() != App.globalGet(Bytes("george"))),
                               Return(Int(0)))

# check if npc is dead or fully replenished
alive_or_dead = If(Or(App.globalGet(Bytes("health")) >= Int(10), 
        App.globalGet(Bytes("health")) <= Int(0)), Return(Int(0)))

The injure and heal functions check if George has transcended (reached full health), or has died before updating the health variable (either by adding or subtracting 1). The game ends when the NPC’s health becomes 0 or max (10) as every transaction after that will revert.

on_injure = Seq([
    alive_or_dead,
    correct_amt_for_george,
    App.globalPut(Bytes("health"), App.globalGet(Bytes("health")) - Int(1)),
    Return(Int(1)),
])

on_heal = Seq([
    alive_or_dead,
    correct_amt_for_george,
    App.globalPut(Bytes("health"), App.globalGet(Bytes("health")) + Int(1)),
    Return(Int(1)),
])

The update and delete logic will check if the user owns the contract before executing. Finally, we combine all these methods into a single program.

program = Cond(
    [Txn.application_id() == Int(0), on_init],                                  # init
    [Txn.on_completion() == OnComplete.DeleteApplication, Return(is_creator)],  # delete
    [Txn.on_completion() == OnComplete.UpdateApplication, Return(is_creator)],  # update
    [Txn.application_args[0] == Bytes("heal"), on_heal],                        # heal
    [Txn.application_args[0] == Bytes("damage"), on_injure],                    # injure
)

Running the script will produce the teal files required during deployment. Please ensure you’re running python version 3 or higher.

python contract.py

If you still have python2 on your system, consider using the command to specify python3.

python3 contract.py

2. Deploying the script

This section requires that you have a private net running, and that you have the goal command installed on your system. If you would rather use a different network like TestNet, you can update the commands to use that network’s data directory instead.

Otherwise, to run a private node, Scale-It has a private node quick-starter that you can install with the following commands:

git clone https://github.com/scale-it/algo-builder
cd algo-builder/infrastructure
make create-private-net
export ALGO_PVTNET_DATA=/path/to/algo-builder/infrastruture/node_data/PrimaryNode

To view some information about available accounts and wallets you can use the following commands:

goal wallet list -d $ALGO_PVTNET_DATA
goal account list -d $ALGO_PVTNET_DATA

Let’s use a Makefile in order to make it simpler for us to interact with George through commands like make heal. We start off by setting the variables that we’ll most likely reuse at the top of the file.

GEORGE_ACCOUNT      := creatorAddress
ALGO_PVTNET_DATA    := /path/to/algo-builder/infrastructure/node_data/PrimaryNode
ALGO_TESTNET_DATA   := ~/.testnet-algod
ALGO_MAINNET_DATA   := ~/.mainnet-algod

# pick a net
ALGO_DATA           := $(ALGO_PVTNET_DATA)

# during test we'll use a different account to interact with George
FROM_ACCOUNT        := differentAddress
TO_ACCOUNT          := $(GEORGE_ACCOUNT)

To deploy the app (or bring George to life), we use the following CLI command. For our contract global state we use one string (owner address) and one integer (health variable). We store nothing on the user’s account so local state space will have 0 integers and strings.

deploy:
    goal app create --creator $(GEORGE_ACCOUNT) \
        --approval-prog ./approval.teal \
        --clear-prog ./clear_state.teal \
        --local-byteslices 0 \
        --local-ints 0 \
        --global-byteslices 1 \
        --global-ints 1 \
        -d $(ALGO_(DATA)

Now it’s as simple as running make deploy. Once successfully deployed, you can save the App ID in the environment for ease of use within upcoming shell commands. This also makes it easier to change the active app, without editing the Makefile itself.

export APP_ID=42

3. Test the application

We can now use the application and query its state. For example, if you want to read the values of the global state, use the following command:

read:
    goal app read --app-id $(APP_ID) \
        --global -d $(ALGO_DATA) \
        --guess-format

You should see George’s address and the value of his health in json format. E.g.: make read

{
  "george": {
    "tb": "6HZOQLMJCMKNVNMEJVTI3BD3FZIO2EJGTINJKT4NOYMR577YI4VZHP3NHM",
    "tt": 1
  },
  "health": {
    "tt": 2,
    "ui": 5
  }
}  

Currently it’s set to 5 as that’s our initial value during contract creation. Let’s interact with George to change it. We’ll need to create an atomic transaction that will:
1. Make a call to the application. (For this, we’ll invoke the heal logic of the contract.)
2. Send the required minimum amount to George’s Wallet

The following snippet shows how the heal command will look in the Makefile. First we make a call to the application, then we send at least 100 microalgos to George. The result of this is two transaction files tx1 and tx2.

heal-cmd:
    goal app call --app-id $(APP_ID) \
        --app-arg 'str:heal'  \
        -f $(FROM_ACCOUNT) \
        -d $(ALGO_DATA) \
        -o tx1

amount:
    goal clerk send -a 100 \
        -t $(TO_ACCOUNT) \
        -f $(FROM_ACCOUNT) \
        -d $(ALGO_DATA) \
        -o tx2

Now we need to create and sign a group transaction so the contract will be able to interrogate it. And then, finally, we send the atomic transaction to the network.

group-send:
    cat tx1 tx2 > ctx
    goal clerk group -i ctx -o gtx -d $(ALGO_DATA)
    goal clerk sign -i gtx -o stx -d $(ALGO__DATA)
    goal clerk rawsend -f stx -d $(ALGO_DATA)

The full command to send the atomic transfer should now be simply: heal-cmd amount group-send.

heal: heal-cmd amount group-send

If everything was done correctly, on reading the state again, the health variable should have updated. George should have a little tip too.

$ make heal
$ make read
{
  "george": {
    "tb": "6HZOQLMJCMKNVNMEJVTI3BD3FZIO2EJGTINJKT4NOYMR577YI4VZHP3NHM",
    "tt": 1
  },
  "health": {
    "tt": 2,
    "ui": 6
  }
}

Now let’s injure him twice. His health variable should have decreased to 4.

$ make injure
$ make injure
$ make read
{
  "george": {
    "tb": "6HZOQLMJCMKNVNMEJVTI3BD3FZIO2EJGTINJKT4NOYMR577YI4VZHP3NHM",
    "tt": 1
  },
  "health": {
    "tt": 2,
    "ui": 4
  }
}

Sending to the wrong address in the second transaction of the group will cause the approval program to return a failure, thus causing the entire atomic transfer to fail. For an atomic transfer to be successful, all transactions attached to it should succeed.

To retire the application, we call the app delete method from the goal CLI:

delete:
    goal app delete --app-id $(APP_ID) -f $(GEORGE_ACCOUNT) -d $(ALGO_DATA)

And then you can call make delete from the command line.

4. Further development

You can now build a front end that, for example, will return a feel good message during heal or a nasty remark while being injured :)

The full project can be found at my github

July 22, 2021