Skip to main content

How to Write a zkApp UI

A zkApp consists of a smart contract and a UI to interact with it. To allow users to interact with your smart contract in a web browser, you typically want to build a website UI.

You can write the UI with any framework like React, Vue, or Svelte, or with plain HTML and JavaScript.

Using one of the provided UI framework scaffolds

When you create a project using the zkApp CLI, you can choose a supported UI framework to be scaffolded as a part of your zkApp project. For example, Next.js, Sveltkit, or Nuxt.js.

Adding your smart contract as a dependency of the UI

You can use one of the provided scaffolding options and add your smart contract to an existing frontend, a different UI framework, or a plain HTML and JavaScript website.

Specify the smart contracts to import

The index.ts file is the entry point of your project that imports all smart contract classes you want access to and exports them to your smart contract. This pattern allows you to specify which smart contracts are available to import when consuming your project from npm within your UI.

In index.ts:

import { YourSmartContract } from './YourSmartContract.js';

export { YourSmartContract };

Local development

To test iteratively and use your smart contract within your UI project during local development, you can use npm link. This local use allows for rapid development without having to publish your project to npm.

  1. To change into your smart contract project directory:

    cd <your-project>
  2. To create the symlinks:

    npm link <your-package-name>

    where your-package-name is the name property used in your smart contract's package.json.

    For example, the name property in package.json for the sudoku example project that you created in How to Write a zkApp is sudoku.

    To create the symlinks for sudoku:

    npm link sudoku
  3. To import the smart contracts into your UI project, add the import statement to the index.ts file:

    import { YourSmartContract } from 'your-package-name';`

    For example, to import the sudoku example project, your index.ts file is:

    import { SudokuZkApp } from './sudoku.js';

    export { SudokuZkApp };
  4. After you make changes to your project files, be sure to build your project so that the changes are reflected in the smart contract consumed by your UI project:

    npm run build

Publish to npm for production

  1. Create an npm account.

    If you don't already have an account, go to npm Sign Up.

  2. Login to npm:

    npm login

    When prompted, enter your username, password, and email address.

  3. To publish your package from the root of your smart contract project directory:

    npm publish

Package names must be unique. An error occurs if the package name already exists. To use a different package name, change the name property in the package.json file.

To check existing package names on npm, use the npm search command.

To avoid naming collisions, npm allows you to publish scoped packages: @your-username/your-package-name. See Introduction to packages and modules in the npm reference docs.

Consuming your smart contract in your UI

After you have published your smart contract to npm, you can add it to any UI framework by importing the package.

  1. Install your smart contract package from the root of your UI project directory:

    npm install your-package-name

    If you published a scoped npm package:

    npm install @your-username/your-project-name
  2. Import your smart contract package into the UI using:

    import { YourSmartContract } from ‘your-package-name’;

    where YourSmartContract is the named export that you chose in your smart contract.

tip

For a more performant UI, render your UI before importing and loading your smart contract so the o1js wasm workers can perform initialization without blocking the UI.

For example, if your UI is built using React, instead of a top level import, load the smart contract in a useEffect to give the UI time to render its components before loading o1js.

Loading your contract with React

useEffect(() => {
(async () => {
const { YourSmartContract } = await import('your-package-name');
})();
}, []);

Loading your contract with Svelte

onMount(async () => {
const { YourSmartContract } = await import('your-package-name');
});

Loading your contract with Vue

onMounted(async () => {
const { YourSmartContract } = await import('your-package-name');
});

Enabling COOP and COEP headers

To load o1js code in your UI, you must set the COOP and COEP headers.

These headers enable o1js to use SharedArrayBuffer that o1js relies on to enable important WebAssembly (Wasm) features.

  • Set Cross-Origin-Opener-Policy to same-origin.
  • Set Cross-Origin-Embedder-Policy to require-corp.

You can enable these headers in a number of different ways. If you deploy your UI to a host such as Vercel or Cloudflare Pages, you can set these headers in a custom configuration file. Otherwise, set these headers in the server framework of your choice (for example, Express for JavaScript).

Set headers for Vercel

If your app will be hosted on Vercel, set the headers in vercel.json.

{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }
]
}
]
}

Set headers for Cloudflare Pages

To host your app on Cloudflare Pages, set the headers in a _headers file.

/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Connecting your zkApp with a user's wallet

The Mina community has created a variety of different wallets. Only the Auro Wallet for Chrome supports interactions with zkApps.

To interact with your zkApp, users of your zkApp must have the Auro Wallet installed:

  • window.mina is automatically available in the user's browser environment.
  • Your zkApp uses this object to interact with the wallet.
  1. Install the Chrome extension for Auro Wallet.

  2. Get accounts.

    To fetch a user's list of Mina accounts, use the requestAccounts() method:

    let accounts;

    try {
    // Accounts is an array of string Mina addresses.
    accounts = await window.mina.requestAccounts();

    // Show first 6 and last 4 characters of user's Mina account.
    const display = `${accounts[0].slice(0, 6)}...${accounts[0].slice(-4)}`;
    } catch (err) {
    // If the user has a wallet installed but has not created an account, an
    // exception will be thrown. Consider showing "not connected" in your UI.
    console.log(err.message);
    }

    It is useful to indicate if the user's wallet is successfully connected to your zkApp:

  3. Send a transaction.

    After your user interacts with your zkApp, you can sign and send the transaction using sendTransaction(). You receive a transaction ID as soon as the Mina network has received the proposed transaction. However, this does not guarantee that the transaction is accepted in the network into an upcoming block.

    try {
    // This is the public key of the deployed zkapp you want to interact with.
    const zkAppAddress = 'B62qq8sm7JdsED6VuDKNWKLAi1Tvz1jrnffuud5gXMq3mgtd';

    const tx = await Mina.transaction(async () => {
    const YourSmartContractInstance = new YourSmartContract(zkAppAddress);
    await YourSmartContractInstance.foo();
    });

    await tx.prove();

    const { hash } = await window.mina.sendTransaction({
    transaction: tx.toJSON(),
    feePayer: {
    fee: '',
    memo: 'zk',
    },
    });

    console.log(hash);
    } catch (err) {
    // You may want to show the error message in your UI to the user if the transaction fails.
    console.log(err.message);
    }

    The convention is to show the error message in your UI.

info

For details about the Mina Provider API, see the Mina Provider API Reference docs.

Display assertion exceptions in your UI

If an assertion exception occurs while a user interacts with any of your smart contract methods, you want to capture this error and display a helpful message for the user in your UI.

  1. Use a try-catch statement to catch exceptions when a user invokes a method on your smart contract.

  2. Use a switch-case statement to identify which exception was thrown. Add a matching case for each unique assertion within your method. To assist with this error handling, consider setting custom error messages for your assertions while writing the smart contract. For example: INSUFFICIENT_BALANCE.

  3. Display a helpful error message for the user within your UI, like:

    try {
    YourSmartContract.yourMethod();
    } catch (err) {
    let uiErrorMessage;
    switch (err.message) {
    // A custom error thrown within YourSmartContract.yourMethod()
    // when there is an insufficient balance.
    case 'INSUFFICIENT_BALANCE':
    // Set a helpful message to show the user in the UI.
    uiErrorMessage =
    'Your account has an insufficient balance for this transaction';
    break;
    // etc
    }
    }