Create Publication

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Tutorial Thumbnail
Beginner · 1 hour

Using WalletConnect with Reach in an RSVP system

This solution provides an RSVP System using WalletConnect and Reach. It is integrated with the Algorand Pera Wallet and shows how to integrate transactions with an approval process in the wallet. This solution uses the WalletConnect standard in a Reach solution. The application is a Web-based solution with a React frontend.

Requirements

Background

This sample provides the backend Reach code in index.rsh and the React frontend in index.js. The solution runs on TestNet.

Steps

Step 1 - Setup Pera Wallet

To use the Algo Pera Wallet for TestNet, select settings | Developer Settings | Node Settings | TestNet shown below in Figures 1-1, 1-2, and 1-3. Create an account to test with if you do not have one already tapping the + symbol on the top far right on the home page. There is also a link in Developer Settings to fund a TestNet account from a dispenser. Funds are needed to cover transaction fees as well as the RSVP amount due to hold a seat.

Pera Algo Wallet Settings

Figure 1-1 - Pera Algo Wallet Settings

Pera Algo Wallet Node Settings

Figure 1-2 Pera Algo Wallet Node Settings.

Pera Algo Wallet - Set to TestNet
Figure 1-3 Pera Algo Wallet - Set to TestNet

Step 2 - Connect a Pera Wallet account with the RSVP system

The following two lines of code are used for WalletConnect integration in the frontend index.js. When the solution is executed, the user will be shown a QR Code. Scanning this QR Code will bring the user to their Wallet and prompt them to select an account to connect with.

import { ALGO_WalletConnect as WalletConnect } from '@reach-sh/stdlib';

stdlib.setWalletFallback(stdlib.walletFallback({
   providerEnv: 'TestNet', WalletConnect }));

To Run:

The following environment variables need to be set or they can be provided in the code as options when creating an instance of stdlib as follows in index.js: REACH_CONNECTOR_MODE: ALGO-live is used to access TestNet.

var opts = {
 REACH_CONNECTOR_MODE: 'ALGO-live',
 ALGO_SERVER: 'https://academy-algod.dev.aws.algodev.network',
 ALGO_PORT: '',
 ALGO_TOKEN: '2f3203f21e738a1de6110eba6984f9d03e5a95d7a577b34616854064cf2c0e7b',
 ALGO_INDEXER_SERVER: 'https://algoindexer.testnet.algoexplorerapi.io',
 ALGO_INDEXER_PORT: '',
 ALGO_INDEXER_TOKEN: '',
 HTTPS:false
};

const stdlib = loadStdlib(opts);

./reach update

make react

The development server starts once you see this in the terminal output window.

You can now view reach-react-app in the browser.

  Local:            http://localhost:3000
  On Your Network:  http://172.27.0.3:3000

If you see an error similar to the following, delete the node_modules folder and re-run npm install.

ERROR in ./src/index.js 36:32-39
export 'jsxDEV' (imported as '_jsxDEV') was not found in 'react/jsx-dev-runtime' (module has no exports)

Once the development server starts, navigate to http://localhost:3000 and you should see a QR code displayed. In the Pera Wallet app, hit the scan symbol, to the left of the + symbol at the top of the home page as shown in Figure 2-1 and Figure 2-2 and scan it.

Press the Scan symbol
Figure 2-1 Press the Scan symbol

Scan the QR Code
Figure 2-2 Scan the QR Code

In the above Figure 2-2, note at the bottom “32 apps connected”. If you ever wish to disconnect an app simply click on that to bring the list and click “…” on the one you want to remove and select Disconnect.

Select Account to connect to as shown in Figure 2-3. Press and hold the account in the account list to copy/save the account address off. You can paste that in the explorer as shown in Figure 3-8 later.

Select an account to connect to. Then, press and hold the Account in the account list, select copy Account address and save off.
Figure 2-3 Select an account to connect to. Then, press and hold the Account in the account list, select copy Account address and save off.

Step 3 - Use the RSVP System

After an account is connected as the default account, the RSVP system will then start as shown in Figure 3-1.

Reservation System
Figure 3-1 Reservation System

