Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6982d7c
refactor: lift session manager into shared/wallet/session
mverzilli Apr 28, 2026
1afb25e
refactor: address review feedback on shared/wallet/session lift
mverzilli Apr 28, 2026
ceb9868
refactor: lift StandaloneShell into shared/ui
mverzilli Apr 28, 2026
1231c69
refactor: make StandaloneShell.setPin async-ready
mverzilli Apr 28, 2026
a3c4d20
wip
mverzilli Apr 29, 2026
1f85cfb
extension-wallet: add constants and content script
mverzilli Apr 29, 2026
e4ba35d
extension-wallet: add Argon2id KDF and vault probe
mverzilli Apr 29, 2026
d553392
extension-wallet: add vault metadata store
mverzilli Apr 29, 2026
0ddf472
extension-wallet: add vault lock state machine
mverzilli Apr 29, 2026
cef88c6
extension-wallet: define port message envelope
mverzilli Apr 29, 2026
7ee803f
extension-wallet: add port server
mverzilli Apr 29, 2026
865365a
extension-wallet: add port client and roundtrip test
mverzilli Apr 29, 2026
9954634
extension-wallet: add wallet host (vault + session + port server)
mverzilli Apr 29, 2026
f178b80
extension-wallet: add offscreen entry point
mverzilli Apr 29, 2026
dc25a7f
extension-wallet: add offscreen lifecycle + keep-alive
mverzilli Apr 29, 2026
91e1024
extension-wallet: add approval window queue
mverzilli Apr 29, 2026
ca1b1e7
extension-wallet: add auto-lock alarm wrapper
mverzilli Apr 29, 2026
d79067e
extension-wallet: port remembered-apps logic
mverzilli Apr 29, 2026
6ea2b0e
extension-wallet: add background SW with offscreen forwarding
mverzilli Apr 29, 2026
d5c6cec
extension-wallet: add onboarding wizard
mverzilli Apr 29, 2026
a457e87
extension-wallet: add popup with lock screen
mverzilli Apr 29, 2026
ff03a48
extension-wallet: add expanded view (StandaloneShell over port)
mverzilli Apr 29, 2026
14b5434
extension-wallet: add approval window
mverzilli Apr 29, 2026
ea21018
extension-wallet: polish prep — gitignore, README, top-level flavors …
mverzilli Apr 29, 2026
c192641
extension wallet prototype
mverzilli May 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,33 @@ The wallet uses **Native Messaging** for secure communication between the browse
- **Native Host**: A small binary (`native-host`) that bridges extension ↔ Electron via stdio/socket
- **Electron App**: Runs the wallet-worker process that handles account management and signing

## Wallet flavors

This repo contains five wallet flavors that share a common core in `shared/`:

| Flavor | Where wallet logic runs | Directory |
| ---------------------------- | -------------------------------------------------------- | ------------------- |
| Native (Electron) | Worker thread inside an Electron app | `app/` |
| Web standalone | Browser page at the wallet's own origin | `web/` |
| Web iframe | Browser page embedded by dApps via postMessage | `web/` (same build) |
| Extension (relay) | Forwards to the Electron app via Native Messaging | `extension/` |
| Extension (self-contained) | Inside the extension itself (offscreen document) | `extension-wallet/` |

The relay extension and self-contained extension can be installed side-by-side; they appear as two separate wallet options in dApp wallet pickers (different `WALLET_ID`s).

### Self-contained extension wallet

The `extension-wallet/` directory contains a MetaMask-shaped extension where the wallet logic runs inside the extension. No Electron app required.

```bash
cd extension-wallet
yarn install
yarn dev # Chrome
yarn dev:firefox # Firefox
```

See `extension-wallet/README.md` for the architecture and `docs/superpowers/specs/2026-04-28-browser-extension-wallet-design.md` for the full design.

## Updating to Latest Nightly

