RIDE for dApps hits Waves TestNet!

Waves Tech
Waves Protocol
Published in
8 min readMar 18, 2019

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.

let Alice = Address(base58'3NBVqYXrapgJP9atQccdBPAgJPwHDKkh6A8')let Bob = Address(base58'3N78bNBYhT6pt6nugc6ay1uW1nLEfnRWkJd')let fee = 600000match(tx) {# only MassTransferTransactions are allowedcase tx : MassTransferTransaction =># check if the transaction has exactly two predefined recipientstx.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 == fee# the other types of transactions are prohibitedcase _ => 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.

To write your first dApp on testnet, use our developers tool Waves IDE:

{-# STDLIB_VERSION 3 #-}{-# CONTENT_TYPE DAPP #-}{-# SCRIPT_TYPE ACCOUNT #-}# predefined addresses of recipientslet Alice = Address(base58'3NBVqYXrapgJP9atQccdBPAgJPwHDKkh6A8')let Bob = Address(base58'3N78bNBYhT6pt6nugc6ay1uW1nLEfnRWkJd')@Callable(i) func splitBalance() = {# calculate the amount of WAVES that will be transferred to Alice and Boblet transferAmount = wavesBalance(this) / 2# the result of a contract invocation contains two transfers (to Alice and to Bob)
TransferSet([ScriptTransfer(Alice, transferAmount, unit),ScriptTransfer(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 valueslet dataTxFee = 500000let paymentAmount = 1000000000let this = tx.sendermatch (tx){# only DataTransactions are allowedcase  t: DataTransaction =># extract the payment transaction's id from the DataTransactionlet paymentTxId = toBase58String(t.proofs[0])# extract the provided payment transaction from the blockchainlet paymentTx = transactionById(fromBase58String(paymentTxId))match (paymentTx) {# the provided payment transaction should be a TransferTransactioncase paymentTx : TransferTransaction =># check if the payment transaction was not used before (to prevent double-spending)getString(this, paymentTxId) == unit# 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(t.data) == 2&& getString(t.data, paymentTxId) == "used"&& getInteger(t.data, toBase58String(tx.sender.bytes)) == height# check if the fee is correct&& t.fee == dataTxFeecase _ => false}# the other types of transactions are prohibitedcase _ => 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.

{-# STDLIB_VERSION 3 #-}{-# CONTENT_TYPE DAPP #-}{-# SCRIPT_TYPE ACCOUNT #-}# fixed payment amountlet paymentAmount = 1000000000@Callable(i) func pay() ={# check if the attached payment is at least 10 WAVESlet payment = extract(i.payment)if (payment.assetId != 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([DataEntry(toBase58String(i.caller.bytes), height)])}

Voting

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
let this = tx.sendermatch(tx){# only DataTransactions are allowedcase tx : DataTransaction =># extract the chosen candidate from the transactionlet candidate = tx.data[0].key# check if the transaction was signed by the voterlet voterPublicKey = tx.proofs[1]let voterKey = toBase58String(addressFromPublicKey(voterPublicKey).bytes) + "_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(tx.data) == 2&& extract(getInteger(tx.data, candidate)) == extract(getInteger(this, candidate)) + 1&& extract(getBoolean(tx.data, voterKey)) == truecase _ => false}

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

{-# STDLIB_VERSION 3 #-}{-# CONTENT_TYPE DAPP #-}{-# SCRIPT_TYPE ACCOUNT #-}@Callable(i) func vote(candidate : String) ={let voterKey = toBase58String(i.caller.bytes) + "_voted"# check if the caller is registered as a voterif (!isDefined(getBoolean(this, voterKey))) then throw("You are not registered as a voter")# check if the voter has not voted yetelse if (extract(getBoolean(this, voterKey)) == true) then throw("You have already voted")# check if the candidate is in the voting listelse match (getInteger(this, candidate)) {case r : Int =>WriteSet([DataEntry(candidate, r + 1),DataEntry(voterKey, true)])case _ => throw("Candidate is not in the voting list")     } }

Wallet

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).

{-# STDLIB_VERSION 3 #-}{-# CONTENT_TYPE DAPP #-}{-# SCRIPT_TYPE ACCOUNT #-}func getBalance(address: String) ={match getInteger(this, address){case a: Int => acase _ => 0} }@Callable(i) func deposit() ={let caller = toBase58String(i.caller.bytes)let currentBalance = getBalance(caller)let payment = match(i.payment){case p : AttachedPayment => pcase _ => throw("You have to provide a payment to deposit")}if (payment.assetId != unit) then throw("This wallet cannot hold assets other than WAVES")   else {let newBalance = currentBalance + payment.amountWriteSet([DataEntry(caller, newBalance)])} }@Callable(i) 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("Not enough balance")else {let newBalance = currentBalance - amountScriptResult(WriteSet([DataEntry(caller, newBalance)]),TransferSet([ScriptTransfer(i.caller, amount, unit)]))} }

Exchanger

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.

{-# STDLIB_VERSION 3 #-}{-# CONTENT_TYPE DAPP #-}{-# SCRIPT_TYPE ACCOUNT #-}let WAVES = unit                                                    # amount assetlet USD = base58'Ft8X1v1LTa1ABafufpaCWyVj8KkaxUWE6xBhW6sNFJck'   # price assetlet buyPrice = 301let sellPrice = 299let scale = 100000000@Callable(i) func buyWAVESforUSD() ={let payment = extract(i.payment)if (payment.assetId != USD) then throw("To buy WAVES for USD you have to provide USD")else {let amount = payment.amount * scale / buyPriceTransferSet([ScriptTransfer(i.caller, amount, USD)])} }@Callable(i) func sellWAVESforUSD() ={let payment = extract(i.payment)if (payment.assetId != WAVES) then throw("To sell WAVES for USD you have to provide WAVES")else{let amount = payment.amount / (scale / sellPrice)TransferSet([ScriptTransfer(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!

--

--

Waves Tech
Waves Protocol

Waves Tech is a powerful blockchain-agnostic ecosystem focused on inter-chain DeFi, the embodiment of technological freedom for blockchain-based finance.