Select Create an event and enter the desired RSVP Fee and the deadline or accept the defaults and press Launch as shown in Figure 3-2. The deadline is the number of blocks created. The actual time in seconds that the event takes is roughly 4 times the number of blocks specified. Go to the Pera Algo Wallet and you should see an opt-in confirmation for the contract. Wait while the event is initialized.

Launch Event
Figure 3-2 Launch Event

Stay on this page until all transactions are signed. Press confirm and accept Opt-in as shown in Figure 3-3.

Opt-in
Figure 3-3 Opt-in

Then you will see the contract application call displaying the app ID, confirming that the smart contract has been deployed to the blockchain in Figure 3-4 below.

Contract deployed and the Application ID is displayed.
Figure 3-4 Contract deployed and the Application ID is displayed.

You will see a Multiple Transaction request as in Figure 3-5. Click “Confirm All”

Confirm Multiple request inner transactions
Figure 3-5 Confirm Multiple request inner transactions.

Next, confirm the application call as shown in Figure 3-6.

Confirm Application call
Figure 3-6 Confirm Application call.

Then copy the contract information that is displayed. This is the application id as shown in Figure 3-7 similar to…

{
  "type": "BigNumber",
  "hex": "0x05a6751c"
}

Application ID
Figure 3-7 Application ID

Verify information on the Account in a blockchain explorer such as Algoexplorer.

Here are all of the transactions from Step 3 in Figure 3-8 using the account using the account address you saved off above in Figure 2-3. The list of transactions appear below the account information.

Transactions to Create the RSVP Event
Figure 3-8 Transactions to Create the RSVP Event

The first transaction deployed the RSVP dApp to the blockchain and you can see the approval and clear program and the Reach generated TEAL as in Figure 3-9.

Application Deployed to Blockchain
Figure 3-9 Application Deployed to Blockchain.

Step 4 - RSVP, Checkin, and Close Event

Before refreshing the page, make sure you have the contract info saved off. Next, refresh the page and select rsvp in Figure 4-1.

RSVP for an event.
Figure 4-1 RSVP for an event.

Paste in the application id information and select the RSVP button. It should display the account address in Figure 4-3.

Paste in Contract Information for the Event
Figure 4-2 Paste in Contract Information for the Event.

Confirm the account on the Algo Pera wallet for the Multiple Transaction Request and wait while your RSVP is confirmed in Figure 4-3. It may take a while for the txn request to show up in your wallet. Your RSVP account address will be displayed. Make sure your contract info is saved off, then copy and save the RSVP address as well. Here I am testing with two devices as discussed below in Figure 4-9, so it shows Bob’s account from the second device. When testing with one device, the account you see displayed here will be the same account you started with above for Alice.

Copy and save off your address from RSVP
Figure 4.3 Copy and save off your address from RSVP

Refresh the page and select Check-in. Paste the contract info and the account address and sign the transaction in the wallet. See Figure 4-4

RSVP to the event

Figure 4.4 RSVP to the event

Refresh the page and select Close the event, paste in the contract information and wait for the event to close. You will see “You have closed the event” appear. Note if the time (specified number of blocks provided at event creation) is not up yet, it will hang. To see if it has ended yet, bring up the Terminal in the chrome browser using option + command + I. or use the developer tools flyout menu choice in the browser… More Tools | Developer Tools as shown in Figures 4.5 and 4.6. If you see Checkin.timesUp errored with Error: API call failed then the timer simply has not expired yet. Try again after the estimated time expires. The total time for the event in seconds would be the value of duration (number of blocks) when the admin set the event up, times approximately 4 seconds per block.

Chrome Developer tools
Figure 4-5 Chrome Developer tools

Developer Tools Console
Figure 4-6 Developer Tools Console

View the results in a TestNet Blockchain Explorer. The fee is sent from account BOB.. to the contract account which begins with RZJ… and then returned to BOB from the contract Account once he is checked in. See Figure 4.7.

Note

Algo Pera wallet currently does not show inner transactions at the time of writing this solution, so verify with an explorer.

