Understanding how the Reach smart contract interacts with a web frontend
Introduction
For developers new to the Reach programming language, one of the most difficult hurdles to cross is understanding exactly how a Reach smart contract interacts with the frontend. This is mostly because the smart contract interacts with the frontend asynchronously.
By itself, the smart contract logic is relatively straightforward. The program can include loops with definite conditions for how many times they run. It can also include races where multiple participants race to submit data to the smart contract. It can include forks that act like the conventional Javascript switch
statement. Nothing out of the ordinary. However, when the smart contract logic meets frontend logic, it can be confusing. I'll try to unravel the knots surrounding this union.
The first thing to note is that different types of entities can be defined in the smart contract. How the frontend interacts with the smart contract depends on which entity type the frontend is representing. Some of these entities include:
Participant
ParticipantClass
API
View
Event
Participant
In my opinion, Participants are the easiest to implement. They interact with the smart contract through their interact object. The smart contract decides the structure of this object, specifying its interface. The frontend only needs to provide an object that follows the expected interface and then connect that object to the smart contract. The smart contract can read the provided object's values, as well as call the object's methods.
Through the object's values, data is passed from the frontend to the smart contract. Through the object's methods, data goes back and forth between them depending on the method's return values. To visualize this let's look at an example.
Say there's a participant Alice
who provides an amount wager
to the smart contract, and can perform two actions getHand
and seeOutcome
.
const Alice = Participant('Alice', {
wager: UInt,
getHand: Fun([], UInt),
seeOutcome: Fun([UInt], Null)
})
wager
is a number and must have a value when the object gets initialized on the frontend.getHand
is a function with no arguments. It returns a number.seeOutcome
is a function with one argument, a number. It doesn't return anything.
On the frontend, this is how the object is initialized:
...
//define interact object
const Alice = {
wager: 7, //because prime numbers rock
getHand: () => {
//logic for computing return value
//value gets sent to smart contract
//value can be computed asynchronously or synchronously
let hand = 5;
return hand
},
seeOutcome: (numberFromSmartContract) => {
console.log(numberFromSmartContract)
}
}
//connect to smart contract
const contract = account.contract(backend);
backend.Alice(contract, Alice);
On the smart contract, the values and methods on the frontend object get called here:
Alice.only(() => {
const wager = declassify(interact.wager);
const hand = declassify(interact.getHand());
interact.seeOutcome(100);
});
Alice.publish(wager, hand);
When the flow of the program on the smart contract gets to interact.wager
, the wager value (7) specified by the frontend gets stored in the wager
variable on the smart contract.
When interact.getHand()
gets called, the smart contract program flow pauses and waits for a return value from the frontend. Until that is provided, the smart contract pauses and waits. The smart contract behaves this way for every interact method with a return value.
When interact.seeOutcome(100)
gets called, the value 100
gets passed to the frontend as an argument to the seeOutcome
method of the Alice
object. This step isn't asynchronous.
ParticipantClass
The ParticipantClass
interacts with the frontend the same way as the Participant
. But it behaves weirdly when the smart contract expects data from the ParticipantClass
interact function. The ParticipantClass
has been deprecated by Reach and should be avoided entirely.
API
APIs are functions you can call on a smart contract. Unlike with the Participant
, these functions aren't called by the smart contract, but instead by the frontend. The smart contract decides when the function can be called, but only during that window can the function be called. Calling the function outside that window would send an error message to the frontend.
The error message looks something like this; Error: Expected the DApp to be in state(s) [2], but it was actually in state 1.
Like with the Participant
, the smart contract defines the interface for the API functions. On the smart contract, this is how the API class is defined:
const UserActions = API('UserActions', {
checkValue: Fun([], UInt),
incrementValue: Fun([UInt], Null)
})
Here the API class has two functions that can be called on the frontend. The checkValue
function has no arguments. This means no data gets to the smart contract from the frontend. This is because the function gets called on the frontend, and the caller of a function is responsible for providing its arguments. It has a return value which means data gets sent from the smart contract to the frontend. This will make more sense to you when you see a code snippet of the function call on the frontend.
incrementValue
has a function argument. This means data gets sent to the smart contract from the frontend. However, it doesn't have a return value, meaning no data gets sent back to the frontend from the smart contract.
There are two ways to initiate an API function in a smart contract: either as part of a fork (the equivalent of a Javascript switch
statement) or by itself. Here's an example of initiating the checkValue
API function by itself.
...
//The program pauses here until the checkValue function is called on
//the frontend
const [_, resolve] =
call(UserActions.checkValue);
resolve(10);
commit();
...
_
indicates that the function has no arguments.resolve
is how data gets sent back to the frontend. In this example, the value10
gets sent back to the frontend.10
must be of the same data type defined in thecheckValue
function's interface.
For the incrementValue
API function:
...
//The program pauses here until the incrementValue function is called on the frontend
const [newValue, resolve] =
call(UserActions.incrementValue);
resolve();
commit();
//newValue is available for the rest of the program
...
newValue
is the argument passed to the function when called on the frontend. It becomes available for computations on the smart contract.
On the frontend, this is how to call the checkValue
function:
...
//should be within an async function scope
const contract = account.contract(backend, contractInfo);
const returnedValue = await contract.apis.UserActions.checkValue();
console.log(returnedValue);
...
The return value gets stored in
returnedValue
.Wrap the function call in a
try-catch
to handle potential errors due to incorrect timing or invalid arguments.
For the incrementValue
function:
...
//should be within an async function scope
await contract.apis.UserActions.incrementValue(14);
...
incrementValue
doesn't have any return values and hence doesn't need to be stored to a variable.
Ensuring the API functions get called by the frontend at proper sync with the smart contract can be challenging. The best way to go about this is to keep track of the program flow on the smart contract, and only make those functions available when viable.
For example, the incrementValue
function can only be called after the checkValue
function gets called.
View
Like the API, Views are functions that can be called on the smart contract from the frontend. However, Views behave differently from how the API class does. After a View function has been defined, it can be called on the frontend at any point in time as long as the smart contract is still on.
Through its return value, data gets passed from the smart contract to the frontend. However, this value gets wrapped in a Maybe
type (because this View may not be initialized).
The Maybe type
Some
, data] or [None
, null]. [some
, data] is for when the data exists, while [None
, null] gets returned when the data doesn't exist.Lets define a simple View function that returns the square of any number passed to it.
const Square = View({
getSquare: Fun([UInt], UInt),
//accepts a number from frontend and returns a number to frontend
});
...
//During consensus step
Square.getSquare.set((m) => m * m);
...
Now the View
function can be accessed on the frontend of the program, even if the contract has moved past the line of code where the function was implemented.
On the frontend:
...
/* wrap scope in async function */
const contract = account.contract(backend);
const square = await contract.v.getSquare(4); //16
console.log(`The square of 4 is ${square}`);
...
Calling getSquare
here triggers it's smart contract counterpart, no async nightmares to worry about.
Events
Events are quite interesting in how they work. They allow data to flow only from the smart contract to the frontend. With Events, the frontend can keep track of the program flow on the smart contract, kind of like the Javscript console.log()
.
After an Event is created on the smart contract, the frontend can subscribe to it and get notified everytime that Event gets fired on the smart contract.
Suppose there is an Event named fullCycle
that gets triggered every time a while loop completes a cycle. It would be written like this on the smart contract:
...
const Notify = Events({
fullCycle: [UInt] //data flows in only one direction
});
...
var [x] = [0];
invariant(balance() == 0);
while(x < 10){
...
//Must be in consensus step
Notify.fullCycle(x);
...
[x] = [ x + 1 ]
continue;
}
The iterating value x
gets passed to the frontend through the fullCycle
event. One really cool thing about Events
is that other data get passed to the frontend as well, not just the function argument. The timeStamp for that event, in network time
, gets sent as well.
On the frontend, this is how the Event gets subscribed to:
...
const contract = acc.contract(backend);
contract.e.fullCycle.monitor((evt) => {
const { when, what: [ iteration ] } = evt;
console.log(`${iteration} registered at ${when}`);
});
...
Unlike the API
and View
classes which are functions on the frontend, the Event
is an object with methods. Each method provides a different way to interact with the Event
on the smart contract. Our only concern for now is the monitor
method.
The monitor
method has an evt
argument. evt
is the object { when: Time, what: T }
, where T
is the data passed from the smart contract to the frontend through the event, and when
is the network time-stamp of the event. T
is passed as a tuple
, and has to be destructured as one.
Everytime the Event gets triggered on the smart contract, the monitor
method gets called. Programming logic can then be added to this method to perform actions like trigger a notification for users, or take them to a different webpage etc.
Conclusion
Playing around with each entity will improve your understanding of how to use them effectively and enable you to make better decisions about which entity to use in different situations. Each of them has particular scenarios where it best fits.
Subscribe to my newsletter
Read articles from Timothy Ogwulumba directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by