Today I want to show you how one can use docker compose
in order to bootstrap
a local miniwasm
blockchain where one can deploy and
test smart contracts in isolation.
You can find the code below as a working example in this repository.
Before we look at the smart contract test, we have to speak about the requirements. In a typical application of our customers, a requirement might be to update UI state when the relevant smart contracts have been interacted with. To this end one can leverage websocket subscriptions to events concerning the smart contracts.
For today’s post I would like to simplify these requirements a bit: Imagine an application which shows the current balance of an address to the user. Whenever the balance on the blockchain changes, the UI should update.
The implementation of these requirements is pretty straightforward. We need to
store the value of the balance as state in the front end, potentially using
something like zustand
, redux
or vanilla javascript (I went for the latter
in my code, but you can make it as fancy as you like). Then we need to
initialize the state with the value from the chain, which requires some IO,
typically an async operation in JavaScript. Additionally we have to set up a
subscription to change events for this value in order to get real time updates
to it. You can read up the
implementation details
in the repository, they are not very important for this post.
Since we cannot “code in the dark” and just hope that we got the implementation right, we have to test. If we were to test this functionality by hand, we would have to:
Probably, we would not get the implementation straight on the first attempt. For example, I was struggling a bit to get the right query filter for my subscription:
wsClient.subscribe(
'Tx',
{
'transfer.recipient': address,
},
async () => (balance = await getBalance(lcdClient, address)),
)
It took me some attempts to find the correct value. Testing each one of these by hand would have been very expensive in sense of development time spent.
Please also note that this is a very simple and contrived example. Usually, requirements are much more complex.
Let us consider the blockchain on which to run tests on. We could use the public Initia testnet for this, however we are then limited to 30 INIT per day per person, so potentially we can quickly run out of funds to test. Of course we could “recycle” the money by sending it back from the receiving address but this also takes time and is annoying.
One idea could be to use our own testnet chain with our own coins of which we could have lots and lots. Better, but so far we would still spam our public testnet with many pointless transactions and we would have to do the gymnastics with the user interface and the button clicking.
So what we really want is to automatically run the tests. To give you a feel on what such a test could look like, here is an excerpt from the test file in the repo.
describe('Live user balance', () => {
test('The balance updates when funds are received', async () => {
// arrange
const randomWallet = createRandomWallet()
const balance = await liveBalance({
websocketUrl: 'ws://localhost:26657/websocket',
lcdUrl: 'http://localhost:1317',
})(randomWallet.key.accAddress)
onTestFinished(balance.destroy)
// assert
expect(balance.getBalance()).toBe(0)
await waitForBoth([
// act
sendFunds(randomWallet.key.accAddress, 1000),
// assert
vi.waitFor(() => expect(balance.getBalance()).toBe(1000)),
])
})
})
So here we:
0
),This test is only possible with a precondition: We need to have an account with more or less unlimited funds on the blockchain we want to run our tests on. Let us spawn one!
In order to have a local blockchain available, we need to start and configure a
node before we can run the tests. You can see in the
CI script
that we use docker compose
for this. We use the official
miniwasm
docker image
but change the entrypoint to be:
#!/bin/sh
if [ -f /root/.minitia/config/genesis.json ]; then
echo "genesis.json found. Skipping initialization commands..."
else
echo "genesis.json not found. Running initialization commands..."
minitiad init operator --chain-id testnet &&
minitiad keys add operator --keyring-backend test &&
minitiad genesis add-genesis-account init1j65sfxpkpstety502upxk0t6xhvcuclxawpqt8 1000000000000000umin --keyring-backend test &&
minitiad genesis add-genesis-validator operator --keyring-backend test &&
minitiad start
fi
echo "Starting minitiad..."
minitiad start
This means that before we start the chain for the first time, we credit ourselves a very high amount of funds to be used in the tests.
Please refer for the complete setup to the repository. Once all is in place we can start the local block chain with
docker compose up -d
and stop it with
docker compose down -v
which will also delete any state the blockchain had after our tests (probably
some random addresses funded with 1000umin
).
With the local blockchain running in the background and our wallet being as rich as it can possibly be, we can now start developing by running
yarn test
which will start vitest
in development mode, so every
change to the source files will trigger a rerun of the test. Now rerunning the
tests after changes to the code takes less than 800ms in the smallest github
code space instance.
I hope you enjoyed this demonstration of a viable test setup for app developers on Initia chains such as Contro. I recommend to fork or clone our repository when you want to build something similar. Please also let me know if you have further comments on the set up, found a better way to do things or have questions on how to get it running for yourselves.
Until then have a glob time!
Levin - Lead Web Developer
Please note that getBalance
is synchronous and does not do a query to the
blockchain or a remote server. It simply returns the current local value of
the balance. ↩