Angular Integration Guide
Install a Wallet
- Install a wallet that supports zkApp transactions. For this tutorial, we’ll use Auro Wallet. Download it here.
- Add the Auro Wallet browser extension.
- Open the extension and follow the steps to create a new wallet.
- Click "Mainnet" at the top of the extension view, then select "Show Testnet" from the menu. After that, select "Devnet".
- Using Devnet will allow us to interact with a test version of the Mina network without needing to spend real Mina to pay for transaction fees.
- Fund your wallet using the Mina Faucet.
- You'll need to wait one block (~3 minutes) to see the change in balance reflected on chain. You can use Minascan to track the status of your transaction.
Initialize the Project
- Install the Angular CLI globally:
npm install -g @angular/cli
- Create a new Angular project by running:
ng new <project-name>
- Install the Angular CLI
npm install -g @angular/cli
- Create a new project
ng new <project-name>
- Select your preferred stylesheet format.
- For Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering), choose Yes.
- Install the
o1js
library:
cd <project-name>
npm install o1js
- Start the local development server.
- This command runs
ng serve
which we will further configure by changingoptions
under theserve
build target inangular.json
.
- This command runs
npm run start
Create the ZkApp Contract
- Navigate out of the demo project directory and install the
zkapp-cli
globally:
cd ../
npm install -g zkapp-cli
- Initialize a new zkapp with the CLI. When prompted to create a UI project, select none.
zk project add
- Change into the newly created
add
directory and build the contract:
cd add
npm run build
- We've already deployed an instance of the default
Add
contract to Devnet at B62qnTDEeYtBHBePA4yhCt4TCgDtA4L2CGvK7PirbJyX4pKH8bmtWe5 so you won't need to deploy the contract you just created. You'll still need to include the contract code in your project so that it can be compiled into a verification key and proving key.- The proving key enables users to generate proofs of valid contract execution directly in their browsers. A user can run a contract call locally, create a proof of its execution using the proving key, and then publish the proof on-chain to update the zkApp’s state. Since the verification key is stored on-chain, the network will accept a transaction sent to this address if it includes a proof generated with the proving key that matches the on-chain verification key.
Call Contracts
- Move back into the Angular project.
- To interact with the deployed
Add
contract, we’ll add code to fetch the current state and initiate a transaction. This code will execute only in the browser, so we'll add it toafterNextRender
in the constructor ofsrc/app/app.component.ts
.afterNextRender
only runs after the Angular component has fully rendered.- The Auro wallet injects a Mina provider into the global context. It is accessible as
window.mina
.
import {afterNextRender, Component} from '@angular/core';
import {RouterOutlet} from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'angular-o1js-demo';
constructor() {
afterNextRender(async () => {
const {Mina, PublicKey, fetchAccount} = await import('o1js');
const {Add} = await import('../../../add');
// connect the Mina instance to testnet
Mina.setActiveInstance(Mina.Network('https://api.minascan.io/node/devnet/v1/graphql'));
// we've already deployed the Add contract on testnet at this address
// https://minascan.io/devnet/account/B62qnTDEeYtBHBePA4yhCt4TCgDtA4L2CGvK7PirbJyX4pKH8bmtWe5
const zkAppAddress = `B62qnTDEeYtBHBePA4yhCt4TCgDtA4L2CGvK7PirbJyX4pKH8bmtWe5`;
await fetchAccount({publicKey: zkAppAddress});
const zkApp = new Add(PublicKey.fromBase58(zkAppAddress));
// Read state from the testnet Add contract
console.log(`Reading state of add contract at ${zkAppAddress}: num=${zkApp.num.get()}`);
try {
// retrieve the injected mina provider if it exists
const mina = (window as any).mina;
const walletKey: string = (await mina.requestAccounts())[0];
console.log(`Injected mina provider address: ${walletKey}`);
await fetchAccount({publicKey: PublicKey.fromBase58(walletKey)});
console.log("Compiling Add");
await Add.compile();
console.log("Compiled Add");
// send a transaction with the injected Mina provider
const transaction = await Mina.transaction(async () => {
await zkApp.update();
});
await transaction.prove();
const {hash} = await mina.sendTransaction({
transaction: transaction.toJSON(),
});
// display the link to the transaction
const transactionLink = `https://minascan.io/devnet/tx/${hash}`;
console.log(`View transaction at ${transactionLink}`);
} catch (e: any) {
console.error(e.message);
if (e.message.includes("Cannot read properties of undefined (reading 'requestAccounts')")) {
console.error("Is Auro installed?");
} else if (e.message.includes("Please create or restore wallet first.")) {
console.error("Have you created a wallet?");
} else if (e.message.includes("User rejected the request.")) {
console.error("Did you grant the app permission to connect to your wallet?");
} else {
console.error("An unknown error occurred:", e);
}
}
});
}
}
- The above code:
- Connects Mina to a Devnet node so transactions are broadcasted to Devnet.
- Requests the user's address from the mina provider injected into the browser context by the wallet.
- Compiles the
Add
zkApp contract to generate and cache the proving key, which will allow the app to create proofs for transactions. - Creates a zkapp transaction calling
update
on theAdd
contract. - Proves the transaction using the proving key which o1js has internally cached.
- Prompts the user to broadcast the transaction to the network with their wallet.
- Now run the application in your browser with
npm run start
(which executesng serve
) and open the browser console.- Approve the connection request displayed in Auro.
SharedArrayBuffer Headers for ng serve
- You'll see that some of the code works, like the on-chain state retrieval, but compiling a zkapp fails with
DataCloneError: Failed to execute 'postMessage' on 'Worker': SharedArrayBuffer transfer requires self.crossOriginIsolated
. SharedArrayBuffer
is a JavaScript object that lets different threads share memory. Since o1js's proving is very computationally intensive, we us WASM workers for parallel processing in the browser.- For security reasons,
SharedArrayBuffer
needs certain headers to be set. These prevent cross origin resources (scripts and content loaded from external domains, iframes, and popups) from accessing shared memory.- Cross-Origin-Opener-Policy (COOP) must be set to
"same-origin"
to prevents cross-origin resources from accessing the main document’s memory. - Cross-Origin-Embedder-Policy (COEP) must be set to
"require-corp"
to restrict the way cross origin resources can be loaded by the main document. They'll either need to be from the same origin or include theCross-Origin-Resource-Policy: cross-origin
header.
- Cross-Origin-Opener-Policy (COOP) must be set to
- Depending on how the application is being run, there are different ways to set these headers. Running the application locally with
ng serve
uses@angular-devkit/build-angular:dev-server"
which we can configure in the project'sangular.json
file at/projects/angular-o1js-demo/architect/serve/configurations/development
. - Architect is Angular's task runner, the entries (called build targets) under
architect
each represent tasks that the Angular CLI can run (ng build
,ng serve
,ng test
, etc). Thebuilder
property of each target specifies the program that Architect should run to execute the task. Theoptions
can be used to supply parameters to the builder, and theconfigurations
specifies a custom set of options for different target configurations (development, production, etc). - Running
ng serve
locally runs the@angular-devkit/build-angular:dev-server
builder, and in its options object we can specify custom headers specifying the headers required forSharedArrayBuffer
as follows:
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"headers": {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp"
}
},
- Restart the server with
npm run start
and view the application in the browser again - theSharedArrayBuffer
error should be gone!
Loading o1js
- We still have another error:
Uncaught ReferenceError: __async is not defined
. - This one comes from the way Angular bundles dependencies internally. We'll address it by supplying our own custom webpack config which will exclude o1js from the bundle generated by Angular. Then we'll copy o1js into our static assets directory so it's served with the app and use import maps to import o1js directly.
Create a Custom Webpack Config
- Start by creating a custom webpack config by adding a file
webpack.config.js
at the root of your project with the following contents:
module.exports = {
externals: {
'o1js': 'o1js'
}
};
Update Builders to Use Custom Webpack
- Install builders which support using custom webpack configs - Angular's default builder will ignore the webpack file.
npm i @angular-builders/custom-webpack
- Update the
serve
andbuild
build targets to use the@angular-builders/custom-webpack
builders and load the file.- In
angular.json
under/projects/angular-o1js-demo/architect/build
, replace the default builder"builder": "@angular-devkit/build-angular:application",
with"builder": "@angular-builders/custom-webpack:browser"
.- rename the browser property to
main
inoptions
. - add
"customWebpackConfig": { "path": "./webpack.config.js" },
tooptions
. - delete
server
,prerender
, andssr
fromoptions
.
- rename the browser property to
- In
angular.json
under/projects/angular-o1js-demo/architect/serve
, replace the default builder"builder": "@angular-devkit/build-angular:dev-server",
with"builder": "@angular-builders/custom-webpack:dev-server"
.
- In
- The build targets should look like this:
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
"path": "./webpack.config.js"
},
"main": "src/main.ts",
"outputPath": "dist/<project-name>",
"index": "src/index.html",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kB",
"maximumError": "4kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-builders/custom-webpack:dev-server",
"configurations": {
"production": {
"buildTarget": "<project-name>:build:production"
},
"development": {
"buildTarget": "<project-name>:build:development"
}
},
"options": {
"headers": {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp"
}
},
"defaultConfiguration": "development"
},
Copy o1js into Static Assets
- Now we'll write a script to copy o1js into
public
where our static assets are served along with the application and then import it directly with animportmap
. - Add a script to
package.json
that copies o1js fromnode_modules
to a new directory atpublic/lib/o1js
.- Files under public are served with the app, so the file itself will be available at
http://localhost:4200/lib/o1js/index.js
.
- Files under public are served with the app, so the file itself will be available at
"copy-libs": "mkdir -p public/lib/o1js && cp node_modules/o1js/dist/web/index.js public/lib/o1js/index.js"
- Add the
copy-o1js-lib
task to the build script and the start script."build": "npm run copy-libs && ng build"
"start": "npm run copy-libs && ng serve"
- Add
public/lib
to.gitignore
.
Load o1js with an importmap
- Above the closing
</head>
tag insrc/index.html
add these scripts to import o1js frompublic/lib/o1js
:
...
<script type="importmap">
{ "imports": { "o1js": "./lib/o1js/index.js" } }
</script>
<script type="module">
import * as o1js from "o1js";
window.o1js = o1js;
console.log("Set the global o1js instance: ", window.o1js);
</script>
</head>
- Now instead of importing o1js as a regular npm dependency, we declare it as a top level variable in app component knowing that it will exist in the global context of the browser at runtime. Add the following to the top of
src/app.component.ts
:
// at the top of the file:
import type * as o1jsTypes from 'o1js';
declare var o1js: typeof o1jsTypes;
- Remove the import of o1js inside of
afterNextRender
and replace it with this:
const {Mina, PublicKey, fetchAccount} = o1js;
Running the App Locally
- Congratulations! The app should work as expected when served with
npm run start
(ng serve
). - Restart the application with
npm run start
. - Verify that
Set the global o1js instance: Module {…}
was logged in the console, indicating that o1js was successfully loaded from our public assets. - Verify that the current
num
on theAdd
zkapp is logged, meaning that we're successfully reading state from the contract atB62qnTDEeYtBHBePA4yhCt4TCgDtA4L2CGvK7PirbJyX4pKH8bmtWe5
on Devnet. - Verify that "Compiled Add" is logged, meaning that the SDK has successfully generated a proving key for the
Add
zkapp. - If you're connected to Devnet and your account is funded with Devnet tokens, you should be be able to broadcast a successful transaction calling
update
on the zkapp. After a few minutes, the state change associated with the transaction will take effect on chain and you'll seenum
increase when you reload the page!
Deploying to GitHub Pages
- Now we'll set the app up for deployment to GitHub pages.
- Publish your project to a GitHub repository.
- Run
ng deploy
and select GitHub Pages. - Add
baseHref
tooptions
underbuild
in angular.json with the name of your GitHub repository.- Do not remove the slashes!
"baseHref": "/<your-github-repo-name>/"
- Create a deploy script in package.json which copies the required libraries
"deploy": "npm run copy-libs && ng deploy --dir=dist/<project-name>"
- Deploy the app.
npm run deploy
- You can view deployment details at
https://github.com/<username>/<repo-name>/actions
and your live site athttps://<username>.github.io/<repo-name>/
.
SharedArrayBuffer in Deployed Instance
- View the site and open the browser console. You'll see the same error about the SharedArrayBuffer from before! The headers set previously apply only to
ng serve
, so we’ll set them up for GitHub Pages. - Install
coi-serviceworker
.
npm i coi-serviceworker
- Update the script that copies
o1js
topublic
to also include thecoi-serviceworker
file:
"copy-libs": "mkdir -p public/lib/o1js && cp node_modules/o1js/dist/web/index.js public/lib/o1js/index.js && cp node_modules/coi-serviceworker/coi-serviceworker.min.js public/lib/coi-serviceworker.min.js"
- Create a file
src/app/COIServiceWorker.ts
with the following contents.- Be sure to replace
<your repo name>
with the name of your GitHub repo!
- Be sure to replace
export {};
function loadCOIServiceWorker() {
console.log('Load COIServiceWorker', navigator);
if (typeof window !== 'undefined' && 'serviceWorker' in navigator && window.location.hostname != 'localhost') {
navigator.serviceWorker.register('/<your repo name>/lib/coi-serviceworker.min.js')
.then(registration => console.log('COI Service Worker registered with scope:', registration.scope))
.catch(error => console.error('COI Service Worker registration failed:', error));
}
}
loadCOIServiceWorker();
- Import it at the top of
src/app/app.component.ts
.
import './COIServiceWorker';
- Redeploy the application with the
COIServiceWorker
files.
npm run deploy
Congratulations, you’ve developed and deployed a zkApp UI with Angular!
Next steps include learning to use web workers to prevent computationally expensive operations like compile
from blocking the UI thread, handling events, and building more complex zkApp contracts!