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.