This tutorial demonstrates how to build a browser-based wallet application for Aztec using React and Vite. It showcases creating wallets, deploying contracts, and interacting with the Aztec network directly from a web browser.
This example is compatible with Aztec v5.0.0-rc.1.
To set this version:
aztec-up 5.0.0-rc.1A web application that demonstrates three core Aztec operations:
- Create Wallet - Initialize an EmbeddedWallet with a Schnorr account
- Deploy Contract - Deploy the PrivateVoting contract to Aztec
- Cast Vote - Interact with the deployed contract
- Node.js v18 or higher
- Yarn package manager
- A running Aztec sandbox instance
yarn installaztec start --local-networkThe local network must be running on http://localhost:8080 before starting the app.
yarn devOpen your browser to http://localhost:5173 and click through the steps.
If you want to build this project from scratch, follow these steps:
yarn create vite testwallet-webapp-tutorial-new --template react-ts
cd testwallet-webapp-tutorial-new
yarn installyarn add @aztec/accounts@5.0.0-rc.1 \
@aztec/aztec.js@5.0.0-rc.1 \
@aztec/wallets@5.0.0-rc.1 \
@aztec/noir-contracts.js@5.0.0-rc.1yarn add -D vite-plugin-node-polyfills @types/nodeReplace the default vite.config.ts with:
import { defineConfig, searchForWorkspaceRoot } from 'vite'
import react from '@vitejs/plugin-react'
import { nodePolyfills, type PolyfillOptions } from "vite-plugin-node-polyfills";
const nodeModulesPath = `${searchForWorkspaceRoot(process.cwd())}/node_modules`;
// Workaround for vite-plugin-node-polyfills module resolution bug
// See: https://github.com/davidmyersdev/vite-plugin-node-polyfills/issues/81
const nodePolyfillsFix = (options?: PolyfillOptions | undefined) => {
return {
...nodePolyfills(options),
resolveId(source: string) {
const m =
/^vite-plugin-node-polyfills\/shims\/(buffer|global|process)$/.exec(
source,
);
if (m) {
return `${nodeModulesPath}/vite-plugin-node-polyfills/shims/${m[1]}/dist/index.cjs`;
}
},
};
};
export default defineConfig({
server: {
// Headers needed for bb WASM to work in multithreaded mode
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
},
},
plugins: [
react(),
nodePolyfillsFix({
include: ["buffer", "path", "process", "net", "tty"],
}),
],
optimizeDeps: {
include: ['pino', 'pino/browser'],
exclude: ['@aztec/noir-noirc_abi', '@aztec/noir-acvm_js', '@aztec/bb.js', '@aztec/noir-noir_js']
},
})Aztec's browser support requires several special configurations:
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
}Aztec uses WebAssembly with SharedArrayBuffer for multithreaded cryptographic operations. Modern browsers require these CORS headers to enable SharedArrayBuffer for security reasons.
nodePolyfillsFix({
include: ["buffer", "path", "process", "net", "tty"],
})Aztec SDK packages (@aztec/aztec.js, @aztec/accounts, etc.) were originally designed for Node.js and depend on Node.js APIs that don't exist in browsers:
buffer- Binary data handling used by cryptographic operations and WASM modulesprocess- Environment variables and process informationpath- File path utilities used by module resolutionnet&tty- Required by the Pino logger used throughout Aztec SDK
The vite-plugin-node-polyfills provides browser-compatible implementations of these APIs.
Note: The nodePolyfillsFix wrapper is a workaround for a module resolution bug in vite-plugin-node-polyfills v0.19.0+ where the plugin's polyfill imports fail to resolve correctly during Vite's optimization phase, causing runtime errors even though builds succeed. The fix manually resolves polyfill paths to their absolute file system locations.
optimizeDeps: {
include: ['pino', 'pino/browser'],
exclude: ['@aztec/noir-noirc_abi', '@aztec/noir-acvm_js', '@aztec/bb.js', '@aztec/noir-noir_js']
}- Include: Pre-bundle the Pino logger for better performance
- Exclude: Prevent Vite from optimizing Aztec's WASM modules, which would break their loading mechanism
import { createAztecNodeClient } from '@aztec/aztec.js/node'
import { EmbeddedWallet } from '@aztec/wallets/embedded';
const nodeURL = 'http://localhost:8080';
const aztecNode = await createAztecNodeClient(nodeURL);
const wallet = await EmbeddedWallet.create(aztecNode);import { getInitialTestAccountsData } from '@aztec/accounts/testing';
const [accountData] = await getInitialTestAccountsData();
const accountManager = await wallet.createSchnorrInitializerlessAccount(
accountData.secret,
accountData.salt,
accountData.signingKey
);
const accountAddress = accountManager.address;import { PrivateVotingContract } from '@aztec/noir-contracts.js/PrivateVoting';
const deployedContract = await PrivateVotingContract.deploy(wallet, address)
.send({ from: address });import { AztecAddress } from '@aztec/aztec.js/addresses'
const contract = await PrivateVotingContract.at(contractAddress, wallet);
await contract.methods.cast_vote(AztecAddress.random())
.send({ from: address });Problem: Node.js polyfills aren't loading correctly.
Solution: Ensure you're using the nodePolyfillsFix wrapper in your Vite config (not the plain nodePolyfills plugin). The wrapper fixes a module resolution bug in the polyfills plugin.
Problem: Errors about SharedArrayBuffer or WASM initialization failures.
Solution: Verify the CORS headers in your Vite config:
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
}Check browser console - these headers must be present in the response.
Problem: Cannot resolve Aztec packages or their dependencies.
Solution:
- Ensure all Aztec packages are on the same version (e.g.,
5.0.0-rc.1) - Verify WASM modules are excluded in
optimizeDeps.exclude - Clear Vite cache:
rm -rf node_modules/.vite
Problem: Application can't reach the Aztec sandbox.
Solution:
- Ensure the Aztec local network is running:
aztec start --local-network - Verify it's accessible at
http://localhost:8080 - Check CORS if sandbox is on a different port
test-wallet-webapp/
├── src/
│ ├── App.tsx # Main application component
│ ├── main.tsx # Application entry point
│ ├── consoleInterceptor.ts # Console output capture utility
│ └── ...
├── vite.config.ts # Vite configuration with Aztec compatibility
├── package.json # Dependencies
└── README.md # This file