Shell — Chrome Extension
@xchain-wallet/extension is the Chrome MV3 extension shell. It targets Chrome, Edge, Brave, and other Chromium browsers; Firefox is on the post-GA roadmap.
Surfaces
The extension exposes four user-facing surfaces, all rendering the same React core:
| Surface | Entry point | When |
|---|---|---|
| Popup | popup.html → src/popup/main.jsx |
Toolbar icon click |
| Full-screen | popup.html?fullscreen=1 |
“Open in tab” from popup menu |
| Approval window | approval.html → src/approval/main.jsx |
Triggered by privileged dApp / wallet operation |
| Onboarding | popup.html?onboarding=1 |
First install |
Background processes:
| Process | Entry point | Role |
|---|---|---|
| Service worker | background.js → src/background/index.js |
Vault, SDK, signers, message routing |
| Content script | src/content/contentScript.js |
Origin stamping; relay between page and service worker |
| Injected provider | src/inject/xchainProvider.js |
window.xchain for dApps |
Manifest
packages/extension/manifest.json is MV3 with a hardened permission set:
{
"manifest_version": 3,
"name": "XChain Wallet",
"version": "0.1.0.6",
"version_name": "1.0.0-rc.6",
"permissions": ["storage"],
"host_permissions": [],
"background": { "service_worker": "background.js", "type": "module" },
"content_scripts": [{
"matches": ["http://*/*", "https://*/*"],
"js": ["content/contentScript.js"],
"run_at": "document_start",
"all_frames": false
}],
"web_accessible_resources": [{
"resources": ["inject/xchainProvider.js"],
"matches": ["http://*/*", "https://*/*"]
}]
}
Deliberate choices:
- No
host_permissions. The wallet doesn’t need to read or modify page state — only inject the provider script.web_accessible_resourcescarries the injection without granting host access. storageonly. Notabs, noactiveTab, nowebRequest, nonotifications(browser-notifications use the SDK WebSocket layer instead). Smaller permission set = smaller audit surface = less scary CWS listing.- Versioning split. Chrome’s
versionis integer-tuple-only and rejects semver prerelease tags like1.0.0-rc.6. The wallet derives a Chrome-valid tuple from the wallet semver viapackages/core/scripts/derive-extension-version.js: stableM.m.p→M.m.p; prereleaseM.m.p-rc.N→0.M.m.N. The leading0keeps prereleases strictly below stable tuples in Chrome’s upgrade ordering.version_namecarries the human-readable semver.
The 11-rule manifest audit (packages/core/scripts/extension-manifest-audit.js) gates every commit:
- MV3 set
versionCWS-validversionequalsderiveExtensionVersion(root.version)version_namemirrorsroot.versionpackages/extension/package.jsonversion matches rootdescription≤ 132 charshomepage_urlset- 128-px icon present
- Action toolbar icon set
- Content scripts well-formed
- No broad host permissions without justification
Service worker
src/background/index.js boots a MessageHost (src/background/MessageHost.js) that owns:
- The Vault (encrypted, in
chrome.storage.local) - A
SDKRegistrymappingchainId → xchain-sdkinstance - The active session master key (in
chrome.storage.session) - The
approvalBrokerfor routing privileged ops to the approval window - Per-origin permission state in
connectedSites
Every privileged op enters the service worker as a typed message, routes through the broker → approval popup → user-confirmation, and exits as a typed result envelope.
Content script
src/content/contentScript.js runs in the isolated content-script world at document_start. It:
- Inserts the
<script>tag pointing atsrc/inject/xchainProvider.js - Listens for
window.postMessageevents from the injected provider - Stamps
origin(read from the host-page’swindow.location.origin) onto every message - Forwards the stamped message to the service worker via
chrome.runtime.sendMessage - Forwards the service worker’s response back to the page via
window.postMessage
The page can never read or modify the stamped origin because it never sees the post-stamp message — only the service worker does.
Injected provider
src/inject/xchainProvider.js runs in the page world. It exposes window.xchain and proxies calls back through the content-script relay. The provider is a thin shim — every method ends in a postMessage to the content script, and every callback resolves on a matching response.
The injected script is built deterministically: same source → same bytes. Reproducible-build verification (see Reproducible Builds) covers it as part of the pre-signing artifact.
Approval window
src/approval/main.jsx renders one of several approval surfaces depending on the operation:
| Approval kind | When |
|---|---|
connect |
Origin requesting connect |
signMessage |
Origin requesting message signature |
signPsbt |
Origin requesting PSBT signature |
signAction |
Origin requesting XChain action signature |
sendAction |
Origin requesting sign + broadcast |
signIn |
Origin requesting Sign-In with XChain |
internalSign |
Wallet itself initiating a sign (Send, Issue, etc.) |
Each approval surface is built from the same review pattern as the wallet’s internal sign screens: form values + decoded summary + raw PSBT bytes, with an explicit user-confirm. See Security & Threat Model — Sign-screen safety rails.
The approval window runs in the extension’s origin (not the page’s), so the page cannot inject script into it or read its state.
Storage layout
| Namespace | Contents |
|---|---|
chrome.storage.local |
The encrypted Vault blob; salt + Argon2id parameters; settings; cosigner-side multisig session state |
chrome.storage.session |
The session master key (cleared on browser close) |
Both are subject to Chrome’s ~10 MB per-extension quota. The wallet’s typical state (a wallet with 100s of addresses, 1000s of contacts, and active multisig sessions) fits well inside the quota; vault size is monitored as part of the smoke suite.
Privacy policy + CWS
packages/extension/PRIVACY_POLICY.md is the public-facing policy hosted alongside the CWS listing. It covers:
- What’s stored on-device (encrypted vault, addresses, contacts, dApp grants, queued PSBTs)
- What leaves the device (user-configured RPC endpoints; optional vendor hardware-bridge calls)
- Permission justifications
- Camera-scanner
getUserMediaruntime prompt - Absence of analytics, advertising, crash-reporting SDKs
- Absence of Google API integration
- CWS-mandated single-purpose + limited-use disclosures
The CWS submission playbook lives in the platform repo (gitignored) at claude/reports/specs/2026-04-24_cws-submission.md. CWS submission is one of the three remaining user-driven items before v1.0.0 GA.
Test runbook
packages/extension/docs/TEST_DAPP_RUNBOOK.md walks through a hands-on bridge round-trip:
- Build the extension (
pnpm --filter @xchain-wallet/extension build) - Load
packages/extension/dist/as an unpacked extension - Run the test-dapp (
pnpm --filter @xchain-wallet/test-dapp dev) - Click each bridge method in the test-dapp and verify the approval popup behavior
The runbook is the manual companion to the headless bridge-e2e.smoke.js suite — together they cover the bridge end to end.
Copyright © 2026 Dankest, LLC
Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-or-later). See LICENSE and NOTICE for full terms.