Forward values allow advanced interactivity between smart contracts, enabling use-cases that would not normally be possible.
Before we dive into the specifics, let’s look at an example of how forward values are used:
interface TokenPayableContract {
readonly approveReceiveTransfer: (from: Address, value: Fixed<8>, ...args: ForwardValue[]) => boolean;
}
export class Token extends SmartContract {
// Note that the implementation here is only to show how we
// can use forward values and is an incomplete implementation of a
// Token transfer method.
public transfer(from: Address, to: Address, value: Fixed<8>, ...args: ForwardValue[]): boolean {
const contract = Contract.for(to);
if (contract !== undefined) {
const payableContract = SmartContract.for<TokenPayableContract>(to);
return payableContract.approveReceiveTransfer(from, value, ...args);
}
return true;
}
}
Note
We’re using a rest parameter as the final parameter of the
transfer
method. Declaring a rest parameter means that the function will accept 0 or more additional arguments of that type.
ForwardValue
s represent any type. They’re opaque to the contract that declared them. Instead of using them directly, the contract forwards them to another contract. In the example above we check to see if the target or to
Address
is a smart contract. If it is, we get an instance of it and invoke the approveReceiveTransfer
method, forwarding any additional arguments that we received in the call to transfer
.
This pattern allows the target contract a chance to react to the transfer, as well as allows the user to provide any additional arguments the contract may require to react to the transfer.
The counterpart to ForwardValue
is the tagged type ForwardedValue<T>
. ForwardedValue<T>
tags the type T
such that the NEO•ONE toolchain will generate client APIs that simplify forwarding values. Given the following smart contract:
export class Escrow extends SmartContract {
public approveReceiveTransfer(from: Address, value: Fixed<8>, to: ForwardedValue<Address>): boolean {
// Update the escrow account for [from, to] with value
return true;
}
}
The NEO•ONE toolchain will generate a method called forwardApproveReceiveTransferArgs
:
const receipt = await token.transfer.confirmed(
from,
escrow.definition.networks[networkName].address,
value,
...escrow.forwardApproveReceiveTransferArgs(to),
);
The forwardApproveReceiveTransferArgs
call above not only sets up the call to forward the specified arguments, but it additionally adds the Escrow
contracts events (if any) to the resulting receipt
.
Forwarding values also works recursively. For example, if the Escrow
contract also specified a rest parameter of ForwardValue
s and called another smart contract Foo
's bar
method that expected a ForwardedValue<string>
, the client API invocation would look like:
const receipt = await token.transfer.confirmed(
from,
escrow.definition.networks[networkName].address,
value,
...escrow.forwardApproveReceiveTransferArgs(to, ...foo.forwardBarArgs('value')),
);
In this case, the receipt
would contain the events for the transfer
call, the approveReceiveTransfer
call and the bar
call.
Reactive smart contracts are powerful and enable many use-cases that would otherwise not be possible. We recommend the following pattern when implementing your smart contracts.
Whenever your smart contract has a method that takes an action
on an Address
, always check to see if the target Address
is a smart contract. If it is, invoke the approveReceive<action>
method of that smart contract with the same arguments action
was called with, except the argument that is the smart contract Address
itself. Additionally, pass a rest parameter of ForwardValue
s to the method. Take a look at the Token
example above to see this pattern in action.