LiquidApps Walkthrough #3: LiquidAccounts

No Hassle. No Wallets. No Keys.

DAPP Network
The DAPP Network Blog
6 min readJan 10, 2020


In earlier walkthroughs, we have set up Zeus SDK — although Zeus IDE would have worked for our purposes, too — and added vRAM to the cardgame contract. We also looked at staking to a DAPP Service Provider and deploying to Kylin testnet.

It’s time to add LiquidAccounts, a powerful option for virtual accounts that makes dApps work in any browser and eliminates the need for users to run wallet applications or manage keys. LiquidAccounts can still give users control of their own keys by generating each key from a username/password combo, which we’ll do below.

First, make sure your Zeus SDK is up to date — or install it, if needed.

Node Version Manager is the recommended setup, as we used in walkthrough #1. Or, you can simply use Node.js 10.x.

$ npm install -g @liquidapps/zeus-cmd

(As usual, don’t enter the $. It just represents that this is a terminal command.)

You can, alternatively, use Zeus IDE for your development, regardless of your system specs or environment.

If you don’t have an “in progress” version of the cardgame app from the previous walkthrough, grab it from this repo:

This is the standard Elemental Battles card game from’s online tutorial, but modified to use vRAM, as we did in walkthrough #1. Note that the frontend has a few modifications, as well, but this doesn’t interfere with this walkthrough.

We’re going to do the following in this walkthrough:

  1. Add the necessary preprocessor directives for LiquidAccounts.
  2. Convert our actions’ multiple arguments to single payload structs.
  3. Convert our require_auth statements to require_vaccount.
  4. Add the VACCOUNTS_APPLY macro.
  5. Add the necessary init, regaccount, xdcommit, and xvinit functions to the dispatcher.
  6. Delay the eviction of user data to vRAM to improve performance.
  7. Deploy to testnet and initialize the chain ID.

1. Add DAPP Network preprocessor directives

The vRAM-enabled cardgame contract’s cardgame.hpp file begins with the following preprocessor section:

#include “../dappservices/multi_index.hpp”#define DAPPSERVICES_ACTIONS() \
#define CONTRACT_NAME() cardgame

We need to add a few elements, marked in bold, so that this section now reads:

#include “../dappservices/vaccounts.hpp”
#include “../dappservices/ipfs.hpp”
#include “../dappservices/multi_index.hpp”
#define CONTRACT_NAME() cardgame