View from bottom up. The fee is sent from account BOB… and then back to him after RSVPing
Figure 4-7 View from bottom up. The fee is sent from account BOB… and then back to him after RSVPing.

For testing with two devices, one for an Admin account and one for an Attendee (Bob) account start two incognito windows with these URLs.

Note

You may need to disconnect the account from the above exercise to see the QR Code.

Connected apps
Figure 4-8 - Connected apps

Note

In Figure 4-8 above, at the bottom “32 apps connected”. If you ever wish to disconnect an app simply click on that to bring up the list and click “…” on the one you want to remove and select Disconnect.

Using these URLs:

http://localhost:3000 (Admin)

http://127.0.0.1:3000/ (Bob)

2 QR codes, 1 for Admin, one for the attendee.
Figure 4-9 - 2 QR codes, 1 for Admin, one for the attendee.

Scan the left with the Admin Account on the first device and the one on the right with Bob’s account on the other device.

If you repeat the process and do not check-in, the funds will not be sent back to the Attendee. You should see something like Figure 4-10 below. This time the contract address starts with CUZ…

From the Admin window run the functions for Create Event and omit Check-in. From the Attendee window just RSVP. Then back in the Admin windows, close the event.

See results below where the Admin ends up with the registration fee in Figure 4-10.

Read from bottom up. Contract sends 2 Algos to ADM… which is the Admin account for when no RSVP is provided
Figure 4-10 Read from bottom up. Contract sends 2 Algos to ADM… which is the Admin account for when no RSVP is provided.

The Frontend and Backend code

In the backend code below index.rsh, note the Admin participant, Attendee, and Checkin interfaces. The admin will publish the price and deadline. The parallelReduce code provides the logic for RSVP and check-in. The first .api flow performs the logic for RSVP. The second .api flow takes care of the check-in process and transfers the fee back to the checked-in account. After the timeout, the funds that were collected for all of those who registered will be sent to those that attended, the leftover amount will be transferred to the admin.

Here is the Backend Code in the index.rsh file:

'reach 0.1';
'use strict';
// This section provides the interface to the frontend
// Fun stands for Function
// Admin is the Participant
// Many people can attend and check-in through the API's

export const main = Reach.App(() => {
 const D = Participant('Admin', {
   price: UInt,
   deadline: UInt,
   ready: Fun([], Null),
 });
 const A = API('Attendee', {
   iWillGo: Fun([], Bool),
 });
 const C = API('Checkin', {
   theyCame: Fun([Address], Bool),
   timesUp: Fun([], Bool),
 });
 init();

 // Administrator sets the price and deadline in blocks
 // Commits and publishes that info to the blockchain
 // Signed by the Admin Account in Pera Wallet
 D.only(() => {
   const price = declassify(interact.price);
   const deadline = declassify(interact.deadline);
 });
 D.publish(price, deadline);
 commit();
 D.publish();
 D.interact.ready();

 const deadlineBlock = relativeTime(deadline);
 const RSVPs = new Set();
 // APIs for RSVP (iWillGo) and Checkin (theyCame)

 const [ keepGoing, howMany ] =
   parallelReduce([true, 0])
   .invariant(balance() == howMany * price)
   .invariant(RSVPs.Map.size() == howMany)
   .while( keepGoing )
   // iWillGo is signed by the attendee
   .api_(A.iWillGo, () => {
     check( ! RSVPs.member(this), "not yet" );
     return [ price, (k) => {
       k(true);
       RSVPs.insert(this);
       return [ keepGoing, howMany + 1 ];
     }];
   })
   // theyCame is signed by the Admin
   .api_(C.theyCame, (who) => {
     check( this == D, "you are the boss");
     check( RSVPs.member(who), "yep" );
     return [ 0, (k) => {
       k(true);
       transfer(price).to(who);
       RSVPs.remove(who);
       return [ keepGoing, howMany - 1 ];
     }];
   })
   // after timeout is signed by the Admin signs to close the event
   .timeout( deadlineBlock, () => {
     const [ [], k ] = call(C.timesUp);
     k(true);
     return [ false, howMany ]
   });
 const leftovers = howMany;
 transfer(leftovers * price).to(D);
 commit();
 exit();
});

