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
Beginner · 30 minutes

Siam - Managing Global Application State

With Siam, you can store data directly on-chain using a simple interface written in Go:

err := b.PutElements(ctx, map[string]string{
    "key1" : "value1",
    "key2" : "value2",
})
err = b.DeleteElements(ctx, "key1")

data, err := b.GetBuffer(ctx)

// will print map[key2:value2]
fmt.Print(data)

You don’t need to write any smart contracts or generate any transactions. Data is stored directly in the global state of a stateful smart contract (a.k.a application) on the Algorand blockchain. You can use this to e.g. provide oracle data to other smart contracts.

Requirements

  • Basic knowledge about Blockchain and Algorand.
  • Basic knowledge of Go

Steps

1. Installation

To use this library in your Go project, simply go to the folder that contains the go.mod file and run

go get github.com/m2q/algo-siam

Then you can import the package using

import siam "github.com/m2q/algo-siam"

2. Configuration

Configuring Siam requires three things:

  • URL/IP address of an Algorand node

  • An API token

  • A base64-encoded private key of an account that will be used to manage the storage (we’ll call this the target account). It needs to have enough ALGO balance to create applications and publish transactions.

If you are running your own node, you should have the URL/IP and API token already. If you are not running your own node, you can use the URL https://testnet.algoexplorerapi.io (AlgoExplorer does not require an API token). However, if you’re frequently developing on Algorand, I’d advise using the Algorand sandbox.

To generate a new private key, simply run the following line in Go

siam.PrintNewAccount()

which will print something like this

Public Address: YTBLRR2R72QRL6YMOJAXUJSLLM74VHPGSVMOJ2QAYZ3QGQL3GVVHHL2AQM
Private Key: L5W36fpdAYnX2V2zgcLlTEfn65G0nuYLeeTlhrpH/N/EwrjHUf6hFfsMckF6JktbP8qd5pVY5OoAxncDQXs1ag==

Alternatively, you can use one of the available Algorand SDKs (Go, Python, JavaScript, …).

3. Create an AlgorandBuffer

The AlgorandBuffer is our interface to the on-chain key-value store. To create one, we need the 3 variables from before:

c := client.CreateAlgorandClientMock(URL, token)
buffer, err := siam.NewAlgorandBuffer(c, base64key)

That’s it. You can also use environment variables instead.

Environment Variable Example value
SIAM_URL_NODE https://testnet.algoexplorerapi.io
SIAM_ALGOD_TOKEN aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
SIAM_PRIVATE_KEY 5GfJ7SmRCsPpTGGdbSFdCwQ+gNeazr1MWqhNzPmxChSYRRnrvfwopM4QPLQLRu74aJgYP8gAoVhM6bklliG3VQ==

If you configured these environment variables, you can create an AlgorandBuffer with one line:

buffer, err := siam.NewAlgorandBufferFromEnv()

Note

If the node is not reachable, the API key is incorrect or the target account doesn’t have enough funds, then an error will be returned. Never ignore returned errors.

Verifying it works

If no errors were returned you can run fmt.Println(buffer.AppId) to find out the application’s ID that will hold your data. You can always use this ID to manually check if the application has the right data and the program works as intended, by using CLI tools like goal or entering the ID into AlgoExplorer, if you deployed to the testnet/mainnet.

4. Writing and Deleting Data

To store data, simply call PutElements and provide the data as a map

data := map[string]string{
    "key1": "value1",
    "key2": "value2",
    "key3": "value3",
}

err := buffer.PutElements(context.Background(), data)
if err != nil {
    log.Fatalf("error writing data: %s", err)
}

The method PutElements follows the usual PUT semantics. That means that you can use it to update values of existing keys

err := buffer.PutElements(context.Background(), map[string]string{"key2" : "newValue"})
if err != nil {
    log.Fatalf("error writing data: %s", err)
}

To delete data, simply provide the keys.

err = buffer.DeleteElements(context.Background(), "key1", "key3")
if err != nil {
    log.Fatalf("error deleting data: %s", err)
}

Now there should be only one key value pair left in the Algorand application. We can confirm that by calling

data, err = buffer.GetBuffer(context.Background())
if err != nil {
    log.Fatalf("error fetching data: %s", err)
}

fmt.Println(data)

which prints

map[key2:newValue]

Note

Instead of providing context.Background(), you can provide your own context with timeouts and cancel functions.

5. Limitations

Note that Algorand stateful smart contracts have several limitations. You can find an up-to-date parameter list here. The most important thing to note is the Max number of global state keys and Max key + value size. These determine the max number of elements that the application can store, and the size of each kv-pair.

Name Current Value
Max number of global state keys 64
Max key + value size 128 Bytes
Max key size 64 Bytes

6. Use-case: Oracle for Esports Match Data

Now that you know how to store data on the Algorand blockchain, you can start writing your own oracle. You can check out an example I wrote, siam-cs. It’s an oracle that stores recent CSGO esports data. The keys are match IDs, and the value is the winner of a match. The heart of the application is pretty simple:

// serve attempts to bring the AlgorandBuffer in a desired state. 
func (o *Oracle) serve(ctx context.Context) {
    // fetch CSGO matches
    past, future, err := o.cfg.PrimaryAPI.Fetch()
    if err != nil {
        log.Print(err)
        return
    }
    desired := ConstructDesiredState(past, future, client.GlobalBytes)
    err = o.buffer.AchieveDesiredState(ctx, desired)
    if err != nil {
        log.Print(err)
    }
}

First, it fetches the newest information from a third-party API provider and then constructs a desired state. The desired state is what we want the on-chain state to look like. In my case, I wanted the buffer to contain only upcoming and recently played matches. If a match is older than 3 days, it’s discarded. So I need to Delete old matches, Put new matches, and update matches that concluded and have a winner.

The method AchieveDesiredState makes this easy for us. It brings the application into a desired state as efficiently as possible, by constructing the smallest number of Put/Delete transactions and executing them. If the on-chain data is already “desired”, then no transaction will be submitted.

For the oracle to be robust, you would ideally compare data from several providers at the same time and only allow data to be published if every provider agrees.