```bash
Expand Down
13 changes: 13 additions & 0 deletions extension-wallet/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# WXT generated artifacts
.wxt
.output

# Build stats
stats.html
stats-*.json

# Logs
*.log

# Firefox-specific dev config that WXT may generate locally
web-ext.config.ts
116 changes: 116 additions & 0 deletions extension-wallet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# extension-wallet

Self-contained Aztec wallet browser extension. Wallet logic runs inside the extension itself (no Electron dependency), modeled on MetaMask.

This is distinct from `extension/` at the repo root, which is a transport-only relay between dApps and the Electron app at `app/`.

## Development

```bash
yarn install # also runs `wxt prepare` to generate .wxt/
yarn dev # Chrome
yarn dev:firefox # Firefox
yarn test # vitest unit tests
yarn compile # tsc --noEmit
```

`yarn dev` opens a Chrome instance with the extension loaded and an isolated profile. The first run pops an onboarding tab automatically.

## Architecture

```
┌───────────────────────────────┐
│ dApp page (any origin) │
└────┬──────────────────────────┘
content script (entrypoints/content.ts)
┌───────────────────────────────┐
│ Background service worker │
│ (entrypoints/background.ts) │
│ - BackgroundConnectionHandler│
│ - Approval window queue │
│ - Auto-lock alarm │
│ - Remembered apps │
└────┬──────────────────────────┘
chrome.runtime.Port (PortClient ↔ PortServer)
┌───────────────────────────────┐
│ Offscreen document │
│ (entrypoints/offscreen/) │
│ - VaultState (lock/unlock) │
│ - WalletHost (RPC dispatch) │
│ - PXE / WalletDB / shared │
│ session (lifted from web/) │
└───────────────────────────────┘
▲ ▲
│ │
┌─────────────┘ └────────────────┐
│ │
┌──────────────────┐ ┌─────────────────────┐
│ Popup │ │ Approval window │
│ (lock screen + │ │ (per-request) │
│ status) │ └─────────────────────┘
└──────────────────┘
┌─────────────────────┐
│ Expanded view │
│ (StandaloneShell │
│ from shared/) │
└─────────────────────┘
```

### Files

- `entrypoints/background.ts` — Service worker (router only)
- `entrypoints/offscreen/` — Wallet host bootstrap
- `entrypoints/content.ts` — dApp ↔ extension relay
- `entrypoints/popup/` — Status + lock screen
- `entrypoints/approval/` — Per-request approval window
- `entrypoints/expanded/` — Full wallet UI (reuses `StandaloneShell` from `@demo-wallet/shared/ui`)
- `entrypoints/onboarding/` — First-install setup wizard
- `src/vault/` — Argon2id KDF, vault metadata store, lock state machine
- `src/ipc/` — Port message envelope (Zod), `PortServer` (offscreen-side), `PortClient` (UI/SW-side)
- `src/offscreen/wallet-host.ts` — Wires `VaultState`, the lifted session manager, and `PortServer`
- `src/background/` — SW helpers: offscreen lifecycle, approval queue, auto-lock alarm, remembered apps
- `src/ui/port-wallet-api.ts` — Builds an `InternalWalletInterface` Proxy that forwards over `PortClient`

### RPC namespaces

The port server multiplexes four namespaces:

| Prefix | Caller | Target | Examples |
|--------------|-----------------|------------------|---------------------------------------------|
| `vault.*` | UI surfaces | `VaultState` | `vault.unlock`, `vault.isUnlocked` |
| `network.*` | UI surfaces | host config | `network.set`, `network.get` |
| `wallet.*` | UI surfaces | `InternalWallet` | `wallet.getAccounts`, `wallet.createAccount`|
| `dapp.*` | dApp (via SW) | `ExternalWallet` | `dapp.simulateTx`, `dapp.sendTx` |

`wallet.*` and `dapp.*` are populated dynamically by enumerating `InternalWalletInterfaceSchema` and `WalletSchema`.

## Extension ID

The Chromium extension ID is derived from the `key` field in `wxt.config.ts`. Run `yarn dev` once and look at the `chrome://extensions` page — the ID will appear under the loaded extension. Copy it back into this README if you want it handy.

## Browser support

- **Chrome / Brave / Edge**: full support via `chrome.offscreen`.
- **Firefox**: no `chrome.offscreen` API; falls back to a hidden minimized window hosting `offscreen.html` (see `src/background/offscreen-lifecycle.ts`).

## Coexistence with `extension/`

Both extensions can be installed side-by-side. They use different `WALLET_ID`s (`aztec-extension-wallet` vs `aztec-keychain`), so the wallet-sdk discovery flow lists both as options in dApp wallet pickers.

## Known deviations from the design plan

