Please note that zkApp programmability is not yet available on Mina Mainnet, but zkApps can now be deployed to Berkeley Testnet.
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 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.
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.
To change into your smart contract project directory:
cd <your-project>
To create the symlinks:
npm link <your-package-name>
where
your-package-name
is thename
property used in your smart contract'spackage.json
.To import the smart contracts into your UI project, add the import statement to the
index.ts
file:import { YourSmartContract } from 'your-package-name';`
After making changes, 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
Create an npm account.
If you don't already have an account, go to npm Sign Up.
Login to npm:
npm login
When prompted, enter your username, password, and email address.
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.
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
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.
For a more performant UI, render your UI before importing and loading your smart contract so the SnarkyJS 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 SnarkyJS.
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 SnarkyJS code in your UI, you must set the COOP and COEP headers.
These headers enable SnarkyJS to use SharedArrayBuffer that SnarkyJS relies on to enable important WebAssembly (Wasm) features.
- Set
Cross-Origin-Opener-Policy
tosame-origin
. - Set
Cross-Origin-Embedder-Policy
torequire-corp
.
You can enable these headers 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.
Install the Chrome extension for Auro Wallet.
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:
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(() => {
const YourSmartContractInstance = new YourSmartContract(zkAppAddress);
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);
}A best practice is to show the error message in your UI.
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 and display a helpful message for the user in your UI.
Use a try-catch statement to catch exceptions when a user invokes a method on your smart contract.
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, consider setting custom error messages for your assertions while writing the smart contract. For example:
INSUFFICIENT_BALANCE
.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
}
}