Here is the React Code in the frontend in index.js.

Only two lines of code are needed for Pera Wallet integration as shown here.

import { ALGO_WalletConnect as WalletConnect } from '@reach-sh/stdlib';
stdlib.setWalletFallback(stdlib.walletFallback({
   providerEnv: 'TestNet', WalletConnect }));

If the MyAlgo Wallet is desired instead of WalletConnect, replace the commented-out WalletConnect Import and set WalletFallback accordingly.

import { ALGO_MyAlgoConnect as MyAlgoConnect } from '@reach-sh/stdlib';

stdlib.setWalletFallback(stdlib.walletFallback({
     providerEnv: 'TestNet', MyAlgoConnect }));

Environment settings are provided in the opts map which is passed into the loadStdlib param as shown here:

var opts = {
 REACH_CONNECTOR_MODE: 'ALGO-live',
 ALGO_SERVER: 'https://academy-algod.dev.aws.algodev.network',
 ALGO_PORT: '',
 ALGO_TOKEN: '2f3203f21e738a1de6110eba6984f9d03e5a95d7a577b34616854064cf2c0e7b',
 ALGO_INDEXER_SERVER: 'https://algoindexer.testnet.algoexplorerapi.io',
 ALGO_INDEXER_PORT: '',
 ALGO_INDEXER_TOKEN: '',
 HTTPS:false
};
const stdlib = loadStdlib(opts);

AlgoSigner is not supported at the time of writing this. The code below shows how to use the Pera Algo Wallet. The four main functions in the frontend are to Create the Event, Attendee RSVP, Attendee Check-in, and Close the event.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import * as backend from './build/index.main.mjs';
import { loadStdlib } from '@reach-sh/stdlib';
// import { ALGO_MyAlgoConnect as MyAlgoConnect } from '@reach-sh/stdlib';
import { ALGO_WalletConnect as WalletConnect } from '@reach-sh/stdlib';
// provide environment settings
var opts = {
 REACH_CONNECTOR_MODE: 'ALGO-live',
 ALGO_SERVER: 'https://academy-algod.dev.aws.algodev.network',
 ALGO_PORT: '',
 ALGO_TOKEN: '2f3203f21e738a1de6110eba6984f9d03e5a95d7a577b34616854064cf2c0e7b',
 ALGO_INDEXER_SERVER: 'https://algoindexer.testnet.algoexplorerapi.io',
 ALGO_INDEXER_PORT: '',
 ALGO_INDEXER_TOKEN: '',
 HTTPS:false
};

const stdlib = loadStdlib(opts);
stdlib.setWalletFallback(stdlib.walletFallback({
   providerEnv: 'TestNet', WalletConnect }));
// stdlib.setWalletFallback(stdlib.walletFallback({
//       providerEnv: 'TestNet', MyAlgoConnect }));
const {standardUnit} = stdlib;
const defaultPrice = '2';
const defaultDeadline = '32';

function renderDOM() {
 ReactDOM.render(
   <React.StrictMode><App /></React.StrictMode>,
   document.getElementById('root')
 );
}

class App extends React.Component {
 constructor(props) {
   super(props);
   this.state = {mode: 'Connect'}
 }
 async componentDidMount() {
   const acc = await stdlib.getDefaultAccount();
   try {
     const faucet = await stdlib.getFaucet();
     stdlib.transfer(faucet, acc, stdlib.parseCurrency(100));
   } catch (e) {
     console.error(e);
   }
   this.setState({mode: 'Select', acc });
 }
 selectRole(role) { this.setState({mode: 'RunRole', role}); }
 doCreate()  { this.selectRole(<Create  acc={this.state.acc} />); }
 doRSVP()    { this.selectRole(<RSVP    acc={this.state.acc} />); }
 doCheckin() { this.selectRole(<Checkin acc={this.state.acc} />); }
 doClose()   { this.selectRole(<Close   acc={this.state.acc} />); }
 render() {
   const {mode, addr, bal, role} = this.state;
   const parent = this;
   let app = null;
   if (mode === 'Connect') {
     app = (
       <div>
         Please wait while we connect to your account.
         If this takes more than a few seconds, there may be something wrong.
       </div>
     )
   } else if (mode === 'Select') {
     app = (
       <div>
         <p>
           <button
             onClick={() => parent.doCreate()}
           >Create</button>
           <br />
           Create an event
         </p>
         <p>
           <button
             onClick={() => parent.doRSVP()}
           >RSVP</button>
           <br />
           RSVP for an event
         </p>
         <p>
           <button
             onClick={() => parent.doCheckin()}
           >Checkin</button>
           <br />
           Check-in at an event
         </p>
         <p>
           <button
             onClick={() => parent.doClose()}
           >Close</button>
           <br />
           Close an event
         </p>
       </div>
     );
   } else { // 'RunRole'
     app = role;
   }
   return (
     <div className="App">
       <header className="App-header" id="root">
         {app}
       </header>
     </div>
   );
 }
}

