In this tutorial we will build a simplified version of the Tanda Club DApp using the Reach framework. This will help us learn some of the basics or nessisary features of a Tanda Club DApp. Using Reachs high level features it will be easy to implement a Tanda Club DApp. Reach
s linear state allows us to track all users during each phase of the Dapp.
Requirements:
- Install Reach
- Install
Docker
andDocker Compose
. - Install
Make
. - Finally, you will
Node.js
to run the React frontend.
The agenda for this tutorial is to:
- Discuss the setup of the Reach program.
- Initial scaffolding, APIs and Participant interact interfaces.
- Front-end setup in Javascript.
- Implementing the core transaction loop.
- Implementing and testing the logic for all users.
- React frontend setup.
- React frontend implementation.
- Yeah, we're done!
- Let's go!
Here is a skeleton of the Reach program. Before that theres a few things to note whille writing your Reach program.
- Who is involved in the application.
- What info will they know as they start
- What info will they know as they progress in the application.
- What will lead to the termination of the program.
So lets answer these questions for our DApp.
- Who is involved in the application.
1
pool creator and N contributors.
- What info will they know as they start.
- Creator has the pool and know the contribution amount for each cycle duration.
- Contributors knows the contribution amount and the dycle duration.
- What info will they know as they progress in the application.
- Contributors will learn the pool contribution amount.
- What will lead to the termination of the program.
- The pool will terminate itself when all Participants has recieved their contribution amount.
Now back to our DApp setup.
'reach 0.1';
/* section1: datatype definitions */
const poolDetails = Object({ /* Datatypes and constructor */ });
// Other declarations
/* section2: Participant interfaces */
export const main = Reach.App(() => {
const PC = Participant('PoolCreator', {
/* fill in interface */
})
/* section3: APIs */
const C = API('Contributor', {
/* fill in interface */
});
/* section4: Other APIs interface */
const A = API('Any', {
/* fill in interface */
});
/* section5: Views */
const V = View(, {
/* fill in view */
});
/* section7: Events */
const PP = Events({
/* fill in phase */
});
/* deploy app */
init();
/* section8: first consensus publication and payment */
PC.publish();
/* section9: set a view */
V.poolDetails.set();
/* section10: set an event (Registration Phase)*/
PP.Phase(Phase.registration());
/* section11: A linear state that keeps track of registered users */
const RegisteredUsers = new Set();
/* section12: registration While loop with parallelReduce*/
const [] =
parallelReduce([])
.invariant()
.while(/* while loop condition */)
.api(/* api call */)
/* section13: Contribution and request payment loop*/
// First the two linear states to keep track of paid users and contributors.
const usersPaidSet = new Set();
const contributorsSet = new Set();
// Then the loop.
var [] = []
invaraint();
while(/* while loop condition */){
commit();
/* perform transaction logic */
// -------------> Contribute Section <------------------
// -- A user calls the contribute api
// -- increment the number of users who has contributed.
// -- add the user to the set of users who has contributed.
PC.publish()
PP.phase(Phase.Contribution())
const [] =
parallelReduce([])
.invariant()
.while(/* while loop condition */)
.api(/* api call */)
.timeout(/* timeout */)
continue();
}
commit();
}
Intimidatingly, the above code is a lot of code. Lets go through the steps one by one briefly.
- In
section one
we define the datatypes that will be used. - In
section two
we define the participant interfaces. - In
section three
we define the APIs. We would have two APIs, one for the contributors to call and register themselves and one for anyone to call and contribute to the pool. Is being done this way so as to enable the pool creator to contribute during the contribution phase. - In
section four
we define the other APIs. - In
section five
we define the views. These views will be used to display the pool details to the contributors. - In
section six
we define the events. These events will be used to keep track of the phase of the pool, and when one contributes to the pool. - In
section seven
we deploy the app, using theinit
function. - In
section eight
The pool creator performs the first publication. - In
section nine
we set the view. - In
section ten
we emit an event that brocasts that the registration phase just began. - In
section eleven
A linear state that will keep track of regisgered users is declared. - In
section twelve
The registration while loop. - In
section thirteen
The contribution and request payment loop.
Next is our Javascript frontend code:
import { loadStdlib } from '@reach-sh/stdlib';
import * as backend from './build/index.main.mjs';
const stdlib = loadStdlib();
(async () => {
console.log("Starting");
console.log("Creating accounts")
const bal = reach.parseCurrency(20000);
const nContributors = 5;
const accPoolCreator = await reach.newTestAccount(bal);
const accContributors = await reach.newTestAccounts(nContributors, bal);
const ctcPC = accPoolCreator.contract(backend);
const ctcContributors = accContributors.map(acc => acc.contract(backend, ctcPC.getInfo()));
let resolveReadyForContributors = null;
const pReadyForContributors = new Promise(r => resolveReadyForContributors = r)
console.log(`Deploying .......`);
const pPoolCreator = ctcPC.p.PoolCreator({
// implement Alice's interact object here
});
do {
// Registration, Contributions and request payment api calls here
} while (phase !== 'Finished')
})();
What is happening here?
- Imports the reach standard library loader.
- imports your backend, which ./reach compile will produce.
- Then loads the standard library.
- Next, an asynchronous arrow function that runs till the completion of the app.
- Next is a starting balance being instatiated for the test accounts.
- Pool creator account is being created.
N
number of contributors is then created with the same starting balance.- The pool creator deploys the contract.
- While the contributors attaches to it.
- The pReadyForContributors
promise
will be explained later. - A backend for poolcreator is initialized
- using a
do while
loop to go through all phases.
In the previous section we defined the participant interfaces and APIs. Now we are going to put these together to implement the actual functionality. First we will implement the pool creator participant interface.
const PoolDetails = Object({
poolName: Bytes(16), // Name of the pool (e.g. "Pool 1").
poolDescription: Bytes(200), // Description of the pool (e.g. "Pool 1").
contributionAmt: UInt, // amount to be paid
penaltyAmt: UInt, // Amount to be deducted from the user, if he fails to contribute.
duration: UInt, // weeks, months, years. (in UNIX time)
maxUsers: UInt, // max amount of contribution
});
export const main = Reach.App(() => {
const PC = Participant('PoolCreator', {
poolDetails: PoolDetails,
readyForContributors: Fun([], Null),
});
const C = API('Contributor', {
contribute: Fun([], Null),
});
const A = API('Any', {
requestPayment: Fun([], Null),
contribute: Fun([], Null),
});
init();
PC.only(() => {
const poolDetails = declassify(interact.getPoolDetails);
});
PC.publish(poolDetails);
const startingContribution = contributionAmt + penaltyAmt;
commit();
PC.pay(startingContribution);
PC.interact.readyForContribution();
commit();
PC.publish()
})
Lets stop here for now and go through the code one by one.
- Using an
Object
we define the pool details. - Then we define the main export of the program, this is were the compiler will look at when compiling your code.
- The participant are then defined. Here have just one participant (the pool creator).
- Then we define the APIs, here we have two APIs, one for the contributors to call and register themselves and one for anyone to call and contribute to the pool.
- The
init
function marks the deployment of the program, which allows the program to start. PC.only
defines what only the pool creator can do. Here we are just recieving the pool details from the frontend,declassify
ing excplicitly makes the information recieves from the frontend public that is secret by default.- The pool details gotten from the frontend is then published to the consensus network using the
publish
function. - The pool creator then pays the starting contribution amount, which is the sum of the contribution amount and the penalty amount.
- And then the pool creator calls the
readyForContributors
function that notifies that the pool is ready for contribution.
Go back to the frontend and implement the contributor participant interface and APIs.
........................................
let resolveReadyForContributors = null;
const pReadyForContributors = new Promise(r => resolveReadyForContributors = r)
const pPoolCreator = ctcPC.p.PoolCreator({
getPoolDetails: {
poolName: "Umunna Collectio",
poolDescription: "An Arbitrary sized tanda club. Where anyone can join, make a payment as specified an request for a pay after each cycle. This description is going to be 200 bytes long so I'm going to keep typing till",
contributionAmt: reach.parseCurrency(10),
penaltyAmt: reach.parseCurrency(5),
duration: 3,
maxUsers: 4,
},
readyForContributors: () => {
resolveReadyForContributors();
},
Now, to explain what is happening here.
- The
pReadyForContributors
promise
is created. - The
resolveReadyForContributors
is set to a function that will resolve the promise. - The
pPoolCreator
is initialized with thegetPoolDetails
object. - The
readyForContributors
function is called, that will resolve thepReadyForContributors
promise. - Remember, the
ctcPC
is the pool creator contract deployed withaccPoolCreator.contract(backend)
, and thepPoolCreator
is the pool creator'sparticipant
object.
I hope this is clear enough.
Let's go back to the index.rsh
file and continue from were we stopped.
Next is to write the registration phase. A while loop.
........................................
previous code
........................................
const RegisteredUsers = new Set();
const [numOfUsers] =
parallelReduce([ 0 ])
.invariant(numOfUsers >= 0)
.while(numOfUsers <= maxUsers)
.api(C.register,
(() => {check(!RegisteredUsers.member(this))}),
() => penaltyAmt,
((callBack) => {
RegisteredUsers.insert(this)
callBack(null)
return [numOfUsers + 1]
}))
- The
previous code
is the code that was written before the registration phase. - We define the
RegisteredUsers
set. This is used to store the users that have registered. - The
parallelReduce
function is used to create a parallel reduce function, and we keep track of the number of users withnumOfUsers
. - The invariant is used to check variables that will be mutated during the loop, as
reach-lang
does not support mutation. Those values must be true before and after the loop. - Then the
api
function is used to call theregister
API. Thecheck
function is used to check if the user has already registered. The caller then pays the penalty amount. Then acallBack
function is called, that returns whatever to the frontend. In our case it returnsnull
. - The user is then added to the
RegisteredUsers
set. - We then increment the number of users by one.
So, now we have the registration phase settled.
Next stop is to write the contribution phase. Open your index.rsh
file and follow along.
........................................
previous code
........................................
const usersPaidSet = new Set();
const contributorsSet = new Set();
var [usersPaid, numUsers] = [0, 0];
invariant(usersPaid <= numUsers);
while(true) {
commit();
// -------------> Contribute Section <------------------
// -- A user calls the contribute api
// -- increment the number of users who has contributed.
// -- add the user to the set of users who has contributed.
PC.publish()
PP.phase(Phase.Contribution())
const period = absoluteTime(lastConsensusTime() + duration)
const [timedOut, IusersPaid, InumUsers] =
parallelReduce([true, usersPaid, maxUsers ])
.invariant(usersPaid <= numUsers)
.while(timedOut)
.api(
A.contribute,
(() => contributionAmt),
((returnFunc) => {
contributorsSet.insert(this);
U.info(this, contributionAmt);
returnFunc(null)
// InumUsers = InumUsers + 1;
return [true, IusersPaid, InumUsers]
})
)
.timeout(period, () => {
PC.publish()
return [false, IusersPaid, InumUsers]
});
commit();
// -------------> End Contribute Section <----------
PC.publish()
// payment time is reached
commit()
PC.publish()
PP.phase(Phase.Payment());
commit();
// -----------> Payment api <-----------
// -- a user calls the api for payment
// -- a check to make sure the user hasnt been paid before.
// -- a check to make sure the user is a contributor
// -- send balance to the user address.
// -- add the user to the set of users paid.
// -- increment the number of users paid.
const [[], returnPayFunc] =
call(A.requestPayment)
.pay(() => 0)
.assume(() => {
check(!usersPaidSet.member(this));
})
transfer(balance()).to(this)
usersPaidSet.insert(this);
// usersPaid + 1;
returnPayFunc(null);
[usersPaid, numUsers] = [usersPaid + 1, numUsers + 1];
continue;
}
Let's breifly explain what is happening here.
- The
previous code
is the code that was written before the contribution phase. - Two linear states is first created using set (
usersPaidSet
andcontributorsSet
) to keep track of users paid and contributed respectively. - We've explained the invariant and the while loop before.
- This loop will run till all users that contributed to the pool has requested and recieved payments.
- Then a parallelReduce for the contribution phase API call.
- At the end of the specified duration the loop exits, and then the payment phase.
- The payment phase is just an API call. It first verifies that the user isn't in the
usersPaidSet
. A transfer is made to the user. The user is then added to theusersPaidSet
set.
Lets implement these two phases in our javascript frontend.
........................................
previous code
........................................
const tryFn = async (lab, f) => {
const maxTries = 3;
let tries = 1;
const msg = () => `${lab} after trying ${tries} time(s)`
let err = null;
while (tries < maxTries) {
try {
const r = await f();
console.log(msg());
return r;
} catch (e) {
err = e;
tries++;
}
}
console.error(`Failed: ${msg()}`);
throw err;
}
function pretty(r) {
if (!r) {
return r;
} else if (typeof r === 'string') {
return r;
} else if (r._isBigNumber) {
return r.toString();
} else if (r.networkAccount) {
if (r.networkAccount.addr) {
return r.networkAccount.addr.slice(0, 8);
} else if (r.networkAccount.address) {
return r.networkAccount.address.slice(0, 8);
} else {
return '<some acc>';
}
} else if (Array.isArray(r) && r[0] == 'Some') {
return pretty(r[1]);
} else if (Array.isArray(r)) {
return r.map((x) => pretty(x));
} else if (Object.keys(r).length > 0) {
const o = {};
for (const k in r) { o[k] = pretty(r[k]); }
return o;
} else if (r.toString) {
return r.toString();
} else {
return r
}
}
let phase;
do {
const ev = await ctcPC.events.PoolPhase.phase.next();
console.log(pretty(ev))
phase = ev.what[0][0]; // get the name of the phase from the event structure
switch (phase) {
case "Registration":
console.log("Registration phase started");
// ---------------- Registration API ------------
const tryApi = async (fname, verbed, i) =>
await tryFn(`Someone #${i} ${verbed}`, ctcContributors[i].apis.Contributor[fname]);
const tryRegister = async (i) => {
await tryApi('register', 'Registered', i)
};
const reg = [];
for (let i = 0; i < nContributors; i++) {
reg.push(tryRegister(i));
await reg[i];
}
await Promise.all(reg)
await balance()
break;
// Contribution started
case "Contribution":
// ------ Contribute API ----------------
console.log("Contribution phase started");
const tryCApi = async (fname, verbed, i) => {
await tryFn(`Someone #${i} ${verbed}`, ctcContributors[i].apis.Any[fname]);
}
const trycontribute = async (i) => {
await tryCApi('contribute', 'Contributed', i)
};
const contrib = [];
for (let i = 0; i < nContributors; i++) {
contrib.push(trycontribute(i));
await contrib[i];
}
await Promise.all(contrib)
await tryFn(`Pool Creator #${accPoolCreator} Contributed`, ctcPC.apis.Any.contribute);
await reach.wait(3);
console.warn("Contribution timeout occurred");
await balance();
break;
// Payment started
case "Payment":
console.info("Payment Phase started")
// ------- Payment API ------------------
const random = Math.floor(Math.random() * nContributors);
const tryPApi = async (fname, verbed, i) => {
await tryFn(`Someone #${i} ${verbed}`, ctcContributors[i].apis.Any[fname]);
};
const tryRequest = async (i) => {
await tryPApi('requestPayment', 'Requested', i)
};
await tryRequest(random);
await balance()
break;
// The contract is over
case "Finished":
break;
}
} while (phase !== 'Finished');
await pPoolCreator;
Now thats a lot of code. Lets break it down.
- The
previous code
is the code that was written before the registration phase. - We first define a utility function
tryFn
that will try to run a functionf
and catch any errors. - Then a pretty function
pretty
that will pretty print the result of the raw output from the consensus network. - The variable
phase
is used to keep track of the current phase. - A do while loop is used to keep the loop running until the contract is over.
- The variable
ev
is used to get the current phase from the consensus network. - Having gotten the specific phase from the blockchain, we make use of switch statement to decide on which code block to run.
Congratulations you just completed DApp with a Javascript frontend test!! Yoh!!!
For the react frontend, we'll make use of class based component.
It would be great if you have fair knowledge of React, and class based components.
The folder structure is below:
Our focus will be on the views folder. But before then we need to install the dependencies.
clone the repo. Then run:
cd DApp
npm install
In the src
directory, in the index.js
file, We'll have three components:
- The
App
component. - The
Deployer
component, and, - The
Contributor
component.
- The
App
Component renders the AppViews, we'll talk about theAppViews
later. - When this component mounts, The
SelectNetwork
View is rendered. This allows for network section Buttons.
exports.SelectNetwork = class extends React.Component {
render() {
const { parent } = this.props;
return (
<>
{/* <p className='MainContent'> */}
<div className="big-title">
<h1>Future is here,</h1>
<h1>Start Exploring now.</h1>
</div>
<br />
<h3>
Select a network
</h3>
<br />
{/* </div> */}
<div className="cta">
<button className="btn" onClick={() => parent.selectNetwork('ALGO', 'TestNet')}
>Algorand TestNet</button>
<br />
<br />
<button className="btn" onClick={() => parent.selectNetwork('ALGO', 'MainNet')}
>Algorand MainNet</button>
<br />
</div>
</>
)
}
}
- After network selection
ConnectAccount
View is rendered.
exports.ConnectAccount = class extends React.Component {
render() {
const { parent, connector } = this.props;
const ctcInfoStr = false;
return (
<>
<h4 >
Please click the button to connect your account.
{connector === 'ALGO' ? <><br />You may need to disable your popup blocker</> : ''}
{connector === 'ETH' ? <><br />Select the desired network in MetaMask and refresh the page if necessary.</> : ''}
</h4>
{/* <br /> */}
<br />
{ connector === 'ALGO' ? <>
<button className="btn" onClick={
() => parent.openWalletPopUp('MyAlgoConnect')
}>MyAlgoConnect</button>
</> : <>
<button className='MyAlgoWalletButton' onClick={
() => parent.openWalletPopUp('MetaMask')
}>MetaMask</button>
</> }
<br />
<br />
</>
)
}
}
This async
function openWalletPopUp
connects the users wallet and sets thier balance and address in a state: acc
and bal
, as seen below:
async openWalletPopUp(which) {
const {providerEnv} = this.state;
if (which === 'MyAlgoConnect') {
reach.setWalletFallback(reach.walletFallback({
MyAlgoConnect,
providerEnv,
}));
} else if (which === 'MetaMask') {
// Anything to do here? Should just work.
}
const acc = await reach.getDefaultAccount();
const balAtomic = await reach.balanceOf(acc);
const bal = reach.formatCurrency(balAtomic, 4);
console.log(bal);
this.setState({acc, bal, view: 'RoleSelect'}); // XXX create view
}
- On successfull wallet connection, The
RoleSelect
View will be rendered
exports.RoleSelect = class extends React.Component {
render() {
const {parent} = this.props;
return (
<div>
<p className='MainContent'>
Please select a role:
</p>
<br />
<span className='LargeButtonContainer'>
<button className="btn"
onClick={() => parent.selectDeployer()}
>
<h1>Creator</h1>
<p>
{/* Incentivise others to stake by creating and funding a staking pool with rewards. */}
Create a pool and share your pool "ID" with your friends to join!
</p>
</button>
<br />
<br />
<br />
<button className="btn"
onClick={() => parent.selectContributor()}
>
<h1>Contributor</h1>
<p>
Join a tanda pool and get the pot at the end of a round
</p>
</button>
</span>
</div>
);
}
}
All will be rendered in the Wrapper
View.
We'll forcus on the index.js
file. In the Deployer Component, we have an async function that deploys the contract with the specified poolDetails
:
async deploy(getPoolDetails) {
const thiz = this;
const ctc = this.props.acc.contract(backend);
this.setState({view: 'Deploying', ctc});
const deployerP = ctc.p.PoolCreator({
getPoolDetails,
readyForContribution: (async () => {
const ctcInfoStr = await ctc.getInfo();
thiz.setState({view: 'Deployed', ctcInfoStr});
}),
});
this.setState({view: 'Deploying', ctc});
await deployerP;
this.setState({view: 'Done'});
}
async attach(ctcInfoStr) {
const ctcparse = (s) => {
try { return JSON.parse(s); }
catch (e) { return s; }
};
const acc = this.props.acc;
const ctc = acc.contract(backend, ctcparse(ctcInfoStr));
this.setState({ctc, ctcInfoStr, view: 'Attaching'});
await this._refreshInfo(acc, ctc);
This will attach to the contract and call.
async _api(which, name, ...args) {
const {acc, ctc} = this.state;
console.log(`calling api: ${which}.${name}`);
const res = await ctc.apis[which][name](...args);
console.log(pretty(res));
await this._refreshInfo(acc, ctc);
}
async _event() {
const {acc, ctc} = this.state;
console.log(`calling event: `);
const res = await ctc.events.PoolPhase.phase.next();
console.log(res.what[0][0]);
this.setState({phase: this.state.phase + '\n' + res.what[0][0]});
await this._refreshInfo(acc, ctc)
}
DISCLAIMER: Please note that is only a tutorial and not meant to be used in the real-world. The code has not been tested or audited for vulnerabilities.