6.2 KiB
Contractless Browser Wallet
This is the WebExtension application for the Contractless Web3 wallet. It uses
the repository's core package for wallet generation, encryption,
transaction inspection, signing, message proofs, and wallet registration.
The private key is encrypted in extension-local storage. Decrypted key material exists only in the background service worker while the wallet is unlocked and is cleared when the wallet locks or the worker is suspended.
Requirements
Install:
- Rust and Cargo
wasm-pack- Node.js 20 or later
- pnpm
Install the JavaScript dependencies from this directory:
cargo install wasm-pack
pnpm install
Build
Build both browser packages:
pnpm run build
This compiles separate testnet and mainnet versions of the Rust core to WebAssembly and creates:
dist/chrome/
dist/firefox/
Build only one browser when developing:
pnpm run build:chrome
pnpm run build:firefox
Chrome and Firefox use the same TypeScript, HTML, CSS, SDK provider, and two network-specific WebAssembly cores. Each output receives its own manifest because Chrome uses a Manifest V3 background service worker while Firefox uses a Manifest V3 background event script.
Load in Chrome
Open chrome://extensions, enable Developer mode, select Load
unpacked, and choose dist/chrome.
Load in Firefox
Open about:debugging, select This Firefox, select Load Temporary
Add-on, and choose dist/firefox/manifest.json.
The Firefox build requires Firefox 149 or later. Firefox 149 removed the user
gesture restriction from action.openPopup, allowing website connection and
transaction requests to open the wallet approval interface automatically in
the same way as the Chrome build.
Run Mozilla's validator after building Firefox:
pnpm run lint:firefox
Create the Firefox submission archive:
pnpm run package:firefox
The archive is written to web-ext-artifacts. Firefox requires Mozilla signing
before the packaged extension can be installed permanently in standard
Firefox releases.
Safari requires conversion through Apple's Safari Web Extension tooling and Xcode.
Networks
The wallet presents a Testnet/Mainnet selector before wallet creation and unlock. Addresses, active wallets, WebAssembly transaction codecs, API URLs, and website sessions are isolated by the selected network. Switching networks locks the wallet and expires all application access keys.
Testnet uses .cltc addresses and defaults to
https://api.contractless.dev. Mainnet uses .clc addresses and has no API
configured by default. A mainnet wallet may still be created, imported, and
backed up while that endpoint is empty; network lookups, registration, and
transaction broadcasts remain unavailable until a mainnet API is configured.
Provider
Websites receive window.contractless with a Promise-based request method.
Applications should normally use the downloadable client in
../sdk/contractless.js instead of calling the provider directly:
import { ContractlessWallet } from "./contractless.js";
const wallet = new ContractlessWallet({
appId: "example-application"
});
const connection = await wallet.connect({
message: "Sign in to Example Application"
});
Transaction and message requests are routed through the background worker and wallet approval screen. Websites never receive wallet private keys or unrestricted raw-signing access. Single-signer transactions are signed and broadcast by the wallet. The transaction ID and signed bytes are returned only after the configured API accepts the broadcast.
Supported provider methods are:
contractless_detectcontractless_connectcontractless_accountscontractless_balancescontractless_disconnectcontractless_signMessagecontractless_sendTransaction
Connection, message signing, and transaction submission wait for an explicit approval or rejection. A successful connection creates a random 256-bit access key bound to the browser-derived origin, stable application ID, active wallet, and current wallet unlock session.
The website stores its copy in browser sessionStorage. The extension stores
the matching authorization in extension session storage. The key is reusable
until the wallet locks, changes active wallets, or the application
disconnects. It is not a permanent connected-site permission.
Every authorized request includes the claimed origin, application ID, and access key. The extension independently derives the real origin from the browser message sender and rejects mismatches.
See ../sdk/README.md for complete website integration examples.
Wallet registration
The Register wallet button creates the command-38 registration signature and the fixed API handshake proof locally. It sends the address, public key, and signatures to:
POST /api/v1/addresses/register
The private key and wallet encryption key never leave the extension. The API requires the authenticated address and public key to match the registration being submitted before forwarding it to a Contractless node.
The API operator must include the installed extension origin in
API_CORS_ORIGINS, for example:
API_CORS_ORIGINS=chrome-extension://extension-id
Keep any existing allowed website origins in the comma-separated list. Changing the API endpoint in the wallet prompts the user for permission to contact that HTTPS origin.
Firefox extension pages use a moz-extension:// origin. The production API
must allow the installed Firefox extension to make the same wallet requests as
the Chrome build.
Firefox Submission Source
Mozilla reviewers must receive the readable source because Vite bundles the
TypeScript and wasm-pack generates the WebAssembly package. Submit the built
Firefox archive as the add-on and a separate source archive containing at
least:
core/
extension/src/
extension/package.json
extension/pnpm-lock.yaml
extension/pnpm-workspace.yaml
extension/tsconfig.json
extension/vite.config.ts
README.md
Do not include extension/node_modules, extension/dist, or core/target in
the source archive. Reviewers can reproduce the Firefox build by entering the
extension directory and running:
pnpm install --frozen-lockfile
pnpm run build:firefox