Fix duplicate signing; make account switching an explicit action
Root cause of "requires a lot of signs, not just a single one": inject.ts wired Phantom's own accountChanged provider event to auto-trigger re-auth, but calling connect() ourselves also fires that same event -- so a normal silent reconnect raced its own event-triggered handler, producing two competing "wallet connected" reports that each independently asked Phantom to sign a fresh nonce. Removed that event wiring entirely (onWalletEvent, EVENT_CHANNEL) -- inject.ts now only responds to our own explicit calls, never reacts to unsolicited provider events. Per feedback, account switching isn't something that should be inferred from a Phantom event anyway; it's now its own explicit feature: a "Switch account" button in the popup (nexa:switch-account) that tells the content script to disconnect and immediately reconnect, so Phantom's connect UI reflects whichever account is currently active there. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YXiHuScXrjxBh7yFGAPq3B
This commit is contained in:
@@ -101,7 +101,10 @@ Real wallet signing, not a stub keypair. Phantom (and any wallet injecting a com
|
||||
- `src/content-scripts/wallet-bridge/banner.ts` — minimal plain-DOM "Connect your wallet to Nexa" prompt injected onto the page (bottom-right, fixed position) when a wallet isn't already trusted for this origin. No framework, kept deliberately tiny since it's living on someone else's page.
|
||||
- `src/content-scripts/wallet-connect.ts` — orchestrates the above, wired into `content-index.ts` (runs independently of site-adapter matching): on load, tries `connect({ onlyIfTrusted: true })` silently (succeeds with no user interaction if the user already approved this origin in Phantom before); on failure, shows the banner and only calls plain `connect()` from the banner button's own click handler, since **a real user gesture is required for Phantom to show its approval popup on a first-ever connect** — this is why the banner exists in the page rather than the extension popup (a click in the popup's UI doesn't count as a gesture on the axiom.trade page by the time it reaches the wallet, since it crosses an extension-messaging boundary asynchronously). Once connected, reports `{ walletAddress }` to the background via `nexa:wallet-connected`; also handles `nexa:wallet-sign-request` (background asks it to sign a nonce) and `nexa:request-wallet-connect` (background asks it to retry the silent connect, e.g. after a session was invalidated).
|
||||
- **Known gap**: this only works while an axiom.trade tab is open — there's no wallet connection path from the popup alone. That's intentional for now (matches the "only live while on a supported site" framing in backend/CLAUDE.md's RPC-subscription note), not an oversight.
|
||||
- **Account switching and sign-out (implemented)**: `inject.ts` also wires Phantom's own `accountChanged`/`disconnect` provider events (not just our own request/response calls) and relays them as unsolicited messages, since the user can switch accounts or disconnect directly in Phantom's UI without ever touching our banner. `wallet-connect.ts` reacts by re-authenticating as the new account, or showing the connect banner again. A "Sign out" button in the popup (`nexa:sign-out` → `background.ts`) clears the stored session, force-closes the WS connection (`ws-client.ts`'s `disconnect()`, distinct from `reconnectNow()` — it also suppresses auto-reconnect until a new wallet connects), and asks the content script to call `provider.disconnect()`, which revokes Phantom's trust for the origin so the next silent connect correctly fails until the user reconnects.
|
||||
- **Sign out and account switching (implemented) — both explicit user actions, not automatic.** An earlier version of this also wired Phantom's own `accountChanged` provider event to trigger re-auth automatically, but calling `connect()` ourselves also fires that same event — so a normal silent reconnect raced its own event-triggered handler and produced *two* competing "wallet connected" reports, each independently asking Phantom to sign a nonce (a popup every time). Removed entirely; `wallet-bridge/inject.ts` does not listen for any Phantom provider events, only responds to our own explicit calls. See its doc comment for the full story if this is ever reconsidered.
|
||||
- "Sign out" (popup button → `nexa:sign-out` → `background.ts`'s `signOut()`) clears the stored session, force-closes the WS connection (`ws-client.ts`'s `disconnect()`, distinct from `reconnectNow()` — it also suppresses auto-reconnect until a new wallet connects), and asks the content script to call `provider.disconnect()`, which revokes Phantom's trust for the origin so the next silent connect correctly fails until the user reconnects.
|
||||
- "Switch account" (popup button → `nexa:switch-account` → `wallet-connect.ts`'s `switchAccount()`) explicitly disconnects then immediately reconnects, so Phantom shows its connect approval UI for whichever account is currently active there. Not fully verified whether Phantom requires a fresh user gesture for this non-`onlyIfTrusted` connect call relayed from the popup (vs. a direct page click) — if it does, `switchAccount()` falls back to the on-page banner so the user can complete it with a real click.
|
||||
- `background.ts`'s `nexa:wallet-connected` handler distinguishes "already signed in as this wallet" (no-op) from "signed in as a *different* wallet" (re-authenticate) by comparing the reported wallet address against `backend-client.ts`'s stored `{ token, walletAddress }` pair — a bare "do we have a token" check can't tell those apart and was the reason the account-switch case needed fixing in the first place.
|
||||
- **Session storage tracks which wallet it belongs to** (`backend-client.ts`'s `storeSession(token, walletAddress)`, not just a bare token) — `background.ts`'s `nexa:wallet-connected` handler needs this to tell "already signed in" apart from "signed in as a *different* wallet than the one that just connected" (an account switch); a bare "do we have any token" check can't distinguish those and would silently keep the stale session instead of re-authenticating as the new account.
|
||||
- **Firefox gotcha (hit during dev, now fixed)**: the isolated↔main-world bridge originally used `CustomEvent`s dispatched on `window`. That works on Chromium but throws `Uncaught Error: Permission denied to access property "id"` on Firefox — a `CustomEvent.detail` object created in one world can't have its properties read from the other (an Xray-wrapper security restriction specific to Firefox's extension model). Fixed by switching to `window.postMessage` for this bridge, which structured-clones its payload across the boundary correctly on both browsers — the same technique Phantom's own inpage↔content-script bridge uses. If you're extending this bridge, don't reach for `CustomEvent` again for isolated↔main-world data; `postMessage` (with a `channel` field to disambiguate from the page's own postMessage traffic, and an `event.source === window` check) is the pattern here.
|
||||
- **Verified working end-to-end against real Phantom** on Zen: connect → sign → `/auth/verify` → session token stored, confirmed via the `[nexa/wallet-*]` debug logs. `world: 'MAIN'` also needs Firefox 128+; confirmed fine on Zen's base version.
|
||||
|
||||
Reference in New Issue
Block a user