MetaMask-style host for the 1Shot Branding Layer: a thin MAIN-world EIP-1193 / EIP-6963 shim on the dApp page, with OWSProxy hosted Inline in a Chrome side panel / Firefox sidebar.
This avoids page frame-src CSP, hostile host CSS, and per-dApp third-party storage partitions.
Use of 1Shot-hosted public infrastructure is subject to the Public Infrastructure Terms and Acceptable Use Policy. By accessing or using those hosted services, you agree to those terms.
- Node.js 22+
- Chrome 116+ (side panel +
scripting.executeScriptMAIN world) - Firefox 128+ (MV3 + MAIN world + sidebar)
From the embedded-wallet repo root:
npm install
npm run dev:extension # Chrome HMR → extension/dist/chrome-mv3-devDo not use Vite serve (firefox-mv3-dev) for day-to-day Firefox testing.
Firefox’s extension CSP blocks Vite’s eval / wss://localhost:3000 HMR, so
sidepanel/options modules often never run (buttons appear dead, sidebar stuck on
“Loading wallet…”).
npm run build:firefox -w @1shotapi/oneshot-wallet-extension
# same as: npm run dev:firefox -w @1shotapi/oneshot-wallet-extension
# → extension/dist/firefox-mv3 (self-contained CSS/JS)Then about:debugging → Load Temporary Add-on →
extension/dist/firefox-mv3/manifest.json. After code changes, rebuild and
click Reload on the temporary add-on (or remove/re-add).
Legacy Vite serve (broken under Firefox CSP) remains as
npm run dev:firefox:vite -w @1shotapi/oneshot-wallet-extension →
firefox-mv3-dev only.
Open the extension Settings and set Wallet iframe URL to a full URL
(e.g. https://immune-sheep-light.ngrok-free.app/). Default is
https://wallet.1shotapi.com/.
Load the unpacked extension:
- Chrome (dev):
chrome://extensions→ Load unpacked →extension/dist/chrome-mv3-dev - Chrome (stable build):
extension/dist/chrome-mv3 - Firefox (recommended): load
extension/dist/firefox-mv3/manifest.json(frombuild:firefox/dev:firefox) - Firefox Vite-dev (unsupported):
extension/dist/firefox-mv3-dev— CSS may load after CSP tweaks, but entrypoint JS regularly dies; preferfirefox-mv3.
The side panel / sidebar is the wallet UI (Branding iframe). It does not automatically put a provider on the dApp. Injection is explicit:
- Open the dApp tab (or the host Injected playground).
- In the extension side panel top bar, click Inject (or the toolbar popup → Inject on this page). Grant host permission if prompted.
- Optionally Always / Always inject on this origin for reload auto-inject.
- Confirm in the page console:
window.ethereum?.is1Shot === true, and/or- EIP-6963 announce for
com.1shotapi.wallet
- Connect in the dApp. If MetaMask is also installed, enable Prefer 1Shot as window.ethereum in extension Settings, or pick 1Shot from an EIP-6963 wallet list (Uniswap’s shortcut strip may only highlight MetaMask).
Approve / sign stays in the side panel.
Firefox note: Keep the side panel open while connecting. The extension only
opens the sidebar when it has no live panel connection — calling
sidebarAction.open() on an already-open panel can reload it and drop Connect RPCs.
npm run dev:host
npm run build -w @1shotapi/oneshot-wallet-extension # reload the temp add-onOpen the host → sidebar mode Injected → extension Inject on that tab →
Connect. That page never creates OWSProxy; it only talks to the injected
provider.
npm run build:extension # Chrome MV3 → extension/dist/chrome-mv3
npm run build:firefox -w @1shotapi/oneshot-wallet-extension
npm run pack:chrome -w @1shotapi/oneshot-wallet-extension # zip for CWS
npm run pack:firefox -w @1shotapi/oneshot-wallet-extension # zip for AMOnpm run pack:chrome -w @1shotapi/oneshot-wallet-extension- Upload the zip from
extension/.output/(WXT zip output) in the Chrome Developer Dashboard. - Publish as Unlisted for testers.
npm run pack:firefox -w @1shotapi/oneshot-wallet-extension- Sign with AMO “On your own” / self-distributed listing (JWT API credentials), or upload for signing and download the
.xpi. - Extension id:
wallet-extension@1shotapi.com(seebrowser_specific_settings.gecko).
dApp MAIN world content script service worker side panel
───────────────── ────────────── ────────────── ──────────
inpage.js shim ←post→ bridge ←msg→ router / queue ←port→ OWSProxy Inline
window.ethereum openWalletUi() Branding iframe
EIP-6963 announce allowlist inject
@1shotapi/ows-provideris only bundled into the side panel page.- MAIN-world inject uses
chrome.scripting.executeScript({ world: "MAIN" })so pagescript-srcCSP does not block the shim.
- No page scraping or analytics beyond what the Branding iframe already does for wallet UX.
- Scripts inject only after user action or for allowlisted origins the user added.
- Use Always (side panel) so the provider re-injects after a dApp tab refresh. One-shot Inject does not survive reload — without Always you must Inject again before Connect will find 1Shot.
- Optional host permissions are requested per origin when injecting.
The side panel is shared by every tab, so the background keeps connections
per origin, taken from the browser's sender metadata and never from the
page:
- Every routed request carries the requesting origin. Prompting methods and
connection-scoped reads run one at a time in the panel and go to the
Branding Layer through its
forwardEip1193RPC with that origin, so the wallet's own consent badge, analytics, and EIP-7715 grant scoping name the dapp instead of the extension id. Chain reads (eth_call,eth_chainId, …) go straight through and never wait behind a prompt. - The panel header shows
Request from <origin> · <method>for the request the wallet is handling right now (not merely the oldest pending one) and highlights it when that is not the focused tab. eth_requestAccounts/wallet_requestPermissionsfrom an origin that has not connected yet show an Allow / Deny prompt in the panel chrome before the Branding Layer sees the request.- Until an origin is connected,
eth_accounts,wallet_getPermissionsandwallet_getGrantedExecutionPermissionsreturn[], and signing / chain / EIP-7715 grant and revoke methods are refused with EIP-11934100instead of opening a prompt. accountsChanged/chainChanged/connect/disconnectgo only to injected tabs whose current origin is connected.
Connections live in storage.session and reset when the browser closes.
- Closing the side panel destroys the
OWSProxysession; the next RPC reopens the panel and recreates the proxy (wallet storage under the extension top-level partition should restore accounts). - Extension page CSP must allow framing your wallet URL (
https:and localhost are allowed in this test build). - Safari
/create/handoff still opens from Branding inside the panel iframe — allow popups from the extension page if prompted. The create tab does not requirewindow.opener(Safari Web Extension often omits it); the result returns on a same-originBroadcastChannel.
- Inject on a page with strict
frame-src 'self'— Connect Wallet still works (iframe is only in the side panel). - MetaMask installed: EIP-6963 lists 1Shot; Prefer 1Shot toggles
window.ethereum. - MetaMask absent:
window.ethereumis 1Shot after inject. - Allowlist origin → reload → shim present without clicking Inject.
- Settings wallet URL → ngrok Branding → panel loads and unlock/connect works.
-
eth_requestAccounts/ send opens and focuses the side panel.