From 05589c145e70201af2000a8c5d4484eb6ee099f7 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Mon, 20 Apr 2026 10:03:47 +0000 Subject: [PATCH 1/3] feat(wallet): wire PXE + walletDB to SQLite-OPFS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit End-to-end persistence of both PXE state and the wallet's own account DB on the OPFS-backed sqlite-opfs kv-store backend (new in aztec-packages). The rollup address scopes DB names so switching networks doesn't cross-contaminate; each store uses a distinct OPFS SAH-Pool directory because the pool acquires an exclusive lock per directory. - src/services/walletService.ts: construct two AztecSQLiteOPFSStore instances and pass them via { pxe: { store }, walletDb: { store } }. Dev-mode registers console-accessible inspectors. - src/utils/sqliteInspector.ts: dev-only helper exposing window.__aztecStores with downloadPxe()/downloadWallet() (exports a real .sqlite image) and summary() for a quick container+row-count overview. - package.json: add @aztec/kv-store as a direct dep — it's imported from source now, so yarn needs to keep it in the tree. Infra updates to support linked aztec-packages: - scripts/toggle-local-aztec.js: @aztec/wallets added to PACKAGE_MAPPINGS (was missing); VITE_FS_ALLOW_PATHS extended with kv-store and @sqlite.org/sqlite-wasm paths so Vite can serve the worker + wasm. - vite.config.ts: * resolve.alias now dynamic — reads .local-aztec-path for the aztec-packages root, so no user-specific paths get committed. * wasmContentTypePlugin forces Content-Type: application/wasm on .wasm responses (sqlite-wasm's streaming compile needs this). * optimizeDeps.exclude adds @sqlite.org/sqlite-wasm — Vite's prebundle breaks colocated .wasm asset resolution. * nodePolyfillsFix returns absolute paths so Buffer imports from files linked outside the workspace root resolve correctly. --- package.json | 1 + scripts/toggle-local-aztec.js | 5 +++ src/services/walletService.ts | 31 ++++++++++++++- src/utils/sqliteInspector.ts | 73 +++++++++++++++++++++++++++++++++++ vite.config.ts | 60 +++++++++++++++++++++++++++- 5 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 src/utils/sqliteInspector.ts diff --git a/package.json b/package.json index db92d4c..89a5dd2 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@aztec/constants": "v4.3.0-nightly.20260417", "@aztec/entrypoints": "v4.3.0-nightly.20260417", "@aztec/foundation": "v4.3.0-nightly.20260417", + "@aztec/kv-store": "v4.3.0-nightly.20260417", "@aztec/noir-contracts.js": "v4.3.0-nightly.20260417", "@aztec/protocol-contracts": "v4.3.0-nightly.20260417", "@aztec/pxe": "v4.3.0-nightly.20260417", diff --git a/scripts/toggle-local-aztec.js b/scripts/toggle-local-aztec.js index b431daa..4e03fa9 100755 --- a/scripts/toggle-local-aztec.js +++ b/scripts/toggle-local-aztec.js @@ -62,6 +62,7 @@ const PACKAGE_MAPPINGS = { "@aztec/test-wallet": "yarn-project/test-wallet", "@aztec/validator-client": "yarn-project/validator-client", "@aztec/wallet-sdk": "yarn-project/wallet-sdk", + "@aztec/wallets": "yarn-project/wallets", "@aztec/world-state": "yarn-project/world-state", }; @@ -71,6 +72,10 @@ const VITE_FS_ALLOW_PATHS = [ "noir/packages/noirc_abi/web", "noir/packages/acvm_js/web", "barretenberg/ts/dest/browser", + // kv-store's sqlite-opfs backend runs a Web Worker served from dest/; dep of @sqlite.org/sqlite-wasm + // is a transitive that link: doesn't surface into the consumer's node_modules. + "yarn-project/kv-store", + "yarn-project/node_modules/@sqlite.org/sqlite-wasm", ]; function savePath(aztecPath) { diff --git a/src/services/walletService.ts b/src/services/walletService.ts index 53f71a7..89b599a 100644 --- a/src/services/walletService.ts +++ b/src/services/walletService.ts @@ -7,6 +7,9 @@ import { createAztecNodeClient, type AztecNode } from '@aztec/aztec.js/node'; import type { Wallet } from '@aztec/aztec.js/wallet'; import type { ChainInfo } from '@aztec/aztec.js/account'; import { Fr } from '@aztec/aztec.js/fields'; +import { createLogger } from '@aztec/foundation/log'; +import { AztecSQLiteOPFSStore } from '@aztec/kv-store/sqlite-opfs'; +import { registerSqliteInspectors } from '../utils/sqliteInspector'; import { WalletManager, type WalletProvider, @@ -40,7 +43,33 @@ export function createNodeClient(nodeUrl: string): AztecNode { export async function createEmbeddedWallet( node: AztecNode, ): Promise<{ wallet: EmbeddedWallet; address: AztecAddress }> { - const wallet = await EmbeddedWallet.create(node, { pxeConfig: { proverEnabled: true } }); + // Both PXE state and the wallet's own DB go on SQLite-OPFS. Each store needs a + // distinct OPFS pool directory because SAH Pool acquires an exclusive lock on + // its directory — one shared directory would collide in a single tab. The + // rollup address scopes the DB names so switching networks doesn't + // cross-contaminate. + const l1Contracts = await node.getL1ContractAddresses(); + const rollup = l1Contracts.rollupAddress.toString(); + const pxeStore = await AztecSQLiteOPFSStore.open( + createLogger('pxe:data:sqlite-opfs'), + `pxe_data_${rollup}`, + false, + `.aztec-kv-pxe-${rollup}`, + ); + const walletStore = await AztecSQLiteOPFSStore.open( + createLogger('wallet:data:sqlite-opfs'), + `wallet_data_${rollup}`, + false, + `.aztec-kv-wallet-${rollup}`, + ); + const wallet = await EmbeddedWallet.create(node, { + pxe: { proverEnabled: true, store: pxeStore }, + walletDb: { store: walletStore }, + }); + if (import.meta.env.DEV) { + // Expose dev-only inspectors at `window.__aztecStores`. See sqliteInspector.ts. + registerSqliteInspectors({ pxe: pxeStore, wallet: walletStore }); + } let accountManager = await wallet.loadStoredAccount(); if (!accountManager) { accountManager = await wallet.createInitializerlessAccount(); diff --git a/src/utils/sqliteInspector.ts b/src/utils/sqliteInspector.ts new file mode 100644 index 0000000..dc79f2d --- /dev/null +++ b/src/utils/sqliteInspector.ts @@ -0,0 +1,73 @@ +/** + * Dev-only inspectors for SQLite-OPFS stores. + * + * Exposed on `window.__aztecStores` by walletService.ts in development mode so the + * DB contents can be examined from the browser DevTools console without + * copy-pasting recipes. These helpers are a no-op in production builds. + */ + +import type { AztecAsyncKVStore } from '@aztec/kv-store'; + +/** Minimal subset of AztecSQLiteOPFSStore the inspectors need. */ +interface InspectableStore extends AztecAsyncKVStore { + allAsync(sql: string, bind?: unknown[]): Promise; + exportDb(): Promise; +} + +function downloadBytes(bytes: Uint8Array, filename: string): void { + const blob = new Blob([bytes], { type: 'application/x-sqlite3' }); + const url = URL.createObjectURL(blob); + try { + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + } finally { + // Revoke after the click handler has started the download, give the browser a beat. + setTimeout(() => URL.revokeObjectURL(url), 10_000); + } +} + +/** + * Summary row: container name and row count. Useful for a quick overview of what + * each store holds right now. + */ +async function summarize(store: InspectableStore): Promise> { + const rows = await store.allAsync( + 'SELECT container, count(*) AS n FROM data GROUP BY container ORDER BY n DESC', + ); + return rows.map(r => ({ container: String(r[0]), rows: Number(r[1]) })); +} + +/** Stores exposed for inspection, plus their bound helpers. */ +export type SqliteInspectors = { + pxe: InspectableStore; + wallet: InspectableStore; + /** Downloads the PXE store as `pxe.sqlite`. */ + downloadPxe(): Promise; + /** Downloads the walletDB store as `wallet.sqlite`. */ + downloadWallet(): Promise; + /** Prints container/row-count summaries for both stores (console-friendly). */ + summary(): Promise<{ pxe: Array<{ container: string; rows: number }>; wallet: Array<{ container: string; rows: number }> }>; +}; + +/** + * Registers the inspectors on `window.__aztecStores`. Safe to call in SSR/non-dev + * contexts — it bails out cleanly. + */ +export function registerSqliteInspectors(stores: { pxe: InspectableStore; wallet: InspectableStore }): void { + if (typeof window === 'undefined') { + return; + } + const inspectors: SqliteInspectors = { + pxe: stores.pxe, + wallet: stores.wallet, + downloadPxe: async () => downloadBytes(await stores.pxe.exportDb(), 'pxe.sqlite'), + downloadWallet: async () => downloadBytes(await stores.wallet.exportDb(), 'wallet.sqlite'), + summary: async () => ({ + pxe: await summarize(stores.pxe), + wallet: await summarize(stores.wallet), + }), + }; + (window as unknown as { __aztecStores: SqliteInspectors }).__aztecStores = inspectors; +} diff --git a/vite.config.ts b/vite.config.ts index 8f406b4..1d5653d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,12 +13,58 @@ const nodePolyfillsFix = (options?: PolyfillOptions | undefined): Plugin => { resolveId(source: string) { const m = /^vite-plugin-node-polyfills\/shims\/(buffer|global|process)$/.exec(source); if (m) { - return `./node_modules/vite-plugin-node-polyfills/shims/${m[1]}/dist/index.cjs`; + return path.resolve( + process.cwd(), + `node_modules/vite-plugin-node-polyfills/shims/${m[1]}/dist/index.cjs`, + ); } }, }; }; +/** + * Loads resolve aliases for transitive aztec-packages workspace deps that yarn `link:` + * doesn't surface to gregoswap's node_modules. Reads the aztec-packages root from + * `.local-aztec-path` (written by `scripts/toggle-local-aztec.js enable`). Returns `{}` + * when the file doesn't exist (local-aztec disabled), leaving npm resolutions active. + */ +function loadLocalAztecAliases(): Record { + try { + const root = fs.readFileSync(path.resolve(process.cwd(), '.local-aztec-path'), 'utf-8').trim(); + if (!root) { + return {}; + } + return { + '@aztec/bb.js': `${root}/barretenberg/ts/dest/browser/index.js`, + '@aztec/noir-acvm_js': `${root}/noir/packages/acvm_js/web/acvm_js.js`, + '@aztec/noir-noirc_abi': `${root}/noir/packages/noirc_abi/web/noirc_abi_wasm.js`, + '@sqlite.org/sqlite-wasm': `${root}/yarn-project/node_modules/@sqlite.org/sqlite-wasm/index.mjs`, + }; + } catch { + // No .local-aztec-path file — we're using npm packages, no aliases needed. + return {}; + } +} + +/** + * Force `Content-Type: application/wasm` on `.wasm` files served by Vite's dev server. + * Without this, `WebAssembly.compileStreaming()` (used by sqlite-wasm and others) + * rejects the response with "Incorrect response MIME type. Expected 'application/wasm'". + * Vite's dev middleware doesn't set this header by default for files served from + * aliased / @fs paths outside node_modules. + */ +const wasmContentTypePlugin = (): Plugin => ({ + name: 'wasm-content-type', + configureServer(server) { + server.middlewares.use((req, res, next) => { + if (req.url?.includes('.wasm')) { + res.setHeader('Content-Type', 'application/wasm'); + } + next(); + }); + }, +}); + /** * Lightweight chunk size validator plugin * Checks chunk sizes after build completes and fails if limits are exceeded @@ -105,11 +151,16 @@ export default defineConfig(({ command, mode }) => { const isDev = command === 'serve'; const esTarget = isDev ? 'es2016' : 'esnext'; + const localAztecAliases = loadLocalAztecAliases(); + return { base: './', logLevel: process.env.CI ? 'error' : undefined, esbuild: { target: esTarget }, build: { target: esTarget }, + resolve: { + alias: localAztecAliases, + }, server: { // Headers needed for bb WASM to work in multithreaded mode headers: { @@ -121,7 +172,11 @@ export default defineConfig(({ command, mode }) => { }, }, optimizeDeps: { - exclude: ['@aztec/noir-acvm_js', '@aztec/noir-noirc_abi', '@aztec/bb.js'], + // @sqlite.org/sqlite-wasm must be excluded: Vite's prebundle extracts the JS + // into .vite/deps/ but doesn't copy the adjacent sqlite3.wasm binary, so the + // generated fetch URL 404s. Excluding keeps the JS at its real location where + // the .wasm sits next to it. + exclude: ['@aztec/noir-acvm_js', '@aztec/noir-noirc_abi', '@aztec/bb.js', '@sqlite.org/sqlite-wasm'], include: ['@gregojuice/embedded-wallet/ui'], esbuildOptions: { target: esTarget }, }, @@ -132,6 +187,7 @@ export default defineConfig(({ command, mode }) => { ...(isDev ? { devTarget: 'es2016' as const } : {}), }), nodePolyfillsFix({ include: ['buffer', 'path'] }), + wasmContentTypePlugin(), chunkSizeValidator([ { pattern: /assets\/index-.*\.js$/, From 5e69ddcd6de49fc247f2f628f02e7f034db64875 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Mon, 20 Apr 2026 10:08:53 +0000 Subject: [PATCH 2/3] chore(dev): bind dev server on 0.0.0.0 and allow tunnel hostnames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enables testing from devices on the same network (iPhone via mkcert-trusted HTTPS) or through a public tunnel (ngrok, cloudflared). The allowedHosts list covers rotating free-tier ngrok subdomains and ephemeral trycloudflare tunnels — tighten or replace with a specific domain if you want to restrict further. Usage: yarn dev # LAN access at https://:5173 ngrok http 5173 # public HTTPS tunnel, URL printed by ngrok --- vite.config.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/vite.config.ts b/vite.config.ts index 1d5653d..c8fa004 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -162,6 +162,13 @@ export default defineConfig(({ command, mode }) => { alias: localAztecAliases, }, server: { + // Bind on 0.0.0.0 so a tunnel (ngrok, cloudflared) or same-network device + // (e.g. iPhone with mkcert-trusted HTTPS) can reach the dev server. + host: true, + // Accept Host headers from tunnel providers without needing per-URL config. + // Wildcards cover rotating ngrok-free subdomains; trycloudflare.com covers + // ephemeral Cloudflare tunnels. Tighten if you want to restrict further. + allowedHosts: ['.ngrok-free.app', '.ngrok.app', '.trycloudflare.com'], // Headers needed for bb WASM to work in multithreaded mode headers: { 'Cross-Origin-Opener-Policy': 'same-origin', From dd49f8530e81e2ba1357208baedd6ab13330783f Mon Sep 17 00:00:00 2001 From: mverzilli Date: Mon, 20 Apr 2026 15:17:46 +0000 Subject: [PATCH 3/3] docs(local-aztec): explain why sqlite-wasm fs.allow entry is different Reviewer rightly asked why this one entry points at node_modules when the others point at workspace source trees. The short answer is that sqlite-wasm is a real npm package (not a workspace), and it's a transitive dep of @aztec/kv-store that yarn link: doesn't surface into the consumer. Declaring it as a direct dep of gregoswap would not help, because Vite resolves worker imports from the worker file's location (in aztec-packages), not from the consumer's root. Comment expanded inline so the next reviewer doesn't have to re-derive this. --- scripts/toggle-local-aztec.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/scripts/toggle-local-aztec.js b/scripts/toggle-local-aztec.js index 4e03fa9..2aeda1c 100755 --- a/scripts/toggle-local-aztec.js +++ b/scripts/toggle-local-aztec.js @@ -66,14 +66,26 @@ const PACKAGE_MAPPINGS = { "@aztec/world-state": "yarn-project/world-state", }; -// Paths within aztec-packages that need to be allowed in vite's fs.allow +// Paths within aztec-packages that need to be allowed in vite's fs.allow. +// +// Most entries point at built-output directories of workspace packages — those +// packages live in the source tree (yarn workspaces, not node_modules). +// +// The `yarn-project/node_modules/@sqlite.org/sqlite-wasm` entry is the odd one +// out: @sqlite.org/sqlite-wasm is a real npm package, not a workspace, and it's +// a transitive dep of @aztec/kv-store. Yarn's `link:` protocol doesn't install +// the linked package's transitive deps into the consumer, so we can't resolve +// it from gregoswap's own node_modules. Instead, we resolve it from aztec- +// packages's hoisted install (yarn-project/node_modules) and Vite serves from +// there. See the matching alias in vite.config.ts's loadLocalAztecAliases(). +// Declaring the package as a direct dep of gregoswap would not help: Vite +// resolves the worker file's imports from the worker file's location (in +// aztec-packages), not from the consumer's root. const VITE_FS_ALLOW_PATHS = [ "yarn-project/noir-protocol-circuits-types/artifacts", "noir/packages/noirc_abi/web", "noir/packages/acvm_js/web", "barretenberg/ts/dest/browser", - // kv-store's sqlite-opfs backend runs a Web Worker served from dest/; dep of @sqlite.org/sqlite-wasm - // is a transitive that link: doesn't surface into the consumer's node_modules. "yarn-project/kv-store", "yarn-project/node_modules/@sqlite.org/sqlite-wasm", ];