Tutorial 4: Building a zkApp UI in the Browser with React
zkApp programmability is not yet available on the Mina Mainnet. You can get started now by deploying zkApps to the Berkeley Testnet.
Overview
In previous tutorials, we've seen how to write zkApps and deploy them to a network.
In this tutorial, we will implement a browser UI using ReactJS that interacts with a smart contract running on Berkeley.
We will discuss how to setup our project, implement its functionality, and deploy it to Github Pages.
If you would like to try out the application before going through the tutorial, you can do so on an instance already deployed to a Github Pages here. Extra info is printed to the console as well when it is run, to give an idea of what it is doing internally.
What we will build
The application we build will implement the following:
- Loading a public key from an extension-based wallet (tested with Auro >= 2.1.2)
- Checking if the public key has funds, and direct the user to the faucet if not.
- Connecting to example zkApp Addsmart contract, already deployed on Berkeley testnet at a fixed address.
- A button that when clicked sends a transaction.
- A button that when clicked requests the latest state of the smart contract.
Like in tutorial 3, we will provide a couple of helper files, so we can focus on the React implementation itself. What is special about these helpers though, is that they use a webworker. This ensures the UI thread isn't blocked during long computations like compiling a smart contract or proving a transaction.
This example uses an RPC endpoint. An in-browser Mina node is in the works, which will provide full node level security to your users, in the browser.
Project setup
First, setup a new project with
$ zk project 04-zkapp-browser-ui --ui next
On the prompts that appear, select:
- Do you want your NextJS project to use TypeScript? yes
- Do you want to setup your project for GitHub pages? yes
This will create a new project with two folders instead of just one:
- contracts: This stores your smart contract code
- ui: This is where you will write your ui
We will be using the default contract (Add), so all we'll be doing is building it for use from our ui.
To do this, enter the contracts folder, and run the build command:
$ cd contracts/
$ npm run build
In making your own zkApp, you will need to edit files in the contracts folder, and rebuild before accessing it from your UI.
Implementing the UI
Our React UI will have a few components. First, there is the react page itself. In addition though, there is code that uses SnarkyJS.
Because SnarkyJS code is computationally intensive, running it as usual in a script would block our browser's UI thread, causing our page to become unresponsive at times. To solve this, we will put our SnarkyJS code in a web worker.
This will be implemented via 2 helper files:
- zkappWorker.ts: The web worker code itself
- zkappWorkerClient.ts: Code that we run from react to interact with the web worker.
We encourage you to read these files, to understand how they work and to extend them for your own zkApp, but we will not review them in detail here, in favor of spending time on the react code.
Download these files from here and here, and place them in your ui/pages folder.
globals.css
We will be using an <a> link in our application - add the following to the end of ui/styles/globals.css to style the link:
a {
  color: blue;
}
Running the react app
To run the react app, open up two new terminals in the ui folder. In one, type:
$ npm run dev
And in the other,
$ npm run ts-watch
The first of these starts hosting your application, by default at localhost:3000. Your browser will refresh automatically when your page has changes.
The second shows typescript errors. You can watch this as you develop to check for type errors.
Implementing the react app
Open up ui/pages/_app.page.tsx in your editor. You can find the full contents for this file here for reference.
Edit the contents so it looks like this:
  1 import '../styles/globals.css'
  2 import { useEffect, useState } from "react";
  3 import './reactCOIServiceWorker';
  4
  5 import ZkappWorkerClient from './zkappWorkerClient';
  6
  7 import { PublicKey, Field } from 'snarkyjs';
  8
  9 let transactionFee = 0.1;
 10
 11 export default function App() {
 12   return <div/>
 13 }
 14
