Skip to main content

Tutorial 4: Build a zkApp UI in the Browser with React

info

zkApp programmability is not yet available on the Mina Mainnet, but zkApps can now be deployed on Berkeley Testnet.

You're making excellent progress in your zkApp journey:

In this tutorial, you implement a browser UI using React.js that interacts with a smart contract.

Prerequisites

  • Make sure you have the latest version of the zkApp CLI installed:

    $ npm install -g zkapp-cli
  • Ensure your environment meets the Prerequisites for zkApp Developer Tutorials.

  • The Auro Wallet browser extension wallet that supports interactions with zkApps. See Install a Wallet and create a MINA account.

This tutorial has been tested with:

Use the working application first

Before you go through the tutorial steps, take a look at a working zkApp UI example that has already been deployed to GitHub Pages.

  1. In a Chrome web browser with the Auro wallet extension, go to https://ymekuria.github.io/04-zkapp-browser-ui/index.html.
  2. When prompted, select Connect to let the website view your Auro wallet account.
  3. In network dropdown menu in the wallet UI, select the Berkeley network:
    Select Berkeley network in the wallet
  4. Select Get Latest State to view the state of the zkApp.
  5. Next, select Send Transaction.
  6. In your Auro Wallet, select Confirm to send the transaction.

High-Level Overview

In this tutorial, you create a new GitHub repository so you can deploy the UI to GitHub Pages.

You use example code and the zkApp CLI to build an application that:

  1. Loads a public key from an extension-based wallet.
  2. Checks if the public key has funds and if not, directs the user to the faucet.
  3. Connects to the example zkApp Add smart contract that is already deployed on Berkeley Testnet at a fixed address.
  4. Implements a button that sends a transaction.
  5. Implements a button that requests the latest state of the smart contract.
  6. Deploys the zkApp to GitHub Pages.

Like previous tutorials, you use the provided example files so you can focus on the React implementation itself.

This example uses an RPC endpoint.

Create a project

You can have the zk project command scaffold the UI for your project.

  1. Create or change to a directory where you have write privileges.

  2. Create a project by using the zk project command:

    $ zk project 04-zkapp-browser-ui

    To scaffold the UI for your project with the Next.js React framework, select next:

      ? Create an accompanying UI project too? …
    ❯ next
    svelte
    nuxt
    empty
    none
  3. If you are prompted to install the required Next packages, press y to proceed.

  4. Select yes at the ? Do you want to set up your project for deployment to Github Pages? … prompt.

  5. If you are prompted to install the required Next packages, press y to proceed.

  6. Select Yes at the ? Would you like to use TypeScript with this project? prompt.

  7. Select No at the ? Would you like to use ESLint with this project? prompt.

  8. Select No at the ? Would you like to use Tailwind CSS with this project? prompt.

    Your UI is created in the project directory: /04-zkapp-browser-ui/ui with two directories:

    • contracts: The smart contract code
    • ui: Where you write your UI code

    The expected output is:

    ➜  source zk project 04-zkapp-browser-ui --ui next
    ✔ Do you want to set up your project for deployment to GitHub Pages? · yes
    ✔ Would you like to use TypeScript with this project? … No / Yes
    ✔ Would you like to use ESLint with this project? … No / Yes
    ✔ Would you like to use Tailwind CSS with this project? … No / Yes
    Creating a new Next.js app in <yourfilepath>/04-zkapp-browser-ui/ui.

    Using npm.

    Initializing project with template: default

For this tutorial, you run commands from the root of the 04-zkapp-browser-ui/ui directory. You work in the ui/src/pages directory on TypeScript files that contain the UI code.

Each time you make updates, then build or deploy, the TypeScript code is compiled into JavaScript in the build directory.

Install the dependencies

When you ran the zk project command, your UI was created in the project directory: /04-zkapp-browser-ui/ui. The project has two sub-directories:

  • contracts: The smart contract code
  • ui: Where you write your UI code

The dependencies in each sub-directory are installed automatically by the zkApp-cli.

Create a repository

To interact with a deployed zkApp UI on GitHub pages, you must create a GitHub repository.

Go ahead and create your repository now. For other projects, you can name your GitHub repository anything you want. For this tutorial, use 04-zkapp-browser-ui.

  1. Go to https://github.com/new.
  2. For the Repository name, enter 04-zkapp-browser-ui.
  3. Optionally, add a description and a README.

Your project repository is ready to use.

Project structure

