How to Build, Deploy and Test a Waves RIDE dApp

Ilya Smagin
Waves Protocol
Published in
8 min readJul 11, 2019

--

In this article I will show how to write simple decentralised application(dApp) and run it on Waves node. We’ll take a look at the required development tools and good practices as well as a working example.

Development of a dApp is not(and should not be) radically different from development of a regular application: writing the code, writing automated tests, launching a dApp and testing it manually are the necessary steps.

1. Tooling

  1. docker for running a node and Waves Explorer
    Technically, this step is not necessary. You can use existing testnet / experimental-network, but unless you have your own node, your testing cycle will be dramatically slower because:
    - You might(most likely you will) constantly need new accounts with test tokens. The testnet faucet can give you just 10 WAVES every~10 minutes.
    - Average block time on testnet is 1 minute, while on private node it’s just 10 seconds. This can be critical if your script requires several blocks confirmations.
    - Public testnet nodes can have agressive caching turned on
    - Public testnet nodes may be under maintenance

For the sake of simplicity, in this example we’ll assume you’re working with a private node.

2. surfboard command line tool:`nodejs`

  • Download and install Node.js (via ppa, homebrew or exe) : https://nodejs.org/en/download/
  • Install surfboard— a tool that will allow you to run your tests on existing node:
npm install -g @waves/surfboard

3. Visual Studio Code plugin waves-ride :

This step is not neccessary. If you are not a fan of IDEs and prefer plaintext editors — it’s not a problem, all the must-have tools are purely command-line utils. If you usevim — please checkout vim-ride plugin.

4. Waves Keeper browser extension:

https://wavesplatform.com/products-keeper

All set!

2. Run a private node & explorer

  1. Run a node:
docker run -d -p 6869:6869 wavesplatform/waves-private-node

Observe the node is running through REST API at http://localhost:6869:

swagger REST API interface for the node

2. Run an instance of Waves Explorer:

docker run -d -e API_NODE_URL=http://localhost:6869 -e NODE_LIST=http://localhost:6869 -p 3000:8080 wavesplatform/explorer

Open your browser and navigate to http://localhost:3000. You will see an empty chain of you local node rapidly being built.

Local Waves Explorer directed to the local node instance

It’s almost a time to put some transactions in there!

3. RIDE project structure and `surfboard` tool

Create an empty dir and type

surfboard init

This command will initialise a directory with a project structure and a sort-of-a-hello-world application and tests.

If you open that folder with VS Code, here’s what you’ll find:

surfboard.config.json
  • Under ./ride/ folder, you can find a single wallet.ride file. This is the directory where your dApp code is located. We will briefly analyze the dApp in the next section.
  • Under ./test/ folder you can find a *.js file. This where the tests are located.
  • ./surfboard.config.json is the configuration file for running tests.

The important section is envs . Each environment is configured by

  • API_BASE: The REST API endpoint of the node that will be used for running a dApp as well as theCHAIN_ID of the network.
  • SEED: The seed phrase for account with tokens, which will be the source of all the WAVES tokens in your test.

As you can see, the default surfboard.config.json supports multiple environments with local being default one(defaultEnv key is the setting to change).

4. Wallet-demo application

This section is not a guide to the RIDE language — it’s a glance at the app we’re deploying and testing so we can better understand what’s happening on the blockchain.

We’ll take a closer look at simple application called wallet-demo. Anyone can deposit as many Waves tokens to the dApp as he wants, but you can only withdraw your own waves. Two @Callable functions are available for invocation via InvokeScriptTransaction:

  • deposit() , which requires attached payment in WAVES
  • withdraw(amount: Int) , which transfers tokens back to the caller upon successful execution.

Throughout the dApp lifecycle, the the structure of
mapping(address →amount) will be maintained:

+=========================+==============================+
| Action | Resulting state |
+=========================+==============================+
| <initial> | <empty> |
+-------------------------+------------------------------+
| Alice deposits 5 Waves | <alice-address> → 500000000 |
+-------------------------+------------------------------+
| Bob deposits 2 Waves | <alice-address> → 500000000 |
| | <bob-address> → 200000000 |
+-------------------------+------------------------------+
| Bob withdraws 7 Waves | <DENIED!> |
+-------------------------+------------------------------+
| Alice withdraws 4 Waves | <alice-address> → 100000000 |
| | <bob-address> → 200000000 |
+-------------------------+------------------------------+

To get a full picture, here’s the code:

