WebRGB is the window.rgb provider a wallet injects into web pages so a dApp can issue, receive, send and track RGB assets, and pay or receive them over Lightning, without running its own RGB backend. It sits next to WebLN (window.webln) and WebBTC (window.webbtc) and follows the same conventions: one enable() consent per origin, a confirmation in the wallet for every funds-touching call, and errors that carry a code.
SPEC.md is the interface contract. The reference implementation is the KaleidoSwap browser extension from 0.3.0 on. This package ships the TypeScript declarations, discovery, an in-memory mock wallet and a conformance suite.
The playground imports this module and exercises every window.rgb method, with a live log of calls, results and error codes. Drive it against the extension with a signet or regtest wallet — or press install mock wallet and use it with nothing installed at all.
npm install @kaleidorg/webrgbImporting anything from the package augments Window, so window.rgb is typed everywhere. For WebLN typings use @webbtc/webln-types; for NIP-07 use nostr-tools.
import { requestProvider, isProviderError, supports, toAssetArray } from "@kaleidorg/webrgb";
try {
// Waits for the wallet if the page ran first; `enable` connects the origin.
const rgb = await requestProvider({ enable: true });
const info = await rgb.getInfo();
// { ready, network, protocol: "RGB_L1" | "RGB_LN", methods: [...] }
const assets = toAssetArray(await rgb.listAssets());
const { invoice } = await rgb.blindReceive({ assetId: assets[0].id!, amount: 1 });
if (supports(info, "makeLnInvoice")) {
const ln = await rgb.makeLnInvoice({ assetId: assets[0].id!, assetAmount: 5 });
console.log("pay me over Lightning:", ln.invoice);
}
rgb.on("transferSettled", (t) => console.log("settled", t.transferId));
} catch (err) {
if (isProviderError(err) && err.code === "USER_REJECTED") return; // the user said no
throw err;
}| Method | Purpose |
|---|---|
enable() / getInfo() |
Connect the origin; learn network, runtime and served methods |
getAddress() |
Bitcoin address that anchors the wallet's RGB state |
blindReceive(args?) |
Blinded-UTXO receive invoice; omit assetId for any asset, including one the wallet has never held |
witnessReceive(args?) |
Witness receive invoice: the sender funds the UTXO, so it works on an empty wallet |
createUtxos(args?) / getBtcBalance() |
Create colorable UTXOs from on-chain bitcoin; read vanilla and colored balances and how many UTXOs are free |
cancelReceive(recipientId) |
Cancel a pending receive and release its UTXO |
refresh(id?) / getAssetMetadata(id) |
Sync pending transfers; read an asset's contract data |
signPsbt(psbt, opts?) |
Sign the wallet's bitcoin-only inputs in a PSBT |
issueAsset(args) |
Mint a new asset (gated by its own wallet capability) |
listAssets() / getAssetBalance(id) |
Holdings |
sendAsset(args) |
Send against an RGB invoice, or explicitly |
listTransfers(id?) / getTransferStatus(id) |
Transfer history and status |
decodeRgbInvoice(invoice) |
What an invoice asks for, before you pay it — read-only, no prompt |
makeLnInvoice(args) / payLnInvoice(args) |
RGB over Lightning; listed in methods only when the wallet has a Lightning node |
on / off |
transferReceived and transferSettled events |
Feature-detect with supports(info, method) rather than assuming a method exists: a node-less wallet rejects the Lightning methods with METHOD_NOT_SUPPORTED.
listAssets() and listTransfers() are typed wide because wallets differ on whether they wrap the array — pass them through toAssetArray() / toTransferArray().
A blinded invoice needs a free colorable UTXO. A fresh wallet, or one whose UTXOs all hold assets, has none and rejects with NO_AVAILABLE_UTXOS. A faucet-style page can then ask the wallet to create some, or fall back to a witness invoice, which the sender funds:
import { providerErrorCode, supports } from "@kaleidorg/webrgb";
import type { RgbInfo, RgbProvider } from "@kaleidorg/webrgb";
async function receiveInvoice(rgb: RgbProvider, info: RgbInfo): Promise<string> {
try {
return (await rgb.blindReceive()).invoice;
} catch (err) {
if (providerErrorCode(err) !== "NO_AVAILABLE_UTXOS") throw err;
if (supports(info, "witnessReceive")) return (await rgb.witnessReceive()).invoice;
await rgb.createUtxos(); // spends on-chain bitcoin; the wallet asks first
return (await rgb.blindReceive()).invoice;
}
}getBtcBalance().freeColorableUtxos tells you up front, and cancelReceive(recipientId) releases the UTXO an abandoned invoice reserved.
signPsbt(psbt) signs only inputs the wallet controls, and only bitcoin-only ones. Spending a UTXO that carries RGB assets outside an RGB state transition destroys the assets, so a wallet rejects such a PSBT with UNSAFE_PSBT before showing a prompt. Do not build PSBTs over the wallet's colored UTXOs; RGB-aware PSBT flows are for a later version.
window.rgb is a single slot, so two installed wallets cannot both own it. Wallets also announce themselves, EIP-6963 style, and listProviders() collects the answers:
import { listProviders } from "@kaleidorg/webrgb";
for (const { info, provider } of await listProviders()) {
console.log(info.name, info.rdns); // show a picker, then use `provider`
}requestProvider() resolves whichever comes first — an installed window.rgb, an announcement, or the legacy rgb:ready event — so a dApp that wants one wallet needs nothing else.
Wallet side, one call joins discovery:
import { announceProvider } from "@kaleidorg/webrgb";
announceProvider({
info: { uuid: crypto.randomUUID(), name: "Example", rdns: "com.example.wallet" },
provider,
});@kaleidorg/webrgb/mock is an in-memory provider that enforces the same rules a real one does — NOT_ENABLED before enable(), METHOD_NOT_SUPPORTED for anything absent from getInfo().methods, a confirmation step you can make refuse:
import { createMockProvider, installMockProvider, mockPsbt } from "@kaleidorg/webrgb/mock";
// In a test:
const rgb = createMockProvider({ protocol: "RGB_LN", assets: [{ id: "rgb:x", balance: 100 }] });
await rgb.enable();
const sent = await rgb.sendAsset({ assetId: "rgb:x", amount: 1, recipientId: "utxob:y" });
rgb.settle(sent.transferId!); // fires transferSettled
expect(rgb.calls.map((c) => c.method)).toContain("sendAsset");
// signPsbt cannot really sign; it reads PSBTs built with mockPsbt().
await rgb.signPsbt(mockPsbt({ inputs: [{ mine: true }] }));
// In a dev build: put it on window.rgb and let the app find it as usual.
const { uninstall } = installMockProvider();@kaleidorg/webrgb/conformance checks a live wallet against SPEC.md using read-only calls only, so it raises no confirmation:
import { runConformance, formatReport } from "@kaleidorg/webrgb/conformance";
console.log(formatReport(await runConformance(window.rgb!)));| Code | Meaning |
|---|---|
USER_REJECTED |
The user declined the connection or the confirmation prompt |
NOT_ENABLED |
Called before enable() resolved for this origin |
METHOD_NOT_SUPPORTED |
The connected wallet cannot serve this method, or no provider was found |
INVALID_PARAMS |
An argument is malformed or out of range; error.message names it |
ASSET_NOT_FOUND |
The call names an asset the wallet does not know — for blindReceive, omit assetId |
NO_AVAILABLE_UTXOS |
No free colorable UTXO — call createUtxos() or use witnessReceive() |
UNSAFE_PSBT |
signPsbt was asked to sign an input that holds RGB assets |
INTERNAL_ERROR |
Anything else; see error.message |
The error crosses a postMessage boundary on its way out of the wallet, so what you catch is a plain Error carrying code — instanceof will not help. Use isProviderError(err), or providerErrorCode(err) for a switch that must be total.
The declarations mirror SPEC.md and the extension's src/injected.ts. A method added to the provider lands here as a minor bump; a changed signature as a major bump. If the two disagree, trust the wallet and open an issue.
npm install
npm test # tsc over the declarations and index.js, plus the runtime tests
npm run test:package # are-the-types-wrong + publint against the packed tarballMIT