class Create extends React.Component {
 constructor(props) {
   super(props);
   this.setState({mode: 'EnterInfo'});
 }
 async enterInfo(priceStandard, deadline) {
   const ctc = this.props.acc.contract(backend);
   this.setState({mode: 'Wait', priceStandard, deadline, ctc});
   console.log({priceStandard, deadline});
   try {
     await ctc.p.Admin({
       price: stdlib.parseCurrency(priceStandard),
       deadline: stdlib.bigNumberify(deadline),
       ready: () => {
         throw 42;
       }
     });
   } catch (e) {
     if ( e !== 42 ) {
       throw e;
     }
   }
   const ctcInfoStr = JSON.stringify(await ctc.getInfo(), null, 2);
   this.setState({mode: 'Done', ctcInfoStr});
 }
 render() {
   let me = null;
   const parent = this;
   const mode = this.state?.mode || 'EnterInfo';
   if (mode === 'EnterInfo') {
     const priceStandard = this.state?.priceStandard || defaultPrice;
     const deadline = this.state?.deadline || defaultDeadline;
     me = (
       <div>
         What is the RSVP fee?
         <br />
         <textarea
           onChange={(e) => this.setState({
             priceStandard: e.currentTarget.value})}
           placeholder={defaultPrice}
         />
         <br />
         What is the deadline?
         <br />
         <textarea
           onChange={(e) => this.setState({
             deadline: e.currentTarget.value})}
           placeholder={defaultDeadline}
         />
         <br />
         <button onClick={() => parent.enterInfo(
           priceStandard,
           deadline,
         )}
         >Launch</button>
       </div>
     );
   } else if (mode === 'Wait') {
     me = (
       <div>
         Please wait while your event is initialized.
       </div>
     );
   } else { // 'Done'
     const ctcInfoStr = this.state?.ctcInfoStr || '';
     me = (
       <div>
         Your event is ready for users to RSVP to!
         <br />

         Please share the following contract info with them:

         <pre className='ContractInfo'>
           {ctcInfoStr}
         </pre>
       </div>
     );
   }
   return (
     <div className="Create">
       {me}
     </div>
   );
 }
}

class RSVP extends React.Component {
 constructor(props) {
   super(props);
   this.setState({mode: 'EnterInfo'});
 }
 async doRSVP(ctcInfoStr) {
   const ctcInfo = JSON.parse(ctcInfoStr);
   const ctc = this.props.acc.contract(backend, ctcInfo);
   this.setState({mode: 'Wait', ctc});
   await ctc.apis.Attendee.iWillGo();
   this.setState({mode: 'Done'});
 }
 render() {
   let me = null;
   const parent = this;
   const mode = this.state?.mode || 'EnterInfo';
   if (mode === 'EnterInfo') {
     const ctcInfoStr = this.state?.ctcInfoStr || '';
     me = (
       <div>
         What is the event info?
         <br />
         <textarea
           className='ContractInfo'
           spellCheck='false'
           onChange={(e) => this.setState({ctcInfoStr: e.currentTarget.value})}
           placeholder='{}'
         />
         <br />
         <button
           disabled={!ctcInfoStr}
           onClick={() => parent.doRSVP(ctcInfoStr)}
         >RSVP</button>
       </div>
     );
   } else if (mode === 'Wait') {
     me = (
       <div>
         Please wait while your RSVP is confirmed.
       </div>
     );
   } else { // 'Done'
     const {acc} = this.props;
     me = (
       <div>
         You have RSVP'd.
         <br />

         Your address is:

         <pre className='ContractInfo'>
           {stdlib.formatAddress(acc)}
         </pre>
       </div>
     );
   }
   return (
     <div className="RSVP">
       {me}
     </div>
   );
 }
}