- **`dapp.*` and `wallet.*` dispatch use explicit schema-key enumeration** (not a JS `Proxy`). Rationale: spreading a Proxy into a plain object loses the `get` trap, defeating the dispatcher.
- **Approval window uses `authorization.getPending`** (one-shot read at mount) instead of a broadcast-replay mechanism. Avoids spamming every open UI surface with re-broadcasts of every pending auth.
- **`tsconfig.json` overrides `strict: false`** to match `web/`/`shared/` workspace conventions. WXT's auto-generated config is `strict: true`, but `shared/`'s source isn't strict-clean, so following imports under strict tsc would fail in unrelated files. Re-enabling strict mode is gated on making `shared/` strict-clean first.

## Caveats (v1)

- **At-rest encryption is deferred.** The vault uses Argon2id + a probe-based password check, but account secrets are stored in plaintext in IndexedDB. This is intentional pending the in-progress IndexedDB replacement; the lock UX is preserved so the surface area doesn't change when encryption is added later.

## Design

See `docs/superpowers/specs/2026-04-28-browser-extension-wallet-design.md` for the full design.
11 changes: 11 additions & 0 deletions extension-wallet/entrypoints/approval/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Approve Request</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
121 changes: 121 additions & 0 deletions extension-wallet/entrypoints/approval/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { useEffect, useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import { ThemeProvider, createTheme, CssBaseline } from "@mui/material";
import { Fr } from "@aztec/aztec.js/fields";
import { AuthorizationDialog, WalletContext } from "@demo-wallet/shared/ui";
import type {
AuthorizationRequest,
AuthorizationItemResponse,
} from "@demo-wallet/shared/core";
import { PortClient } from "../../src/ipc/port-client";
import { makePortWalletApi } from "../../src/ui/port-wallet-api";

function Approval() {
const [client] = useState(() => {
const c = new PortClient();
c.connect();
return c;
});
const [request, setRequest] = useState<AuthorizationRequest | null>(null);
const [walletAPI, setWalletAPI] = useState<ReturnType<typeof makePortWalletApi> | null>(
null,
);
const resolved = useRef(false);
const requestId = new URLSearchParams(window.location.search).get("requestId");

// Resolve current chain → build a port-backed wallet API for the dialog.
useEffect(() => {
void (async () => {
const net = await client.call<{ chainId: string; version: string }>(
"network.get",
[],
);
setWalletAPI(
makePortWalletApi(client, Fr.fromString(net.chainId), Fr.fromString(net.version)),
);
})();
}, [client]);

// Fetch pending requests, find the one matching our requestId.
useEffect(() => {
if (!requestId) return;
void (async () => {
const pending = await client.call<AuthorizationRequest[]>(
"authorization.getPending",
[],
);
const found = pending.find((r) => r.id === requestId);
if (found) setRequest(found);
})();
}, [client, requestId]);

// Window-close = denial (only if the user hasn't already resolved).
useEffect(() => {
function onUnload() {
if (resolved.current || !request) return;
const itemResponses: Record<string, AuthorizationItemResponse> = {};
for (const item of request.items) {
itemResponses[item.id] = {
id: item.id,
approved: false,
appId: item.appId,
};
}
// Best-effort: postMessage on a port runs synchronously; the offscreen
// may or may not see it before the SW unloads us.
void client.call("authorization.resolve", [
{ id: request.id, approved: false, appId: request.appId, itemResponses },
]);
}
window.addEventListener("beforeunload", onUnload);
return () => window.removeEventListener("beforeunload", onUnload);
}, [client, request]);

if (!request || !walletAPI) return null;

const handleApprove = async (
itemResponses: Record<string, AuthorizationItemResponse>,
) => {
resolved.current = true;
await client.call("authorization.resolve", [
{ id: request.id, approved: true, appId: request.appId, itemResponses },
]);
window.close();
};

const handleDeny = async () => {
resolved.current = true;
const itemResponses: Record<string, AuthorizationItemResponse> = {};
for (const item of request.items) {
itemResponses[item.id] = {
id: item.id,
approved: false,
appId: item.appId,
};
}
await client.call("authorization.resolve", [
{ id: request.id, approved: false, appId: request.appId, itemResponses },
]);
window.close();
};

return (
<WalletContext.Provider value={{ walletAPI }}>
<AuthorizationDialog
request={request}
queueLength={1}
onApprove={handleApprove}
onDeny={handleDeny}
/>
</WalletContext.Provider>
);
}

const theme = createTheme({ palette: { mode: "dark" } });

createRoot(document.getElementById("root")!).render(
<ThemeProvider theme={theme}>
<CssBaseline />
<Approval />
</ThemeProvider>,
);
Loading
Loading