From 56a2ff993032866c3c40e7cfa12d726332f493f2 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Mon, 7 Sep 2026 12:20:17 +0200 Subject: [PATCH] Replace stub wallet keypair with real Phantom sign-in Auth now goes through an actual connected Solana wallet instead of a locally-generated ed25519 keypair. Phantom's window.solana provider is only reachable from a page's own JS world, not an isolated-world content script, so this adds a second world:'MAIN' content script (wallet-bridge.content.ts + wallet-bridge/inject.ts) that talks to window.solana directly and relays to the isolated world via window CustomEvents (wallet-bridge/relay.ts), matched by request id. wallet-connect.ts orchestrates: try a silent onlyIfTrusted connect on load; if that fails, show an on-page banner (wallet-bridge/banner.ts) whose click handler is what actually calls connect() -- Phantom requires a real user gesture for the approval popup on a first-ever connect, which a click relayed from the extension popup wouldn't count as by the time it reaches the wallet. background/wallet-auth.ts runs the REST auth flow (nonce -> ask the tab's content script to sign it -> verify -> store session token) once a wallet reports connected. ws-client.ts no longer force-retries with a known-bad token on auth failure; it calls onAuthExpired instead (which clears the token and prompts a silent wallet reconnect) and exposes reconnectNow() so background.ts can short-circuit the backoff wait once a fresh token exists. identity.ts and its tweetnacl dependency are gone -- no more stub signer. Untested against real Phantom (no browser automation available this session) -- flagged in CLAUDE.md as needing manual verification, along with a note that world:'MAIN' needs Firefox 128+. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01YXiHuScXrjxBh7yFGAPq3B --- CLAUDE.md | 18 +++-- package-lock.json | 9 +-- package.json | 3 +- src/background/backend-client.ts | 60 ++++------------ src/background/identity.ts | 53 -------------- src/background/wallet-auth.ts | 55 ++++++++++++++ src/background/ws-client.ts | 79 +++++++++++++++------ src/content-scripts/content-index.ts | 5 ++ src/content-scripts/wallet-bridge/banner.ts | 56 +++++++++++++++ src/content-scripts/wallet-bridge/inject.ts | 65 +++++++++++++++++ src/content-scripts/wallet-bridge/relay.ts | 42 +++++++++++ src/content-scripts/wallet-connect.ts | 64 +++++++++++++++++ src/entrypoints/background.ts | 16 ++++- src/entrypoints/wallet-bridge.content.ts | 14 ++++ src/shared/messaging.ts | 10 ++- 15 files changed, 413 insertions(+), 136 deletions(-) delete mode 100644 src/background/identity.ts create mode 100644 src/background/wallet-auth.ts create mode 100644 src/content-scripts/wallet-bridge/banner.ts create mode 100644 src/content-scripts/wallet-bridge/inject.ts create mode 100644 src/content-scripts/wallet-bridge/relay.ts create mode 100644 src/content-scripts/wallet-connect.ts create mode 100644 src/entrypoints/wallet-bridge.content.ts diff --git a/CLAUDE.md b/CLAUDE.md index ed13ba6..6ae69e0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -86,11 +86,22 @@ Note the naming mismatch with the wire protocol below: the frontend's internal ` ## Backend connection (implemented) - `src/shared/config.ts` — `BACKEND_HTTP_URL`/`BACKEND_WS_URL`, currently hardcoded to `localhost:8080` (dev only; `host_permissions` in `wxt.config.ts` must stay in sync with whatever host is configured here). Deliberately not `:3000` — that's this extension's own Vite dev server port (`npm run dev`), and running the backend on the same port breaks the dev popup silently: its script tags point at Vite, but the backend answers instead, so nothing ever renders. If you see a blank popup with `http://localhost:3000/...` script tags in "View Page Source" that 404 or return something unexpected, this port collision is the first thing to check. -- `src/background/identity.ts` — **stub wallet**: a locally-generated ed25519 keypair (via `tweetnacl`), persisted in `browser.storage.local`, used as a stand-in for a real Solana wallet signature. This is deliberately temporary — real wallet integration means bridging into the page's injected `window.solana` provider on axiom.trade (content script + page-context script), which hasn't been built yet. `getIdentity()`/`sign()` is the seam that swap plugs into. -- `src/background/backend-client.ts` — the REST auth flow (`POST /auth/nonce` → sign → `POST /auth/verify` → session token), persisted via `getSessionToken()`. -- `src/background/ws-client.ts` — the WS client described above: connects to `/ws?token=...`, reconnects with the protocol's suggested backoff, force-refreshes the session token on `4001`/`auth_expired` before retrying. +- `src/background/backend-client.ts` — session token storage only (`getSessionToken()`/`storeSessionToken()`/`clearSessionToken()`). Getting a token in the first place is `wallet-auth.ts`'s job. +- `src/background/wallet-auth.ts` — runs the REST auth flow (`POST /auth/nonce` → Phantom signature → `POST /auth/verify` → session token) once a content script reports a connected wallet; also `requestWalletReconnect()`, used after the backend invalidates a session. +- `src/background/ws-client.ts` — the WS client described above: connects to `/ws?token=...`. `connectWsClient()` returns a controller with `reconnectNow()` so the background script can short-circuit the backoff wait right after a fresh token arrives. Auth failures (`4001` close, `auth_expired`/`session_revoked` errors) do **not** auto-retry with backoff — they call `onAuthExpired()` instead, since retrying with a known-bad token can't succeed; only real disconnects (network drop, backgrounded browser) use the protocol's suggested backoff schedule. - `src/background/connection-status.ts` — separate from lock state; the popup surfaces this (connecting/connected/disconnected/auth-error) alongside the lock state so a broken connection isn't silently indistinguishable from "unlocked". +### Wallet auth (Phantom) — implemented + +Real wallet signing, not a stub keypair. Phantom (and any wallet injecting a compatible `window.solana`) is only reachable from a **page's own JS world** — a normal (isolated-world) content script cannot call into it directly, hence the two-content-script bridge below. This is the standard pattern for extensions that need to talk to page-injected wallet providers. + +- `src/entrypoints/wallet-bridge.content.ts` + `src/content-scripts/wallet-bridge/inject.ts` — a **second, `world: 'MAIN'`** content script on axiom.trade (Manifest V3 native main-world injection — no `web_accessible_resources`/dynamic `