Browser Extensions
RougeChain provides browser extensions that serve as quantum-resistant wallets, similar to how MetaMask works for Ethereum — but using post-quantum cryptography.
Available Extensions
| Extension | Description | Store |
|---|---|---|
| RougeChain Wallet | Primary browser extension | Chrome Web Store |
Features
- Wallet — View balances, send/receive XRGE, claim faucet, custom token support
- Encrypted Messenger — E2E encrypted chat using ML-KEM-768 + ML-DSA-65
- PQC Mail — Encrypted email with
@rouge.quantaddresses - Vault Lock — AES-256-GCM encryption with PBKDF2 key derivation and auto-lock timer
- Cross-browser — Chrome, Edge, Brave, Opera, Arc, Firefox (Manifest V3)
Installation
From Chrome Web Store
- Visit the extension page on the Chrome Web Store
- Click Add to Chrome
- The extension icon appears in your toolbar
From Source
cd apps/extension
npm install
npm run build
- Open
chrome://extensions(oredge://extensions,brave://extensions) - Enable Developer mode
- Click Load unpacked
- Select the
apps/extension/distfolder
Firefox
cd apps/extension
npm install
npm run build
- Open
about:debugging#/runtime/this-firefox - Click Load Temporary Add-on
- Select
apps/extension/dist/manifest.json
DApp Integration
The extension (and the Qwalla mobile app's dApp browser) inject a window.rougechain
provider object, similar to MetaMask's window.ethereum — but the API is
RougeChain-specific, not the EVM/ethers API. In particular there is no
getAddress() and no getNetwork(); you obtain the user's public key by calling
connect().
Which sites get the provider: from extension 1.6.0, every https:// site and
http://localhost / http://127.0.0.1 — you don't need to ask us to add your domain. The first
connect() from a site opens a "Connect to this site?" prompt; nothing is shared until the user
approves, and every signature or send is approved separately. Users can review and disconnect sites
under Settings → Connected Sites. Plain http:// public sites are not supported. (Extension
1.5.x and earlier injected only on rougechain.io, rougee.app and localhost.)
⚠️ Common mistake: calling
window.rougechain.getAddress()throwse.getAddress is not a function. That method does not exist — RougeChain is not EVM-compatible. Useconnect()(below) and read.publicKey.
Detecting the extension and connecting
// 1. Detect
if (!window.rougechain?.isRougeChain) {
throw new Error("RougeChain wallet not found — install the extension or open in Qwalla.");
}
// 2. Connect (prompts the user, returns their public key)
const { publicKey, displayName, encryptionPublicKey } = await window.rougechain.connect();
console.log("Connected:", publicKey);
The provider may not be injected the instant your script runs. Either check for it
after the rougechain#initialized event, or poll briefly:
function getProvider(timeoutMs = 3000) {
return new Promise((resolve, reject) => {
if (window.rougechain?.isRougeChain) return resolve(window.rougechain);
const onReady = () => resolve(window.rougechain);
window.addEventListener("rougechain#initialized", onReady, { once: true });
setTimeout(() => reject(new Error("RougeChain wallet not detected")), timeoutMs);
});
}
Deriving a rouge1… address
connect() returns the public key (hex). If you need the short Bech32m
rouge1… address, derive it with the SDK:
import { pubkeyToAddress } from "@rougechain/sdk";
const { publicKey } = await window.rougechain.connect();
const address = await pubkeyToAddress(publicKey); // rouge1...
Provider API
| Method | Signature | Description |
|---|---|---|
isRougeChain | boolean | Property — true on the authentic provider. Use it to detect the wallet. |
connect() | → { publicKey, displayName?, encryptionPublicKey? } | Prompt the user to connect. Returns their public key. This replaces getAddress(). |
getBalance() | → { balance, tokens } | Get the connected wallet's XRGE balance and token balances. |
signTransaction(payload) | → { signature, signedPayload, publicKey, payload } | Sign a transaction payload with ML-DSA-65 (key never leaves the wallet). signedPayload is the exact JSON signed. |
signMessage({ message }) | → { signature, publicKey, address } | Extension 1.8.0+; Qwalla after its next update. Sign a text message (at most 4,096 bytes) to prove control of the wallet: logins and token gating. Never a transaction. The user approves every request. See Wallet Authentication. |
sendTransaction(payload) | → { txId, fee?, address?, preview? } | Sign and broadcast. A transfer by default; contract_call / contract_deploy payloads go to the contract endpoints. |
on(event, cb) | void | Subscribe to events (e.g. account/connection changes). |
removeListener(event, cb) | void | Remove an event listener. |
Signing a message (v1.8.0+)
signMessage proves that the visitor controls the connected wallet without sending anything to
the chain. Feature-detect it, because older extension versions and Qwalla builds do not have it:
if (typeof window.rougechain.signMessage !== "function") {
throw new Error("Update your RougeChain wallet to sign in.");
}
const { signature, publicKey, address } = await window.rougechain.signMessage({
message: "I agree to the rules of this raffle.",
});
// Server: verifyMessage(publicKey, message, signature) from @rougechain/sdk 1.13.0+
The site must be connected, and the wallet asks every time, showing your site and the whole
message. The signature is over "\x19RougeChain Signed Message:\n" + byte length + "\n" + message,
which a node never accepts as a transaction; a message that looks like a transaction payload
(JSON with a type / tx_type field) is refused, so keep using signTransaction for those.
For logins use the sign-in message format, which lets the wallet show the domain, nonce and
expiry and warn when the domain is not your site:
Wallet Authentication has the format and a complete token-gating
server.
Smart contract transactions (v1.4.0+)
The extension signs contract_call and contract_deploy payloads and shows a
contract-specific approval: the contract address, method, arguments, gas limit and
maximum fee for a call; the WASM size, predicted contract address and 10 XRGE fee for a
deployment. Missing from, timestamp and nonce are filled in, and the filled
payload is returned so you can submit it:
const { payload, signature, publicKey } = await window.rougechain.signTransaction({
type: "contract_call",
contractAddr: "<40-hex address>",
method: "move",
args: { x: 1, y: 2 },
gasLimit: 50000, // fee = gasLimit × 0.000001 XRGE
});
await fetch("https://api.rougechain.io/api/v2/contract/execute", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ payload, signature, public_key: publicKey }),
});
// or sign and submit in one step:
const { txId, preview } = await window.rougechain.sendTransaction({
type: "contract_call", contractAddr, method: "move", args: { x: 1 }, gasLimit: 50000,
});
Size the gas limit from a free query (POST /api/contract/<addr>/query) first. See
WASM Smart Contracts.
Payable calls (v1.5.0+; the node accepts them from block 190). A
contract_call may carry attach: { symbol, amount }. amount is a positive integer JSON
number: quanta for XRGE (1 XRGE = 1,000,000,000 quanta), raw units for tokens. The approval
view shows "Pays amount SYMBOL to the contract (only if the call succeeds)", with XRGE
formatted from quanta. For XRGE it also shows the max total cost, which is the gas fee plus the
payment. A non-integer, zero, negative or unsafe (> 2^53 − 1) amount is refused before the
approval opens.
await window.rougechain.sendTransaction({
type: "contract_call", contractAddr, method: "roll", args: {}, gasLimit: 50000,
attach: { symbol: "XRGE", amount: 500000000 }, // 0.5 XRGE, in quanta
});
Authenticity: the genuine provider also sets a non-enumerable
Symbol.for("rougechain:authentic") to true, so a dApp can guard against a page
that pre-defines a fake window.rougechain:
const authentic = window.rougechain?.[Symbol.for("rougechain:authentic")] === true;
Security
| Feature | Implementation |
|---|---|
| Vault encryption | AES-256-GCM with a PBKDF2-derived key (600k iterations); a password (min 8 chars) is mandatory at wallet creation |
| Auto-lock | Configurable timer via background service worker |
| Key storage | Encrypted blob in chrome.storage.local; the decrypted key is held only in chrome.storage.session (memory) and never written to disk. Legacy plaintext wallets are force-migrated to encrypted storage. |
| Signing | ML-DSA-65 (FIPS 204) — quantum-resistant |
| Encryption | ML-KEM-768 (FIPS 203) — quantum-resistant |
Permissions
The extensions request minimal permissions:
| Permission | Purpose |
|---|---|
storage | Store encrypted wallet data |
alarms | Auto-lock timer |
notifications | Transaction alerts |
| Host permissions | Connect to RougeChain nodes (rougechain.io, testnet.rougechain.io, localhost) |
Why Not MetaMask?
MetaMask and all EVM wallets use secp256k1 / ECDSA cryptography. RougeChain uses ML-DSA-65 / ML-KEM-768 (post-quantum). The key formats, signature schemes, and transaction structures are fundamentally incompatible. RougeChain's extensions are purpose-built for quantum-safe operations.
See PQC Cryptography for details on the algorithms used.