DApp-dApp interop with Ride V5

Ilya Smagin
Waves Protocol
Published in
3 min readMay 18, 2021

The release of Waves Node 1.3 Jumeirah is focused on Ride update( → V5), providing more features for dApp developers, with cross-dApp interop as key feature. Here’s a quick walkthrough guide on how to use it.

Return Value

With Ride V5, all @Callable functions return type change to tuple of Actions applied and Return Value.

Before:

{-# STDLIB_VERSION 4 #-}@Callable(inv)
func foo() = {
[DataEntry("invoked", true)] # applies state changes
}

After:

{-# STDLIB_VERSION 5#-}@Callable(inv)
func foo() = {
([DataEntry("invoked", true)], 42) # 1. applies state changes
# 2. returns 42 to the caller
}

Return value can either be of a primitive (Int, String, Boolean, Bytevector, Unit (instance ())), or an aggregate ( List[] , Tuple (instances like (1,2),("hello", 42, true) ) type.

Simple example

Let’s say dAppA invokes dAppB deployed at 3Pm..2k by calling foo() function and processes its return value:

@Callable(inv)
func price(assetId: String) = {
let address = addressFromStringValue("3Pm..2k")
strict f = invoke(address, "foo", [], [])
let result = match f {
case i:Int => i + 1
case _ => throw("unexpected return value type")
([], result)
}

Here, we use the new invoke() function and pass the following arguments:

  • Address of the dapp invoked: Address
  • @Callable function name invoked: String
  • List of arguments for the function invoked : List[Any]
  • Payments attached to the invocation : List[AttachedPayment]

Then, we need to process the result. At compile stage we can’t know the result type of the invocation, so upon successful invocation we get a variable(f in this example) of type Any . Hence, we need to match it by type, and, if its type is what we expect, we can work with it (add 1 in this case), otherwise we fail the transaction.

Use `strict` when invoking dApps

The necessity to introduce new keyword comes from the laziness of ride language, and, in turn, the order “parts” of “script” are calculated. It has never been a problem so far since the computations were side-effectless(No write effects happened during the execution of a script).

Problem: Imagine we have a script that grabs a balance, invokes dapp, then grabs the balance again:

...# don't do this
let balanceBefore = wavesBalance(this)
let autotrade = invoke('swopfi-waves-usdn', 'swap', [10 waves])
let balanceAfter = wavesBalance(this)
if(balanceAfter < balanceBefore) then ... else ...

Since let is lazy, the computations happen not at the time of definition, but at the time of the first access to the variable. e.g. balanceAfter would be evaluated before balanceBefore , and no autotrade will be processed at all!

Solution: usestrict instead of let to ensure order of evaluation: strict variable is always evaluated before the next definition/expression.

strict is a macro which ensures variable of the block is materialised before the expression of the block:

strict a = ...     # 'strict' macro
a + b

is just

let a = ...
if(a!=a) then throw("fatal") else # inserted by macro
a + b

in the bytecode.

With s

...# the correct waystrict balanceBefore = wavesBalance(this)
strict autotrade = invoke('swopfi-waves-usdn', 'swap', 10 waves)
strict balanceAfter = wavesBalance(this)
if(balanceAfter < balanceBefore) then ... else ...

Now all the computations happen in the exact order of the definitions.

`invoke` vs `reentrantInvoke`

In short, reentrantInvoke works the same as invoke , but allows reentrancy: dAppA can call dAppB, which calls dAppA, only if dAppA invoked dAppB with reentrantInvoke function. Reentrancy is a dangerous piece of tech, misuse of which caused a lot of hacks around the space, so unless you exactly know what this is and why you need it, just ignore it and always use invoke. It doesn’t allow reentrant calls, so it makes your system safe from user-deployed code. More on that in the docs.

Exceptions

If an exception is raised during transaction execution, the whole transaction is failed and no data/transfers/other actions occur in state of blockchain.

Try out

The feature is activated at stagenet and testnet and will be available at mainnet soon after the release, when voting and activation is complete.

TL;DR: To invoke a dApp from Ride, use invoke() function with strict keyword and match return value to the type you expect.

--

--