This just sets up our react project, with the imports we'll need, and an empty component.
Now, we'll add state to our app:
...
 11 export default function App() {
 12   let [state, setState] = useState({
 13     zkappWorkerClient: null as null | ZkappWorkerClient,
 14     hasWallet: null as null | boolean,
 15     hasBeenSetup: false,
 16     accountExists: false,
 17     currentNum: null as null | Field,
 18     publicKey: null as null | PublicKey,
 19     zkappPublicKey: null as null | PublicKey,
 20     creatingTransaction: false,
 21   });
 22
 23   // -------------------------------------------------------
 24
 25   return <div/>
...
This creates mutable state that we can reference in our UI, and update as our application runs. You can learn more about react state and useState here.
Next, we'll add a function to setup our application:
...
 23   // -------------------------------------------------------
 24   // Do Setup
 25
 26   useEffect(() => {
 27     (async () => {
 28       if (!state.hasBeenSetup) {
 29         const zkappWorkerClient = new ZkappWorkerClient();
 30
 31         console.log('Loading SnarkyJS...');
 32         await zkappWorkerClient.loadSnarkyJS();
 33         console.log('done');
 34
 35         await zkappWorkerClient.setActiveInstanceToBerkeley();
 36
 37         // TODO
 38       }
 39     })();
 40   }, []);
 41
 42   // -------------------------------------------------------
...
We use the react feature "useEffect", so that this is only run once. See more on useEffect here. We further gate running this effect by a boolean hasBeenSetup so we don't run this more than once.
This is also where we setup our web worker client. This will interact with our web worker running SnarkyJS code, so that that code doesn't block the UI thread.
...
 35         await zkappWorkerClient.setActiveInstanceToBerkeley();
 36
 37         const mina = (window as any).mina;
 38
 39         if (mina == null) {
 40           setState({ ...state, hasWallet: false });
 41           return;
 42         }
 43
 44         const publicKeyBase58: string = (await mina.requestAccounts())[0];
 45         const publicKey = PublicKey.fromBase58(publicKeyBase58);
 46
 47         console.log('using key', publicKey.toBase58());
 48
 49         console.log('checking if account exists...');
 50         const res = await zkappWorkerClient.fetchAccount({
 51           publicKey: publicKey!
 52         });
 53         const accountExists = res.error == null;
 54
 55         // TODO
 56       }
...
Continuing, we load our zkApp in the web worker. We load the contract, compile it, create a instance of it at a fixed address, and get its current state:
...
 53         const accountExists = res.error == null;
 54
 55         await zkappWorkerClient.loadContract();
 56
 57         console.log('compiling zkApp');
 58         await zkappWorkerClient.compileContract();
 59         console.log('zkApp compiled');
 60
 61         const zkappPublicKey = PublicKey.fromBase58(
 62           'B62qiqD8k9fAq94ejkvzaGEV44P1uij6vd6etGLxcR4dA8ZRZsxkwvR'
 63         );
 64
 65         await zkappWorkerClient.initZkappInstance(zkappPublicKey);
 66
 67         console.log('getting zkApp state...');
 68         await zkappWorkerClient.fetchAccount({ publicKey: zkappPublicKey })
 69         const currentNum = await zkappWorkerClient.getNum();
 70         console.log('current state:', currentNum.toString());
 71
 72         // TODO
 73       }
...
And lastly for this function, we update the state of the react app:
...
 70         console.log('current state:', currentNum.toString());
 71
 72         setState({
 73             ...state,
 74             zkappWorkerClient,
 75             hasWallet: true,
 76             hasBeenSetup: true,
 77             publicKey,
 78             zkappPublicKey,
 79             accountExists,
 80             currentNum
 81         });
 82       }
 83     })();
