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
The npm link
command allows you to use your smart contract within your UI project during local development without having to publish it to npm. This local use allows for rapid development. See the npm link reference docs.
- At the command line, change into your smart contract project directory using
cd <your-project>
and run thenpm link
command. - At the command line, change into your UI project directory using
cd <your-ui-project>
and run thenpm link <your-package-name>
command.your-package-name
is thename
property used in your smart contract'spackage.json
. - Import the smart contracts into your UI project, using
import { YourSmartContract } from 'your-package-name';
.
After making changes, remember to run npm run build
in your smart contract directory so that the changes are reflected in the smart contract consumed by your UI project.
Publish to npm for production
- Create an npm account. If you don't have an account yet, go to npm Sign Up.
- Login to npm.
To sign in, run
npm login
on the command line. When prompted,enter your username, password, and email address. - Publish your package.
At the root of your smart contract project directory, run
npm publish
. An error occurs if the package name already exists. To change the package name, change thename
property in yourpackage.json
.
To check if a package name exists 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.
- Run
npm install your-package-name
from the root of your UI project directory. - If you published a scoped npm package, run
npm install @your-username/your-project-name
.
- Import your smart contract package into the UI using:where
import { YourSmartContract } from ‘your-package-name’;
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
}
}