-
Notifications
You must be signed in to change notification settings - Fork 2
feat: wire PXE and walletDB to SQLite-OPFS (opt-in) #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 }); | ||
| } | ||
|
Comment on lines
+46
to
+72
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO: this should maybe be reified in embedded-wallet, but while the grego-mono-repo is in the works, this shows what's needed |
||
| let accountManager = await wallet.loadStoredAccount(); | ||
| if (!accountManager) { | ||
| accountManager = await wallet.createInitializerlessAccount(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| /** | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file is just an ad-hoc tool which helps observe what's going on, considering unlinke IndexedDB, we don't get any out of the box storage inspection support from the browser |
||
| * 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<unknown[][]>; | ||
| exportDb(): Promise<Uint8Array>; | ||
| } | ||
|
|
||
| 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<Array<{ container: string; rows: number }>> { | ||
| 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<void>; | ||
| /** Downloads the walletDB store as `wallet.sqlite`. */ | ||
| downloadWallet(): Promise<void>; | ||
| /** 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; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<string, string> { | ||
| 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,12 +151,24 @@ 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: { | ||
| // 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'], | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Used to test on iPhone via tunnels, will probably remove from final PR |
||
| // Headers needed for bb WASM to work in multithreaded mode | ||
| headers: { | ||
| 'Cross-Origin-Opener-Policy': 'same-origin', | ||
|
|
@@ -121,7 +179,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 +194,7 @@ export default defineConfig(({ command, mode }) => { | |
| ...(isDev ? { devTarget: 'es2016' as const } : {}), | ||
| }), | ||
| nodePolyfillsFix({ include: ['buffer', 'path'] }), | ||
| wasmContentTypePlugin(), | ||
| chunkSizeValidator([ | ||
| { | ||
| pattern: /assets\/index-.*\.js$/, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a devtool convenience