Contractless-Web3-Wallets/extension/README.md

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_detect
  • contractless_connect
  • contractless_accounts
  • contractless_balances
  • contractless_disconnect
  • contractless_signMessage
  • contractless_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