This repository is now deprecated in favour of using official Algorand images via AlgoKit LocalNet. This repository will be archived.
This repo is loosely based on Algorand's official sandbox, but solves some performance and experience problems which impact local development and CI/CD usage.
The key problems we resolve are:
- Long build times for CI/CD
- Slow devnet + indexer performance
- Ability to use cross-platform (e.g. on Windows)
This project was developed by MakerX to help us with our initiatives, but we are happily providing it for the Algorand community to use as Open Source.
You'll need Docker installed. The following examples also use docker compose
, which ships with Docker Desktop on Windows and macOS - for Linux users, see the installation instructions.
Either save the following as docker-compose.yaml
locally, or if you've already got a compose setup, just copy in the
services:
version: '3'
services:
algod:
image: makerxau/algorand-sandbox-dev:latest
ports:
- "4001:4001"
- "4002:4002"
- "9392:9392"
indexer:
image: makerxau/algorand-indexer-dev:latest
ports:
- "8980:8980"
restart: unless-stopped
environment:
ALGOD_HOST: "algod"
POSTGRES_HOST: "indexer-db"
POSTGRES_PORT: "5432"
POSTGRES_USER: algorand
POSTGRES_PASSWORD: algorand
POSTGRES_DB: indexer_db
depends_on:
- indexer-db
- algod
indexer-db:
image: "postgres:13-alpine"
container_name: "algorand-sandbox-postgres"
ports:
- "5433:5432"
user: postgres
environment:
POSTGRES_USER: algorand
POSTGRES_PASSWORD: algorand
POSTGRES_DB: indexer_db
Then run docker compose up
in the directory of your compose file. This will pull the pre-built images down from DockerHub.
Currently, the only tags being pushed are latest
, which are manually triggered builds based on the latest stable
releases of both go-algorand and indexer.
Note that when using the latest
tag, if a new build gets pushed to DockerHub but you have an existing stack,
docker compose
won't check to see if there's a new build available. To update, stop the running stack and:
docker compose down
docker compose pull
This should fetch the latest images and you can start your stack again.
Note in particular that we need to use down
here, and not just stop
(which is what ctrl-c will do if you're not running docker compose with the -d
flag),
a new build will generate a new network genesis, but the database container will still contain data from the previous network.
If you don't remove the container before restarting (e.g. through the down
command above), you'll get errors like the following:
algorand-sandbox-indexer | {"error":"genesis hash not matching","level":"error","msg":"importer.EnsureInitialImport() error","time":"2022-02-25T01:33:59Z"}
algorand-sandbox-indexer exited with code 1
If you want to test out a newer version (e.g. the latest beta version of go-algorand
), you can check out this repo,
and run your docker compose
commands from here, the BRANCH
args in docker-compose.yaml
control which git branches
of their respective repos get checked out and built.
Also, be careful of the indexer-db
container persisting after a rebuild, as noted above in Option A, this is even
more important if you're building/re-building locally.
There are a number of things that you can do with sandbox.sh
within the Algorand Sandbox that we have created cross-platform equivalents of using PowerShell Core.
In order to check the status of the running environment, copy status.ps1 into your project wherever you put your docker-compose.yml
file and execute it via ./status.ps1
.
In order to reset the environment, copy reset.ps1 into your project wherever you put your docker-compose.yml
file and execute it via ./reset.ps1
.
In order to run a goal command against your algod instance, copy goal.ps1 into your project wherever you put your docker-compose.yml
file and execute it via ./goal.ps1 {command}
e.g. ./goal.ps1 account list
.
If you want to use the sandbox then you need to get the private key of the initial wallet so you can transfer ALGOs out of it to other accounts you create. There are two ways to do this:
./goal.ps1 account list
./goal.ps1 account export -a {address_from_online_account_from_above_command}
Needing to do this manual step every time you spin up a new development environment or reset your Sandbox is frustrating. Instead, it's useful to have code that uses the Sandbox APIs to automatically retrieve the private key of the default account. The following getSandboxDefaultAccount
function (in TypeScript, but equivalent will work with algosdk in other languages) will help you achieve that:
// account.ts
import algosdk, { Account, Algodv2 } from 'algosdk'
import { getKmdClient } from './client'
export async function isSandbox(client: Algodv2): Promise<boolean> {
const params = await client.getTransactionParams().do()
return params.genesisID === 'devnet-v1' || params.genesisID === 'sandnet-v1'
}
export async function isReachSandbox(client: Algodv2): Promise<boolean> {
const params = await client.getTransactionParams().do()
return params.genesisHash === 'lgTq/JY/RNfiTJ6ZcZ/Er8uR775Hk76c7MvHVWjvjCA='
}
export function getAccountFromMnemonic(mnemonic: string): Account {
return algosdk.mnemonicToSecretKey(mnemonic)
}
export async function getSandboxDefaultAccount(client: Algodv2): Promise<Account> {
if (!(await isSandbox(client))) {
throw "Can't get default account from non Sandbox network"
}
if (await isReachSandbox(client)) {
// https://github.com/reach-sh/reach-lang/blob/master/scripts/devnet-algo/algorand_network/FAUCET.mnemonic
return getAccountFromMnemonic(
'frown slush talent visual weather bounce evil teach tower view fossil trip sauce express moment sea garbage pave monkey exercise soap lawn army above dynamic'
)
}
const kmd = getKmdClient()
const wallets = await kmd.listWallets()
// Sandbox starts with a single wallet called 'unencrypted-default-wallet', with heaps of tokens
const defaultWalletId = wallets.wallets.filter((w: any) => w.name === 'unencrypted-default-wallet')[0].id
const defaultWalletHandle = (await kmd.initWalletHandle(defaultWalletId, '')).wallet_handle_token
const defaultKeyIds = (await kmd.listKeys(defaultWalletHandle)).addresses
// When you create accounts using goal they get added to this wallet so check for an account that's actually a default account
let i = 0
for (i = 0; i < defaultKeyIds.length; i++) {
const key = defaultKeyIds[i]
const account = await client.accountInformation(key).do()
if (account.status !== 'Offline' && account.amount > 1000_000_000) {
break
}
}
const defaultAccountKey = (await kmd.exportKey(defaultWalletHandle, '', defaultKeyIds[i])).private_key
const defaultAccountMnemonic = algosdk.secretKeyToMnemonic(defaultAccountKey)
return getAccountFromMnemonic(defaultAccountMnemonic)
}
To get the Kmd
instance you can use something like this (tweak it based on where you retrieve your ALGOD token and server from):
// client.ts
import {Kmd} from 'algosdk'
// KMD client allows you to export private keys, which is useful to get the default account in a sandbox network
export function getKmdClient(): Kmd {
// We can only use Kmd on the Sandbox otherwise it's not exposed so this makes some assumptions (e.g. same token and server as algod and port 4002)
return new Kmd(process.env.ALGOD_TOKEN!, process.env.ALGOD_SERVER!, '4002')
}
This turned out to be a bug, and has been fixed upstream as of indexer version 2.10.0!
Building the official algorand sandbox as part of CI/CD takes quite some time, so we do this as part of a separate pipeline
(see azure-pipelines.yml
in this repo for the configuration), and push the results to DockerHub. As outlined above,
we've also made these builds public, which saves time locally when updating to new versions.
We've also optimised the docker images to minimise their size by utilising multi-stage builds, and building for multiple architectures (AMD64 and ARM64 - so M1 Macs are supported).
There was a potential race condition in the sandbox setup, where the indexer
might start up before postgres
comes alive,
particularly on first run when the database is being initialised. This is solved in the sandbox by specifying the indexer
service to have restart: unless-stopped
, which we've also retained here, but to prevent potentially confusing error
messages and/or container restarts, we've made the indexer
wait on startup until the postgres
daemon is responding
to network requests.
reachsh/devnet-algo
- on DockerHub, but not always kept up to date, and only supports AMD64 images. Also, no access
to kmd
.
There appears to be an issue with DevMode
enabled, where after ~258 rounds, a Compact Cert is generated, and the
indexer can't determine the signature type for it:
algorand-sandbox-indexer | {"level":"info","msg":"adding block 259","time":"2022-02-25T04:38:44Z"}
algorand-sandbox-indexer | {"error":"AddBlock() err: TxWithRetry() err: attemptTx() err: AddBlock() err: AddBlock() err: getSigTypeDelta() err: unable to determine the signature type","level":"error","msg":"adding block 259 to database failed","time":"2022-02-25T04:38:44Z"}
At this point, the only way to resolve the issue is to re-create the containers by stopping the stack and running docker compose down
before restarting it.