As usual with any DAPP-enabled contract, the start of our class definition, class cardgame : public eosio::contract {, will be replaced with CONTRACT_START()

2. Convert our actions’ multiple arguments to single payload structs

Normally, an EOSIO ACTION can take multiple parameters, any combination of names, strings, vectors, booleans, integers, etc. But in order to use LiquidAccounts — called vaccounts in the code — we’ll need to have users pass a single struct (an object, on the JavaScript side) to our contract’s actions. That struct can contain all of the parameters inside of it, and it will also always contain the user’s LiquidAccount (vaccount) name.

For example, the login action in cardgame originally only takes name username as its single parameter.

2a. Updating login function declaration

We need to change the function definition in cardgame.cpp:

void cardgame::login(player_struct payload) {

and, of course, its matching declaration in cardgame.hpp:

void cardgame::login(player_struct payload);

2b. Updating the function body to use payload

We also need to update each mention of username in the function definition’s body to payload.username. Or, we can avoid needing to change multiple lines by adding a new line at the very beginning of our login function:

name username = payload.username;

2c. Defining the payload struct itself

Finally, we need to actually define the player_struct struct, the type of our payload here, in the public section of our cardgame.hpp file’s class declaration:

struct player_struct {
name username; // the original parameter
EOSLIB_SERIALIZE( player_struct, (username) )

2d. Updating the other actions (startgame, endgame, nextround)

Repeat steps 2a and 2b for the startgame, endgame, and nextround actions, which also previously only took name username and now require the same updates.

Modify the function declarations and definitions in cardgame.hpp and cardgame.cpp, respectively, to take player_struct payload instead of name username, and then be sure to add the name username = payload.username; right up top.

void cardgame::startgame(player_struct payload) {
name username = payload.username;
// same for endgame and nextround

2e. Updating the playcard action with play_struct

Our playcard action is a little different than the others — it takes both a username and a card index number specifying which card in the player’s hand he wants to play, so it requires a struct of its own, which we’ll call play_struct rather than player_struct:

struct play_struct {
name username;
uint8_t player_card_idx;
EOSLIB_SERIALIZE( player_struct, (username)(player_card_idx) )

And, of course, playcard should get the same treatment as the other functions received in 2a and 2b, to make the function work with its new play_struct payload argument.

void cardgame::playcard(play_struct payload) {
name username = payload.username;
uint8_t player_card_idx = payload.player_card_idx;

Not much left to do!

3. Convert our require_auth statements to require_vaccount

LiquidAccounts actions don’t use the usual EOSIO require_auth to get the user’s authorization. Change require_auth(username) to require_vaccount(username) wherever it occurs.

4. Add the VACCOUNTS_APPLY macro

We need to add a new macro just before the CONTRACT_END() macro that replaces our class definition’s closing brace in cardgame.hpp and specifies which payload struct each action takes:


5. Add the necessary regaccount, xdcommit, and xvinit functions to the dispatcher

As you may remember from vRAM, in place of

};EOSIO_DISPATCH(contractname here, (actions here))

we have a CONTRACT_END statement:


We’ll need to add a few DAPP Network actions to this, namely xdcommit, regaccount (which we’ll use each time we create a new LiquidAccounts user), and xvinit (which we’ll call once to specify the chain ID for our app before we start making any virtual accounts).


6. Delay the eviction of user data to vRAM to improve performance

At the top of cardgame.hpp, simply declare a number of seconds you want to keep your user session in RAM for maximum performance. If the user is inactive for this number of seconds, their data will be evicted to vRAM, freeing up resources for your contract:


This is an optional step, but for certain applications, it makes sense. The time may vary considerably based on your expectations for user behavior.

We also need to apply this to our _users table in our contract constructor. It currently reads:

cardgame( name receiver, name code, datastream<const char*> ds ):contract(receiver, code, ds),
_users(receiver, receiver.value),
_seed(receiver, receiver.value) {}

Let’s update the line with _users to this:

_users(receiver, receiver.value, 1024, 64, false, false, VACCOUNTS_DELAYED_CLEANUP),

You can see some further vRAM customization options here that we haven’t noted before.

1024 is the number of shards you’d like for your dApp, and 64 is the number of buckets per shard. These values are more than enough for our purposes and for reasonable usage numbers.

The false, false options are pin_shards and pin_buckets. We don’t want to enable these, as pinning the shards prevents the data from being committed from our contract’s ipfsentry table. Our cleanup_delay already does this, but only temporarily, which is exactly what we want.

If you prefer to declare your tables in each function that uses them rather than using the constructor and declaring users_table _users in the class definition, you can pass the same arguments:

users_table _users (get_self(), get_self().value, 1024, 64, false, false, VACCOUNTS_DELAYED_CLEANUP);

7. Deploy the contract and initialize the chain ID

Our contract is ready to compile and deploy! Once you’ve compiled with no errors and successfully deployed to a local network or to Kylin testnet (where you’ll need to stake to a DSP offering LiquidAccounts), you’ll need to initialize the chain ID with the xvinit action.

$ export CHAIN_ID=5fff1dae8dc8e2fc4d5b23b2c7665c97f9e9d8edf2b6485a86ba311c25639191
$ cleos -u push action $KYLIN_TEST_ACCOUNT xvinit '[\"$CHAIN_ID\"]' -p $KYLIN_TEST_ACCOUNT

Excellent! Our contract is ready to use LiquidAccounts!

In part 2, we’ll work with our new LiquidAccounts from our frontend.

If you don’t want to wait, you can use the dapp client now to push a regaccount action to create a LiquidAccount on your contract and then (using the same method) test your contract’s actions with their parameters formed as the appropriate payload structs.

In order to see how the frontend is implemented before the next walkthrough, you can:

  • take a look at the final frontend code for cardgame on Github, or
  • run $ zeus unbox cardgame from the terminal. You can investigate the frontends/main directory in the cardgame directory that is created. To deploy and play with the frontend after unboxing, run:
$ cd cardgame
$ zeus migrate
$ zeus deploy frontend main

See you next time, and enjoy LiquidAccounts!

Follow LiquidApps

Website | Twitter | Telegram | LinkedIn | Github

Please click here to read an important disclaimer.



DAPP Network
The DAPP Network Blog

DAPP Network aims to optimize development on the blockchain by equipping developers with a range of products for building and scaling dApps.