For all projects, you run zk commands from the root of your project directory.

For this tutorial, run the UI commands from the local /04-zkapp-browser-ui/ui/ directory.

Files that contain the UI code are in the /04-zkapp-browser-ui/ui/src/pages directory.

Preparing the project

Start by deleting the default index.page.tsx file that comes with a new project so that you have a clean project to work with.

  1. In the /04-zkapp-browser-ui/ui/src/pages directory:

    $ rm index.page.tsx

Build the default contract

This tutorial uses the default contract Add that is always scaffolded with the zk project command.

To build the default contract to use in the UI, run this command from the /04-zkapp-browser-ui/contracts directory:

$ npm run build

The expected output is:

> [email protected] build
> tsc

Outside of this tutorial, the workflow for building your own zkApp is to edit files in the contracts folder, rebuild the contract, and then access it from your UI.

Implement the UI

The React UI has several components: the React page itself and the code that uses o1js.

Download helper files

Because o1js code is computationally intensive, it's helpful to use web workers. A web worker handles requests from users to ensure the UI thread isn't blocked during long computations like compiling a smart contract or proving a transaction.

  1. Download the helper files from the examples/zkapps/04-zkapp-browser-ui directory on GitHub:

  2. Move the files to your local /04-zkapp-browser-ui/ui/src/pages directory.

  3. Review each helper file to see how they work and how you can extend them for your own zkApp.

    • zkappWorker.ts is the web worker code
    • zkappWorkerClient.ts is the code that is run from React to interact with the web worker

Download the main browser UI logic file

The example project has a completed app. The index.page.tsx file is the entry file for your application and contains the main logic for the browser UI that is ready to deploy to GitHub Pages.

  1. Download the index.page.tsx example file.

  2. Move the index.page.tsx file to your local /04-zkapp-browser-ui/ui/src/pages directory.

Import and export statements

First, review the import and export statements:

  • The import statements set up your React project with the required imports.
  • The export statement creates a placeholder empty component.
import { useEffect, useState } from 'react';
import './reactCOIServiceWorker';
import ZkappWorkerClient from './zkappWorkerClient';
import { PublicKey, Field } from 'o1js';
import GradientBG from '../components/GradientBG.js';
import styles from '../styles/Home.module.css';

let transactionFee = 0.1;
const ZKAPP_ADDRESS = 'B62qo2Be4Udo5EG1ux9yMJVkXe9Gz945cocN7Bn4W9DSYyeHZr1C3Ea';

export default function Home() {
return <div/>
}
info

The smart contract that the UI interacts with in this tutorial has been deployed to the Berkeley network and the public key is stored as the ZKAPP_ADDRESS in the UI project. If you experience problems with the deployed contract, you can deploy the Add contract included in the contracts folder yourself to the Berkeley network. When deployed, replace ZKAPP_ADDRESS with the public key of your deployment.

Add state

This export statement creates mutable state that you can reference in the UI. The state updates as the application runs:

...
export default function Home() {
const [state, setState] = useState({
zkappWorkerClient: null as null | ZkappWorkerClient,
hasWallet: null as null | boolean,
hasBeenSetup: false,
accountExists: false,
currentNum: null as null | Field,
publicKey: null as null | PublicKey,
zkappPublicKey: null as null | PublicKey,
creatingTransaction: false
});

const [displayText, setDisplayText] = useState('');
const [transactionlink, setTransactionLink] = useState('');
...

To learn more about useState Hooks, see Built-in React Hooks in the React API Reference documentation.

Add a function

This code adds a function to set up the application:

  • The Boolean hasBeenSetup ensures that the react feature useEffect is run only once. To learn more about useEffect Hooks, see useEffect in the React API Reference documentation.

  • This code also sets up your web worker client that interacts with the web worker running o1js code to ensure the computationally heavy o1js code doesn't block the UI thread.

...
// -------------------------------------------------------
// Do Setup

useEffect(() => {
async function timeout(seconds: number): Promise<void> {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, seconds * 1000);
});
}

(async () => {
if (!state.hasBeenSetup) {
setDisplayText('Loading web worker...');
console.log('Loading web worker...');
const zkappWorkerClient = new ZkappWorkerClient();
await timeout(5);

setDisplayText('Done loading web worker');
console.log('Done loading web worker');

await zkappWorkerClient.setActiveInstanceToBerkeley();

// TODO
}
})();
}, []);

// -------------------------------------------------------
...

Load the zkApp in the web worker