# In this example multiple accounts can deposit their funds and safely take them back, no one can interfere with this.# An inner state is maintained as mapping `address=>waves`.{-# STDLIB_VERSION 3 #-}
{-# CONTENT_TYPE DAPP #-}
{-# SCRIPT_TYPE ACCOUNT #-}
@Callable(i)
func deposit() = {
let pmt = extract(i.payment)
if (isDefined(pmt.assetId))
then throw("works with waves only")
else {
let currentKey = toBase58String(i.caller.bytes)
let currentAmount = match getInteger(this, currentKey) {
case a:Int => a
case _ => 0
}
let newAmount = currentAmount + pmt.amount
WriteSet([DataEntry(currentKey, newAmount)])
}
}
@Callable(i)
func withdraw(amount: Int) = {
let currentKey = toBase58String(i.caller.bytes)
let currentAmount = match getInteger(this, currentKey) {
case a:Int => a
case _ => 0
}
let newAmount = currentAmount - amount
if (amount < 0)
then throw("Can't withdraw negative amount")
else if (newAmount < 0)
then throw("Not enough balance")
else ScriptResult(
WriteSet([DataEntry(currentKey, newAmount)]),
TransferSet([ScriptTransfer(i.caller, amount, unit)])
)
}
@Verifier(tx)
func verify() = false

A reference code example can also be found on github.

The VSCode plugin will carry out continuous compilation as you edit the file, so you can always follow the errors in thePROBLEMS tab:

To explicitly compile a file if you want to use any other text editor, use

surfboard compile ride/wallet.ride

It will output a base64 string of compiled RIDE code.

5. Test scenario for `wallet.ride`

Let’s briefly look at the test file. Powered by javascript’s Mocha framework, it has a before function and 3 tests:

  • The before function funds a few accounts via MassTransferTransaction , compiles the script and deploys the compiled script on blockchain.
  • Can deposit signs and broadcasts InvokeScriptTransaction invoking deposit() function, for each of 2 accounts, foo.. and bar... .
  • Can't withdraw more than was deposited so that no-one can “steal” another account’s tokens
  • Can withdraw ensures that correct withdrawals are being processed successfully.

6. Running the tests with `surfboard`, analyzing the results in Waves Explorer

To run the test, execute

surfboard test

By the way, if you have several scenarios (for instance, you need a separate deployment script), you can run a particular scenario by running

surfboard test my-scenario.js

Surfboard will pick up all the tests files in ./test/ folder and run the scenario over a node through the endpoint configured in surfboard.config.json . After several seconds, you will see something like this:

wallet test suite
Generating accounts with nonce: ce8d86ee
Account generated: foofoofoofoofoofoofoofoofoofoofoo#ce8d86ee - 3M763WgwDhmry95XzafZedf7WoBf5ixMwhX
Account generated: barbarbarbarbarbarbarbarbarbar#ce8d86ee - 3MAi9KhwnaAk5HSHmYPjLRdpCAnsSFpoY2v
Account generated: wallet#ce8d86ee - 3M5r6XYMZPUsRhxbwYf1ypaTB6MNs2Yo1Gb
Accounts successfully funded
Script has been set
√ Can deposit (4385ms)
√ Cannot withdraw more than was deposited
√ Can withdraw (108ms)
3 passing (15s)

Hooray! All tests have passed. Now we can look more closely at what happened using WavesExplorer by just browsing blocks or pasting one of the addresses above to the search box: transaction history, dApp state, decompiled binary, and other things can be found there.

Waves Explorer, an app we just deployed

A few surfboard tips:

  1. To test against a different environment like testnet, just use
surfboard test --env=testnet

To get free testnet tokens, use https://wavesexplorer.com/testnet/faucet

2. If you want to explore what information is sent to and from node during the calls of boradcast(...) in tests, run a test with -v (stands for “verbose”) to make surfboard produce extensive logs:

surfboard test -v

7. Using a deployed app with WavesKeeper

  1. Set your WavesKeeper to work with http://localhost:6869:
Waves Keeper configuration to work with local node

2. Import a seed with tokens for the network. For simplicity, just use the staking seed of your node: waves private node seed with waves tokens . The address would be 3M4qwDomRabJKLZxuXhwfqLApQkU592nWxF .

3. You can either start an instance of a 1-page serverless web appliction yourself using npm, or just navigate to the existing one: https://chrome-ext.wvservices.com/dapp-wallet.html

4. Enter a wallet address from your test run(highlighted in the example above) to the dApp address text field.

5. Enter a small number in ‘Deposit’ field and press theDeposit button:

Waves Keeper asking your permission to sign an InvokeScriptTransaction with attached payment of 10 WAVES

6. Accept the transaction:

The transaction has been created and broadcasted to the network. Its ID is visible below

7. Observe the transaction using explorer by pasting its ID into the search field:

8. Conclusion, further steps and resources

In this article, we explored the tools for developing, testing, deploying and using a simple dApp on Waves Platform. This briefly covers a lot of tools: RIDE language, VS Code, Waves Explorer, surfboard and WavesKeeper. Now you are all set to learn more about RIDE and build your first dApp!

Here’s a few links to help:

TL;DR in a gist: https://bit.ly/2YCFnwY

--

--