...
Now that we have finished setting up our UI, we write a new effect, that waits for the account to exist, if it didn't exist before.
If the account has been newly created for example, it will need to be funded from the faucet. We'll add in our UI later a link to request funds for new accounts.
...
 86   // -------------------------------------------------------
 87   // Wait for account to exist, if it didn't
 88
 89   useEffect(() => {
 90     (async () => {
 91       if (state.hasBeenSetup && !state.accountExists) {
 92         for (;;) {
 93           console.log('checking if account exists...');
 94           const res = await state.zkappWorkerClient!.fetchAccount({ 
 95             publicKey: state.publicKey!
 96           })
 97           const accountExists = res.error == null;
 98           if (accountExists) {
 99             break;
100           }
101           await new Promise((resolve) => setTimeout(resolve, 5000));
102         }
103         setState({ ...state, accountExists: true });
104       }
105     })();
106   }, [state.hasBeenSetup]);
107
108   // -------------------------------------------------------
...
Moving on, we'll create functions that are triggered when a button is pressed by a user.
First, a function that will send a transaction:
...
108   // -------------------------------------------------------
109   // Send a transaction
110
111   const onSendTransaction = async () => {
112     setState({ ...state, creatingTransaction: true });
113     console.log('sending a transaction...');
114
115     await state.zkappWorkerClient!.fetchAccount({
116       publicKey: state.publicKey!
117     });
118
119     await state.zkappWorkerClient!.createUpdateTransaction();
120
121     console.log('creating proof...');
122     await state.zkappWorkerClient!.proveUpdateTransaction();
123
124     console.log('getting Transaction JSON...');
125     const transactionJSON = await state.zkappWorkerClient!.getTransactionJSON()
126
127     console.log('requesting send transaction...');
128     const { hash } = await (window as any).mina.sendTransaction({
129       transaction: transactionJSON,
130       feePayer: {
131         fee: transactionFee,
132         memo: '',
133       },
134     });
135
136     console.log(
137       'See transaction at https://berkeley.minaexplorer.com/transaction/' + hash
138     );
139
140     setState({ ...state, creatingTransaction: false });
141   }
142
143   // -------------------------------------------------------
...
And second, a function that will get the latest zkApp state:
...
143   // -------------------------------------------------------
144   // Refresh the current state
145
146   const onRefreshCurrentNum = async () => {
147     console.log('getting zkApp state...');
148     await state.zkappWorkerClient!.fetchAccount({
149          publicKey: state.zkappPublicKey!
150     })
151     const currentNum = await state.zkappWorkerClient!.getNum();
152     console.log('current state:', currentNum.toString());
153
154     setState({ ...state, currentNum });
155   }
156
157   // -------------------------------------------------------...
Lastly, we will update the return <div/> placeholder we originally inserted, with a ui to show the user the state of our application:
...
157  // -------------------------------------------------------
158   // Create UI elements
159 
160   let hasWallet;
161   if (state.hasWallet != null && !state.hasWallet) {
162     const auroLink = 'https://www.aurowallet.com/';
163     const auroLinkElem = (
164       <a href={auroLink} target="_blank" rel="noreferrer">
165         {' '}
166         [Link]{' '}
167       </a>
168     );
169     hasWallet = (
170       <div>
171         {' '}
172         Could not find a wallet. Install Auro wallet here: {auroLinkElem}
173       </div>
174     );
175   }
176 
177   let setupText = state.hasBeenSetup
178     ? 'SnarkyJS Ready'
179     : 'Setting up SnarkyJS...';
180   let setup = (
181     <div>
182       {' '}
183       {setupText} {hasWallet}
184     </div>
185   );
186 
187   let accountDoesNotExist;
188   if (state.hasBeenSetup && !state.accountExists) {
189     const faucetLink =
190       'https://faucet.minaprotocol.com/?address=' + state.publicKey!.toBase58();
191     accountDoesNotExist = (
192       <div>
193         Account does not exist. Please visit the faucet to fund this account
194         <a href={faucetLink} target="_blank" rel="noreferrer">
195           {' '}
196           [Link]{' '}
197         </a>
198       </div>
199     );
200   }
201 
202   let mainContent;
203   if (state.hasBeenSetup && state.accountExists) {
204     mainContent = (
205       <div>
206         <button
207           onClick={onSendTransaction}
208           disabled={state.creatingTransaction}
209         >
210           {' '}
211           Send Transaction{' '}
212         </button>
213         <div> Current Number in zkApp: {state.currentNum!.toString()} </div>
214         <button onClick={onRefreshCurrentNum}> Get Latest State </button>
215       </div>
216     );
217   }
218 
219   return (
220     <div>
221       {setup}
222       {accountDoesNotExist}
223       {mainContent}
224     </div>
225   );
226 }
We divide our UI into 3 sections:
- setuplets the user know when the zkApp has finished loading.
- accountDoesNotExistgives the user a link to the faucet if their account hasn't been funded.
- mainContentshows the current state of the zkApp, and buttons to interact with it.
The buttons allow the user to create a transaction, or refresh the current state of the application, by triggering the onSendTransaction() and onRefreshCurrentNum() functions we wrote above.
And that's it! We've finished the code for our application.
If you've been using npm run dev, you should now be able to interact with this application on localhost:3000, with all the functionality we've implemented over the tutorial.
Deploying the UI to Github Pages
To deploy our project to Github, first push it to a new Github repo. The Github repo must have the same name as the project name when we ran zk project above (in this example, 04-zkapp-browser-ui). If not, change the existing project name strings in next.config.js, and pages/reactCOIServiceWorker.ts to your repo name.
To deploy, just run npm run deploy in the ui directory. After the script builds your application, uploads it to Github, and Github processes it, your application will be available at:
https://<username>.github.io/<repo-name>/index.html
Conclusion
We have built a React UI for our zkApp, to allow users to interact with our smart contract and send transactions to Berkeley Testnet!
You can build a UI for your application using any UI framework, not just React. The zkApp CLI will also soon provide support for simultaneously creating SvelteKit and NuxtJS projects too - stay tuned!
Checkout Tutorial 5 to learn about different SnarkyJS types you can use in your application.