LiquidApps Walkthrough #3: LiquidAccounts
No Hassle. No Wallets. No Keys.
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:
https://github.com/liquidapps-io/cardgame-vram
This is the standard Elemental Battles card game from Block.one’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:
- Add the necessary preprocessor directives for LiquidAccounts.
- Convert our actions’ multiple arguments to single
payload
structs. - Convert our
require_auth
statements torequire_vaccount
. - Add the
VACCOUNTS_APPLY
macro. - Add the necessary
init
,regaccount
,xdcommit
, andxvinit
functions to the dispatcher. - Delay the eviction of user data to vRAM to improve performance.
- 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() \
XSIGNAL_DAPPSERVICE_ACTION \
IPFS_DAPPSERVICE_ACTIONS#define DAPPSERVICE_ACTIONS_COMMANDS() \
IPFS_SVC_COMMANDS()#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 DAPPSERVICES_ACTIONS() \
XSIGNAL_DAPPSERVICE_ACTION \
IPFS_DAPPSERVICE_ACTIONS \
VACCOUNTS_DAPPSERVICE_ACTIONS#define DAPPSERVICE_ACTIONS_COMMANDS() \
IPFS_SVC_COMMANDS()VACCOUNTS_SVC_COMMANDS()#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:
VACCOUNTS_APPLY(((player_struct)(login))((player_struct)(startgame))((player_struct)(endgame))((player_struct)(nextround))((play_struct)(playcard)))
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:
CONTRACT_END((login)(startgame)(playcard)(nextround)(endgame))
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).
CONTRACT_END((login)(startgame)(playcard)(nextround)(endgame)(xdcommit)(regaccount)(xvinit))
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:
#define VACCOUNTS_DELAYED_CLEANUP 120
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 KYLIN_TEST_ACCOUNT=<YOUR_KYLIN_ACCOUNT_NAME>
$ export CHAIN_ID=5fff1dae8dc8e2fc4d5b23b2c7665c97f9e9d8edf2b6485a86ba311c25639191
$ cleos -u https://kylin-dsp-2.liquidapps.io 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!