This code loads the contract and compiles it:

...
await zkappWorkerClient.setActiveInstanceToBerkeley();

const mina = (window as any).mina;

if (mina == null) {
setState({ ...state, hasWallet: false });
return;
}

const publicKeyBase58: string = (await mina.requestAccounts())[0];
const publicKey = PublicKey.fromBase58(publicKeyBase58);

console.log(`Using key:${publicKey.toBase58()}`);
setDisplayText(`Using key:${publicKey.toBase58()}`);

setDisplayText('Checking if fee payer account exists...');
console.log('Checking if fee payer account exists...');

const res = await zkappWorkerClient.fetchAccount({
publicKey: publicKey!
});
const accountExists = res.error == null;

// TODO
}
...

Create an instance

This code creates an instance of the contract at a fixed address and gets its current state:

...
const accountExists = res.error == null;

await zkappWorkerClient.loadContract();

console.log('Compiling zkApp...');
setDisplayText('Compiling zkApp...');
await zkappWorkerClient.compileContract();
console.log('zkApp compiled');
setDisplayText('zkApp compiled...');

const zkappPublicKey = PublicKey.fromBase58(ZKAPP_ADDRESS);

await zkappWorkerClient.initZkappInstance(zkappPublicKey);

console.log('Getting zkApp state...');
setDisplayText('Getting zkApp state...');
await zkappWorkerClient.fetchAccount({ publicKey: zkappPublicKey });
const currentNum = await zkappWorkerClient.getNum();
console.log(`Current state in zkApp: ${currentNum.toString()}`);
setDisplayText('');

// TODO
...

Update the state of the React app

And finally, this function updates the state of the React app:

...
setState({
...state,
zkappWorkerClient,
hasWallet: true,
hasBeenSetup: true,
publicKey,
zkappPublicKey,
accountExists,
currentNum
});
}
})();
}, []);
...

Run the React app

To run the React app, run commands from your local /04-zkapp-browser-ui/ui/ directory.

  1. To start hosting your application at the localhost:3000 default location:

    $ npm run dev

    The zkApp UI in the web browser shows the current state of the zkApp and has buttons to send a transaction and get the latest state.

    Your browser refreshes automatically when your page has changes.

  2. If prompted, request funds from the Testnet Faucet to fund your fee payer account.

    Follow the prompts to request tMINA. For this tutorial, your MINA address is populated on the Testnet Faucet. tMINA arrives at your address when the next block is produced (~3 minutes).

  3. And in the second terminal window:

    $ npm run ts-watch

    This command shows TypeScript errors. As you develop your application, you can watch this window to check for type errors.

Write a new effect

Now that the UI setup is finished, a new effect waits for the account to exist if it didn't exist before.

If the account has been newly created, it must be funded from the faucet.

Later, you add a link in the UI to request funds for new accounts.

...
// Wait for account to exist, if it didn't

useEffect(() => {
(async () => {
if (state.hasBeenSetup && !state.accountExists) {
for (;;) {
setDisplayText('Checking if fee payer account exists...');
console.log('Checking if fee payer account exists...');
const res = await state.zkappWorkerClient!.fetchAccount({
publicKey: state.publicKey!
});
const accountExists = res.error == null;
if (accountExists) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 5000));
}
setState({ ...state, accountExists: true });
}
})();
}, [state.hasBeenSetup]);

// -------------------------------------------------------
...

Create functions for UI buttons

Functions can be triggered when a button is pressed by a user.

First, code for a function that sends a transaction:

...
// Send a transaction

const onSendTransaction = async () => {
setState({ ...state, creatingTransaction: true });

setDisplayText('Creating a transaction...');
console.log('Creating a transaction...');

await state.zkappWorkerClient!.fetchAccount({
publicKey: state.publicKey!
});

await state.zkappWorkerClient!.createUpdateTransaction();

setDisplayText('Creating proof...');
console.log('Creating proof...');
await state.zkappWorkerClient!.proveUpdateTransaction();

console.log('Requesting send transaction...');
setDisplayText('Requesting send transaction...');
const transactionJSON = await state.zkappWorkerClient!.getTransactionJSON();

setDisplayText('Getting transaction JSON...');
console.log('Getting transaction JSON...');
const { hash } = await (window as any).mina.sendTransaction({
transaction: transactionJSON,
feePayer: {
fee: transactionFee,
memo: ''
}
});

const transactionLink = `https://berkeley.minaexplorer.com/transaction/${hash}`;
console.log(`View transaction at ${transactionLink}`);

setTransactionLink(transactionLink);
setDisplayText(transactionLink);

setState({ ...state, creatingTransaction: false });
};

