RIDE for dApps hits Waves TestNet!

Waves | 03.18| 415

The upgrade to Waves’ native blockchain language is a key step along the way to full dApp implementation and Web 3.0 development.

Earlier in January we released our roadmap. We have now reached the first milestone on this, and the release of Node update 0.17 brings RIDE for dApps to TestNet!

As we’ve discussed at length before, blockchain really isn’t designed for carrying out complicated computations. Blockchain architecture simply isn’t suited to this. After seeing the vulnerabilities and edge cases that have arisen on Ethereum, Waves has always maintained that Turing-completeness should not be essential for on-chain blockchain computations. The RIDE language itself is deliberately non-Turing complete for this reason. However, Turing complete computations can still be achieved by spreading operations over consecutive blocks, if such functionality is required. RIDE therefore offers a flexible but safe solution for on-blockchain computation.

RIDE for dApps is being introduced to grant an account a way of assigning a programmable function to itself, with callable functions that are able to:

  1. Receive payments
  2. Change the account’s state
  3. Send WAVES and tokens from the account

To initiate the call, we have added a new command: InvokeScriptTransaction. This needs to be put on the blockchain to call a function, and the sender pays fees to the miner for the invocation to be executed. The sender can optionally attach payment in WAVES or tokens, and upon invocation the contract state can be changed and the contract can make multiple payments.

The existing mechanics for authorisation scripts will be maintained under the @Verifier function of the account. This can be thought of as an ‘admin’ function for the contract’s owner or owners. By default, the contract, contract data and contract tokens are all controlled by the private key for the account. Multisig control is possible. If @Verifier is always false, then the contract is sealed — immutable.

We will now give an overview of some of the use cases that this new functionality makes possible. As we know, these functions can generate transfers (transfer of funds from the contract address) and change a dApp’s state (via DataTransactions from the contract account). Moreover, RIDE for dApps makes implementation of certain use cases far easier and more convenient.

Dividing funds into two equal parts

Suppose we need to implement a mechanism to split the funds in an account equally between two specific addresses.

Consider an implementation using a smart account: the script allows the account to send only MassTransfer transactions that meet the specified conditions:

  1. There are two recipients: Alice and Bob
  2. Fee = 0.006 WAVES
  3. Each of them is sent exactly ((balance-fee) / 2) WAVES

The account can only send a transaction that meets the conditions described above. Other transactions will not be accepted on the blockchain.