class Checkin extends React.Component {
 constructor(props) {
   super(props);
   this.setState({mode: 'EnterInfo'});
 }
 async doCheckin(ctcInfoStr, who) {
   const ctcInfo = JSON.parse(ctcInfoStr);
   const ctc = this.props.acc.contract(backend, ctcInfo);
   this.setState({mode: 'Wait', ctc, who});
   await ctc.apis.Checkin.theyCame(who);
   this.setState({mode: 'Done'});
 }
 render() {
   let me = null;
   const parent = this;
   const mode = this.state?.mode || 'EnterInfo'
   if (mode === 'EnterInfo') {
     const ctcInfoStr = this.state?.ctcInfoStr || '';
     const who = this.state?.who || '';
     me = (
       <div>
         What is the event info?
         <br />
         <textarea
           className='ContractInfo'
           spellCheck='false'
           onChange={(e) => this.setState({ctcInfoStr: e.currentTarget.value})}
           placeholder='{}'
         />
         <br />
         Who is checking in?
         <br />
         <textarea
           onChange={(e) => this.setState({
             who: e.currentTarget.value})}
         />
         <br />
         <button
           disabled={!ctcInfoStr}
           onClick={() => parent.doCheckin(ctcInfoStr, who)}
         >RSVP</button>
       </div>
     );
   } else if (mode === 'Wait') {
     me = (
       <div>
         Please wait while your checkin is confirmed.
       </div>
     );
   } else { // 'Done'
     const who = this.state?.who || '';
     me = (
       <div>
         Done! You have checked in {who}.
         <br />
       </div>
     );
   }
   return (
     <div className="Checkin">
       {me}
     </div>
   );
 }
}

class Close extends React.Component {
 constructor(props) {
   super(props);
   this.setState({mode: 'EnterInfo'});
 }
 async doClose(ctcInfoStr) {
   const ctcInfo = JSON.parse(ctcInfoStr);
   const ctc = this.props.acc.contract(backend, ctcInfo);
   this.setState({mode: 'Wait', ctc});
   await ctc.apis.Checkin.timesUp();
   this.setState({mode: 'Done'});
 }
 render() {
   let me = null;
   const parent = this;
   const mode = this.state?.mode || 'EnterInfo';
   if (mode === 'EnterInfo') {
     const ctcInfoStr = this.state?.ctcInfoStr || '';
     me = (
       <div>
         What is the event info?
         <br />
         <textarea
           className='ContractInfo'
           spellCheck='false'
           onChange={(e) => this.setState({ctcInfoStr: e.currentTarget.value})}
           placeholder='{}'
         />
         <br />
         <button
           disabled={!ctcInfoStr}
           onClick={() => parent.doClose(ctcInfoStr)}
         >Close</button>
       </div>
     );
   } else if (mode === 'Wait') {
     me = (
       <div>
         Please wait while your close is confirmed.
       </div>
     );
   } else { // 'Done'
     me = (
       <div>
         You have closed the event.
       </div>
     );
   }
   return (
     <div className="Close">
       {me}
     </div>
   );
 }
}

renderDOM();

Summary

This tutorial provides information on how to get started using TestNet with the Algorand Pera Wallet, as well as illustrates the signing process of a typical Wallet-based application. The Reach program provides the backend logic to perform the smart contract functions of event creation, RSVP, check-in and Close in the index.rsh file. The frontend provides the User interface written using the React Framework. It also provides the Wallet interaction with just a few lines of code. See more information on the Reach RSVP docs

The source code can be found here:

Github Repo: https://github.com/algorand-devrel/Walletconnect-Reach