// -------------------------------------------------------

And second, code for a function that gets the latest zkApp state:

...
// Refresh the current state

const onRefreshCurrentNum = async () => {
console.log('Getting zkApp state...');
setDisplayText('Getting zkApp state...');

await state.zkappWorkerClient!.fetchAccount({
publicKey: state.zkappPublicKey!
});
const currentNum = await state.zkappWorkerClient!.getNum();
setState({ ...state, currentNum });
console.log(`Current state in zkApp: ${currentNum.toString()}`);
setDisplayText('');
};

// -------------------------------------------------------
...

Update placeholder

Replace the <div/> placeholder with a UI to show the user the state of your application:

...
// Create UI elements

let hasWallet;
if (state.hasWallet != null && !state.hasWallet) {
const auroLink = 'https://www.aurowallet.com/';
const auroLinkElem = (
<a href={auroLink} target="_blank" rel="noreferrer">
Install Auro wallet here
</a>
);
hasWallet = <div>Could not find a wallet. {auroLinkElem}</div>;
}

const stepDisplay = transactionlink ? (
<a href={displayText} target="_blank" rel="noreferrer">
View transaction
</a>
) : (
displayText
);

let setup = (
<div
className={styles.start}
style={{ fontWeight: 'bold', fontSize: '1.5rem', paddingBottom: '5rem' }}
>
{stepDisplay}
{hasWallet}
</div>
);

let accountDoesNotExist;
if (state.hasBeenSetup && !state.accountExists) {
const faucetLink =
'https://faucet.minaprotocol.com/?address=' + state.publicKey!.toBase58();
accountDoesNotExist = (
<div >
<span style={{paddingRight: '1rem'}}>Account does not exist.</span>
<a href={faucetLink} target="_blank" rel="noreferrer">
Visit the faucet to fund this fee payer account
</a>
</div>
);
}

let mainContent;
if (state.hasBeenSetup && state.accountExists) {
mainContent = (
<div style={{ justifyContent: 'center', alignItems: 'center' }}>
<div className={styles.center} style={{ padding: 0 }}>
Current state in zkApp: {state.currentNum!.toString()}{' '}
</div>
<button
className={styles.card}
onClick={onSendTransaction}
disabled={state.creatingTransaction}
>
Send Transaction
</button>
<button className={styles.card} onClick={onRefreshCurrentNum}>
Get Latest State
</button>
</div>
);
}

return (
<GradientBG>
<div className={styles.main} style={{ padding: 0 }}>
<div className={styles.center} style={{ padding: 0 }}>
{setup}
{accountDoesNotExist}
{mainContent}
</div>
</div>
</GradientBG>
);
}

The UI has three sections:

  • setup lets the user know when the zkApp has finished loading.

  • accountDoesNotExist gives the user a link to the faucet if their account hasn't been funded.

  • mainContent shows the current state of the zkApp and buttons to interact with it.

    The buttons allow the user to create a transaction and refresh the current state of the application by triggering the onSendTransaction() and onRefreshCurrentNum() functions shown in the code.

That's it for the code review.

If you've been using npm run dev, you can now interact with the application on localhost:3000. The application has all of the functionality that is implemented in this tutorial.

Deploying the UI to GitHub Pages

Before you can deploy your project to GitHub Pages, you must push it to a new GitHub repository.

  • The GitHub repo must have the same name as the project name.
  • In this tutorial, the project name is 04-zkapp-browser-ui.
  • The zk project command created the correct project name strings for 04-zkapp-browser-ui in the next.config.js and src/pages/reactCOIServiceWorker.ts files.

To deploy the UI:

  1. Change to the local /04-zkapp-browser-ui/ui/ directory.

  2. Run the deploy command:

    npm run deploy

The scripts defined in the /04-zkapp-browser-ui/ui/package.json file do the work to build your application and upload it to GitHub.

After the processing is complete, your deployed zkApp is available at:

https://<username>.github.io/04-zkapp-browser-ui/

where <username> is your GitHub username.

Conclusion

Congratulations! You built a React UI for your zkApp. The UI allows users to interact with your 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 also supports SvelteKit and NuxtJS projects.

You are ready to continue with Tutorial 5: Common Types and Functions to learn about different o1js types you can use in your application.