# Predefined addresses of recipients
let Alice = base58'3NBVqYXrapgJP9atQccdBPAgJPwHDKkh6A8
let Bob = base58'3N78bNBYhT6pt6nugc6ay1uW1nLEfnRWkJd'
match(tx) {
# only MassTransferTransactions are allowed
case tx : MassTransferTransaction =>
# check if the transaction has exactly two predefined recipients
tx.transferCount == 2
&& tx.transfers[0].recipient == Alice
&& tx.transfers[1].recipient == Bob
# check if WAVES are distributed evenly
&& tx.assetId == unit
&& tx.transfers[0].amount == (wavesBalance(tx.sender) - fee) / 2
&& tx.transfers[0].amount == tx.transfers[1].amount
# check if the fee is equal to 0.006 WAVES
&& tx.fee == 600000
# the other types of transactions are prohibited
case _ => false

In RIDE 4 dApps we do this differently: you can divide the funds between two given addresses by calling the ‘split’ function described in the script. This function divides all the funds of the account-contract in half, sending them to two addresses — Alice and Bob. You can call this function by sending InvokeScriptTransaction. In this case, the fee is paid by the one who sends the transaction, so you do not need to check the fee in the script code.

# predefined addresses of recipients
let Alice = base58'3NBVqYXrapgJP9atQccdBPAgJPwHDKkh6A8'
let Bob = base58'3N78bNBYhT6pt6nugc6ay1uW1nLEfnRWkJd'
func split() = {
# calculate the amount of WAVES that will be transferred to Alice and Bob
let transferAmount = wavesBalance(this) / 2
# the result of a contract invocation contains two transfers (to Alice and to Bob)
ContractTransfer(Alice, transferAmount, unit),
ContractTransfer(Bob, transferAmount, unit)

Register of addresses that have paid for a service

Imagine that we want to create a service that allows you to register payment for services by users (for example, a monthly subscription). Subscribers each pay 10 WAVES, with the time of the last payment being entered into the register.

Consider the implementation using a smart account. The account script only allows DataTransactions that meet the following conditions:

  1. 0.005 WAVES fee
  2. Two entries
  • <transfer_id> : “used”
  • <payment_sender_address> : <current_height>

3. In the proofs array, the first element is the transfer transaction id (payment for services), which includes:

  • The recipient address for the contract
  • Amount >= 10 WAVES + 0.005 WAVES (to pay for DataTx fee)
  • Id was not previously used as a key (to prevent double-spending)
# fixed payment and fee values
let dataTxFee = 500000
let paymentAmount = 1000000000
match (tx) {
# only DataTransactions are allowed
case  t: DataTransaction =>
# extract the payment transaction's id from the DataTransaction
let paymentTxId = toBase58String(t.proofs[0])
# extract the provided payment transaction from the blockchain
let paymentTx = transactionById(fromBase58String(paymentTxId))
match (paymentTx) {
# the provided payment transaction should be a TransferTransaction
case paymentTx : TransferTransaction =>
# check if the payment transaction was not used before (to prevent double-spending)
&& !isDefined(getString(this, paymentTxId))
# check if the payment recipient is this account
&& paymentTx.recipient == this
# check if the transfer amount exceeds the required minimum
&& paymentTx.amount >= paymentAmount + dataTxFee
# check if the transferred asset is WAVES
&& paymentTx.assetId == unit
# check if the data satisfies the specified format
&& size( == 2
&& getString(, paymentTxId) == "used"
&& getInteger(, toBase58String(tx.sender.bytes)) == height
# check if the fee is correct
&& t.fee == dataTxFee
case _ => false
# the other types of transactions are prohibited
case _ => false

The main disadvantage of this approach is the complex mechanics: you need to send two transactions, one of which refers to the other, as well as send transactions on behalf of other account (specify the sender address of the contract).

Using RIDE for dApps everything is much easier. The payment can be attached to InvokeScriptTransaction. We need only check in the script that the payment was attached. If the address calling the script pays at least 10 WAVES, it is entered into the registry.

# fixed payment amount
let paymentAmount = 1000000000
func pay() = {
# check if the attached payment is at least 10 WAVES
let payment = extract(i.payment)
if (payment.asset != unit || payment.amount < paymentAmount)
then throw("You have to provide a payment of at least 10 WAVES")
# the result of a contract invocation contains one dataEntry(<caller_address>, <current_height>)
else WriteSet(List(DataEntry(toBase58String(i.caller.bytes), height)))


In this example, a non-anonymous vote is implemented based on known voting lists and candidates. An account ballot is prepared in advance (sent to the list via DataTransaction), including:

  1. The list of voters (for each voter create an entry { <voter_address_voted>, false })
  2. The list of candidates (for each candidate create an entry { <candidate_address>, 0 }).

To implement using a smart account, the account script allows only DataTransactions:

  1. Submitted by voters who have not already voted. The voter’s public key goes in proofs[1] and signs this DataTransactions (the signature itself goes in the proofs[0]).
  2. The DataTx contains exactly two entries:
  • <candidate> : <current_value> + 1
  • <voter_address_voted> : true
match(tx) {
# only DataTransactions are allowed
case tx : DataTransaction =>
# extract the chosen candidate from the transaction
let candidate =[0].key
# check if the transaction was signed by the voter
let voterPublicKey = tx.proofs[1]
let voterKey = toBase58String(addressFromPublicKey(voterPublicKey)) + "_voted"
sigVerify(tx.bodyBytes, tx.proofs[0], voterPublicKey)
# check if the voter has not voted yet
&& extract(getBoolean(this, voterKey)) == false
# check if the data satisfies the specified format
&& size( == 2
&& extract(getInteger(, candidate)) == extract(getInteger(this, candidate)) + 1
&& extract(getBoolean(, voterKey)) == true
case _ => false

In RIDE for dApps, in the contract script we describe the vote function, which can be called to vote for a certain candidate.

func vote(candidate : String) = {
let voterKey = toBase58String(i.caller.bytes) + "_voted"
# check if the caller is registered as a voter
if (!isDefined(getBoolean(this, voterKey))) then throw("You are not registered as a voter")
# check if the voter has not voted yet
else if (extract(getBoolean(this, voterKey)) == true) then throw("You have already voted")
# check if the candidate is in the voting list
else match (getInteger(this, candidate)) {
case r : Integer =>
WriteSet(List(DataEntry(candidate, r + 1),
DataEntry(voterKey, true)))
case _ => throw ("Candidate is not in the voting list")


In this example, a dApp wallet is implemented: you can send a WAVES payment, and it will save it in the wallet (deposit function), and you can take deposited WAVES back out of the wallet (withdraw function).

func getBalance(address: Address) : Int = {
match getInteger(this, toBase58String(address.bytes)) {
case a: Int => a
case _ => 0
func deposit() = {
let caller = toBase58String(i.caller.bytes)
let currentBalance = getBalance(caller)
let payment = match(i.payment) {
case p : AttachedPayment => p
case _ => throw("You have to provide a payment to deposit")
if (payment.asset != unit) then throw("This wallet cannot hold assets other than WAVES")
else {
let newBalance = currentBalance + payment.amount
WriteSet(List(DataEntry(caller, newBalance)))
func withdraw(amount: Int) = {
let caller = toBase58String(i.caller.bytes)
let currentBalance = getBalance(caller)
if (amount < 0) then throw("Can't withdraw negative amount")
else if (amount > currentBalance) then throw("Insufficient balance")
else {
let newBalance = currentBalance - amount
WriteSet(List(DataEntry(currentKey, newBalance))),
TransferSet(List(ContractTransfer(i.caller, amount, unit)))


This example shows implementation for a dApp-exchanger, which buys/sells WAVES and USD at a fixed price. It describes the functions ‘buyWAVESforUSD’ and ‘sellWAVESforUSD’. Users need to attach payment, and the dApp sends a transfer in response.

let WAVES = unit                                                    # amount asset
let USD = base58'Ft8X1v1LTa1ABafufpaCWyVj8KkaxUWE6xBhW6sNFJck'      # price asset
let buyPrice = 301
let sellPrice = 299
let scale = 100000000
func buyWAVESforUSD() = {
let payment = extract(i.payment)
if (payment.asset != USD) then throw("To buy WAVES for USD you have to provide USD")
else {
let amount = payment.amount * scale / buyPrice
TransferSet(List(ContractTransfer(i.caller, amount, USD)))
func sellWAVESforUSD() = {
let payment = extract(i.payment)
if (payment.asset != WAVES) then throw("To sell WAVES for USD you have to provide WAVES")
else {
let amount = payment.amount / (scale / sellPrice)
TransferSet(List(ContractTransfer(i.caller, amount, WAVES)))

This is by no means a complete list of possible use cases. Check out RIDE for dApps on TestNet and let us know your thoughts and your own use cases!

Join Waves Community
Read Waves News channel
Follow Waves Twitter
Subscribe to Waves Subreddit

RIDE for dApps hits Waves TestNet! was originally published in Waves Platform on Medium, where people are continuing the conversation by highlighting and responding to this story.

Comment 0


Are you sure you want to delete this post?