The IronDivi Wallet extension (manifest version 0.0.1) is not in any browser store and has no public download. Everything below was read from its source code, file by file, and the fact check names the line for every claim. When the extension ships, this page will be re-run against it. Until then, treat it as a specification you can code against, not a working example.
1What the provider is
The IronDivi Wallet extension holds the user's keys. Your page never sees them. Instead the extension injects a small object, window.irondivi, into pages it trusts. Your page calls methods on that object; the extension asks the user to approve in its own popup window; the answer comes back as a resolved promise. It is the same shape as window.ethereum for Ethereum dApps, and the source says so in its first comment.
Under the hood, three pieces talk to each other:
- The injected provider (
src/injected/provider.ts) runs in your page. Every call becomes awindow.postMessageof typeIRONDIVI_PROVIDER_MESSAGEwith a random UUID as itsid. - The content script (
src/content/content-script.ts) sees that message, forwards it to the extension's background worker with your page's origin attached, and posts the reply back asIRONDIVI_PROVIDER_RESPONSEwith the sameid. - The background service worker (
src/background/service-worker.ts) checks the origin, checks that your site is connected, opens the approval popup where needed, signs, and answers{ success, data, error }.
window.irondiviisIronDivi === true'1.0.0'0.0.1The two version numbers are different things: window.irondivi.version is the provider API's own string; 0.0.1 is the extension package in its manifest.
Divi dApps connect through this injected provider, and fall back to the web wallet's DIVI_SIGN_* bridge (section 12). There is no third-party relay, no vendor account and no WalletConnect. That is a recorded decision, ADR 0001 (docs/decisions/0001-no-walletconnect.md in the divi-infrastructure and extension repositories), and this repository carries a test (scripts/check-no-walletconnect.sh) that fails the build if it comes back.
2Where the provider appears
The extension does not inject into every page. The content script checks two things before it adds window.irondivi, and both must pass:
| Check | Rule |
|---|---|
| Scheme | https: on any host, or http: only on localhost or 127.0.0.1. file:// pages never get the provider. |
| Site allowlist | The hostname must match a pattern in the extension's allowed sites setting. The default list is *.divi.domains, localhost, 127.0.0.1. In a pattern, * matches anything except a dot and ** matches anything, so *.divi.domains covers services.divi.domains but not a.b.divi.domains. |
So, while you develop, serve your page on http://localhost. Opening the HTML file straight from disk will not work. One line is enough:
python3 -m http.server 8000 # then open http://localhost:8000/
localhost, not 127.0.0.1
The content script injects on both, but the background worker's own origin check accepts plain http only for the hostname localhost. On http://127.0.0.1 the provider appears, and then connect() fails with Invalid origin.
A user can add your production hostname to the allowlist in the extension's settings; a pattern like *.divi.domains or yourapp.example. Until they do, your page sees no provider, which is the "extension isn't there" case in section 12.
3Detect it
The content script runs at document_start, but it has to read the allowlist from extension storage before it injects, so your script may run first. The provider fires a irondivi#initialized event on window as soon as it is attached, and fires it again on DOMContentLoaded for listeners that registered late. Handle both orders:
function getProvider() {
const p = window.irondivi;
return p && p.isIronDivi === true ? p : null;
}
// Already there?
let provider = getProvider();
// Or arriving shortly.
window.addEventListener('irondivi#initialized', () => {
provider = getProvider();
});
Give up after a short wait rather than never. The NFT marketplace's bridge waits 500 ms for the extension before it falls back to the web wallet.
isIronDivi matters
The older PrivateDivi extension injected window.pdivi with isPrivateDivi: true. If you support both, prefer window.irondivi when both are present; that is what the marketplace does.
4Connect and get the address
connect() asks the extension to link your origin to the user's selected account. The extension opens a 400 × 600 approval popup; the user presses Approve or Reject. The promise resolves to an object with the account's first address:
const { address } = await window.irondivi.connect();
// address: 'y…' on testnet, 'D…' on mainnet
const connected = await window.irondivi.isConnected(); // true
const accounts = await window.irondivi.getAccounts(); // ['y…', 'y…', …] every address in the account
await window.irondivi.disconnect(); // forgets this origin
What you actually get back is { address, connected: true }. The provider's own TypeScript type on connect() also lists an optional publicKey, but nothing in the extension fills it in; do not rely on it.
Approval is remembered per origin and per account, in the extension's storage, with two permissions: sign_message and send_transaction. The next connect() from the same origin resolves at once, with no popup. The user can revoke a site from the extension's settings at any time; your page gets no event when that happens (see Events), so check isConnected() before anything that needs the connection.
If the user rejects, connect() throws Connection rejected by user. If the popup sits for five minutes, it throws Connection request timed out. If no account exists in the extension yet, it throws No account selected.
5Read the network and the balance
const balance = await window.irondivi.getBalance(); // a number, in DIVI, for the whole account const network = await window.irondivi.getNetwork(); // see below
Balance. The extension asks its RPC backend getaddressbalance for every address in the account, adds the satoshis up and divides by 100,000,000. The optional address argument is sent along but the background ignores it: you always get the account total. The backends are the public Divi RPC proxies, https://services.divi.domains/api/testnet/rpc/ for testnet and https://services.divi.domains/api/rpc/ for mainnet; see JSON-RPC from Python and JavaScript for what else they answer.
Network. The extension starts on testnet and the user switches from the badge in the popup header or . Each network has its own accounts, because the derivation path differs: m/44'/1'/0'/0/i on testnet and m/44'/301'/0'/0/i on mainnet. Testnet addresses start with x or y (prefix 0x8b); mainnet with D (prefix 0x1e).
getNetwork() always answers 'testnet'
The provider reads settings.network from the GET_SETTINGS reply, but the background wraps that reply one level deeper, as { settings: { network, … } }. The lookup finds nothing and the fallback, 'testnet', is returned on both networks. Until that is fixed, read the network through the raw form instead:
const { settings } = await window.irondivi.request({ method: 'irondivi_getNetwork' });
console.log(settings.network); // 'mainnet' or 'testnet'
Or simply look at the first character of the address connect() gave you.
connect()
Only connect, isConnected and disconnect are origin-gated, and only signing and sending check that the site is connected. getBalance(), getAccounts() and the settings read answer any page the provider was injected into. For a user that means any allowlisted site can read their addresses and balance silently; for you it means these calls work before, and after, the connection.
6Sign a message, then verify it on your server
This is the building block of sign-in with Divi: your server hands out a one-time text, the user signs it, and the server checks the signature against the address. Only a connected site may ask, and every request is approved in the popup with the wallet password.
const message = 'Sign in to example.com, nonce 8f3a…, 2026-10-21T10:00Z'; const signature = await window.irondivi.signMessage(message); // a base64 string, 88 characters, for example: // IFSojv2ii5slKIkNnRcdQ4ythbUA5vGcvcSs014+aEkTa5ig6oiyh9xAhFfSFcEJQJ3ViKFEI7++9oxgoZkEOec=
Before it posts the request, signMessage calls isConnected() and throws Not connected. Call connect() first. if you skipped step 4. The second argument, address, is accepted but not sent: the extension always signs with address index 0, the same address connect() returned. So the pair to verify is always (connect().address, signature).
The signature is the ordinary Divi message signature: the text is prefixed with "DarkNet Signed Message:\n", double-SHA256'd and signed with a recoverable key, 65 bytes, base64. That is exactly what the C++ node's signmessage makes and what verifymessage checks, on the C++ node and on the IronDivi node alike. So your server needs no crypto library, only one RPC call to a node. The public testnet proxy will do:
curl -s https://services.divi.domains/api/testnet/rpc/ \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"verifymessage",
"params":["y7QrsJED7LFSkGjTbY9NkknQjdBGcTv1w2",
"IFSojv2ii5slKIkNnRcdQ4ythbUA5vGcvcSs014+aEkTa5ig6oiyh9xAhFfSFcEJQJ3ViKFEI7++9oxgoZkEOec=",
"I own this address: tutorial 74"]}'
{"error":null,"id":1,"result":true}
That is a real call we made on 2026-10-21. The signature was produced by running the extension's own signing code (src/lib/signer.ts) with a throwaway testnet key, since the extension itself cannot be installed; the node, a Divi Core 3.0.0 behind the proxy, verified it. Change one character of the message and the answer is false. The node answers Invalid address for a bad address and Malformed base64 encoding for a bad signature.
In Node.js on your server, the same call:
async function verify(address, signature, message) {
const r = await fetch('https://services.divi.domains/api/testnet/rpc/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'verifymessage',
params: [address, signature, message] }),
});
return (await r.json()).result === true;
}
Put a nonce and a time in every message you ask the user to sign, and never accept a signature over a message you did not generate. Sign and verify a message covers the same signature from the wallets' side.
7Send a testnet transaction
sendTransaction builds, signs and broadcasts a payment from the user's account and resolves to the txid:
const txid = await window.irondivi.sendTransaction({
to: 'yJehBfdK9KbrwPRjYrzkrPES9uG5nFLjY4', // the testnet faucet's return address
amount: 1.5, // DIVI, not satoshis
// fee: 0.00005, optional, in DIVI; this is the default
// fromAddresses: ['y…'], optional: spend only from these
});
console.log('https://services.divi.domains/testnet/explorer2/tx/' + txid);
Unlike connect(), signMessage() and signRawTransaction(), a sendTransaction from a page does not open the approval window. The background checks that your origin is connected and holds send_transaction permission (granted at connect), applies the spending limits, and then signs with the wallet's current session. If the user has typed their password in the popup within the last five minutes (the authTimeoutMinutes default), the coins go out immediately. If not, the call fails with Not authenticated and nothing is sent. Design for that: tell the user to unlock the extension, then retry.
The default spending limit is 10% of the account balance per transaction. Above it the call fails with Transaction exceeds 10% of balance (max: … DIVI); an absolute limit, if the user set one, fails with Transaction exceeds maximum amount limit of … DIVI. Keep your test amounts small. Get testnet coins from the faucet into the extension first (testnet in the extension and web wallet shows how), and send what you don't need back to yJehBfdK9KbrwPRjYrzkrPES9uG5nFLjY4.
8Sign a raw transaction
For anything sendTransaction can't express (multiple recipients, an OP_RETURN, a multisig you are co-signing, an atomic swap) build the transaction yourself and ask the extension to sign only the inputs that belong to the user. It returns signed hex and never broadcasts; you decide when and whether to send it. Raw transactions and multisignature shows how to build the hex.
const { hex, signedIndexes } = await window.irondivi.signRawTransaction(
txHex, // the unsigned (or partly signed) transaction, hex
[ // one entry per input, in input order
{ txid: '…', vout: 0, amount: 12.5, scriptPubKey: '76a914…88ac' },
],
[0], // which input indexes to sign
0x01 // hashType; default 1 = SIGHASH_ALL
);
amountis in DIVI, as the legacy RPC reports it;scriptPubKeyis hex. The background fetches each prevout's transaction from its RPC backend and checks that amount and script match before it shows anything to the user.- Exactly three hash types are supported:
0x01ALL,0x81ALL|ANYONECANPAY and0x83SINGLE|ANYONECANPAY. Only P2PKH inputs can be signed. - Every input you list must pay one of the account's addresses, or the call fails with
Input N is not owned by this account. - There is no standing permission for raw signing: every request opens the popup, shows the decoded inputs, outputs, fee and warnings, and needs the wallet password.
9Events
function onConnect({ address }) { console.log('connected as', address); }
window.irondivi.on('connect', onConnect);
window.irondivi.on('disconnect', () => console.log('disconnected'));
window.irondivi.off('connect', onConnect);
Two events exist, connect (with { address }) and disconnect (no argument). They fire only when your page's own connect() or disconnect() call succeeds. Nothing fires when the user revokes the site in the popup, switches account or network, or locks the wallet. A listener that throws is caught and logged; it won't break the others.
10The request() form
Like EIP-1193 providers, window.irondivi.request({ method, params }) takes a method name and passes params through untouched. It skips the convenience methods' extras: no isConnected() pre-check, no connect event, no unwrapping of the reply. Unknown names throw Unknown method: ….
| Method | Params | Resolves to |
|---|---|---|
| irondivi_connect | none | { address, connected: true } |
| irondivi_disconnect | none | undefined |
| irondivi_accounts | none | [{ index, address, path }, …]; getAccounts() maps this to addresses |
| irondivi_getBalance | none used | { balance } in DIVI |
| irondivi_getNetwork | none | { settings: { network, authTimeoutMinutes, currency, hideBalance, allowedSites, transactionLimits } } |
| irondivi_signMessage | { message, addressIndex? } | { signature } |
| irondivi_sendTransaction | { to, amount, fee?, fromAddresses? } | { txid, fee } |
| irondivi_signRawTransaction | { tx, prevouts, indexes, hashType } | { hex, signedIndexes } |
Note one difference from signMessage(): the raw form lets you pass addressIndex, so request({ method: 'irondivi_signMessage', params: { message, addressIndex: 2 } }) signs with the account's third address. The user sees the same approval popup.
11A complete minimal page
Save this as index.html, serve it with python3 -m http.server 8000, open http://localhost:8000/. No build step, no dependencies. It detects the provider, connects, shows the balance, signs a message and sends a small testnet payment.
<!DOCTYPE html>
<meta charset="utf-8">
<title>IronDivi provider demo</title>
<button id="connect" disabled>Connect</button>
<button id="sign" disabled>Sign a message</button>
<button id="send" disabled>Send 0.1 testnet DIVI to the faucet</button>
<pre id="out">Looking for window.irondivi …</pre>
<script>
const out = document.getElementById('out');
const log = (...a) => { out.textContent += '\n' + a.join(' '); };
const $ = (id) => document.getElementById(id);
let provider = null;
function found() {
if (provider || !(window.irondivi && window.irondivi.isIronDivi)) return;
provider = window.irondivi;
log('provider found, version', provider.version);
$('connect').disabled = false;
provider.on('connect', ({ address }) => log('event: connect', address));
provider.on('disconnect', () => log('event: disconnect'));
}
found();
window.addEventListener('irondivi#initialized', found);
setTimeout(() => { if (!provider) log('no extension after 1 s: see section 12'); }, 1000);
$('connect').onclick = async () => {
try {
const { address } = await provider.connect();
log('connected as', address);
const { settings } = await provider.request({ method: 'irondivi_getNetwork' });
log('network:', settings.network);
log('balance:', await provider.getBalance(), 'DIVI');
$('sign').disabled = false;
$('send').disabled = settings.network !== 'testnet';
} catch (e) { log('connect failed:', e.message); }
};
$('sign').onclick = async () => {
const message = 'demo login ' + crypto.randomUUID() + ' ' + new Date().toISOString();
try {
const signature = await provider.signMessage(message);
log('signature:', signature);
log('verify with: verifymessage <address> <signature> "' + message + '"');
} catch (e) { log('sign failed:', e.message); }
};
$('send').onclick = async () => {
try {
const txid = await provider.sendTransaction({
to: 'yJehBfdK9KbrwPRjYrzkrPES9uG5nFLjY4', amount: 0.1,
});
log('sent: https://services.divi.domains/testnet/explorer2/tx/' + txid);
} catch (e) { log('send failed:', e.message); }
};
</script>
The Send button stays disabled unless the network read says testnet, so the demo cannot spend real DIVI by accident. In the real thing, verify the signature on your server (step 6) rather than printing it.
12If the extension isn't there: the web wallet bridge
The IronDivi web wallet (/testnet/wallet/, /wallet/) can sign a message for another page through postMessage, with no extension involved. This is Divi's second connection mechanism, the fallback ADR 0001 names for the NFT marketplace. It signs messages only; it does not send transactions. The protocol, from divi-wallet/lib/messaging.js:
- Your page opens the wallet in a popup window with
window.openand waits for it to load. - Your page posts
{ type: 'DIVI_SIGN_REQUEST', payload: { message, requestId } }into that window.messageis a non-empty string of at most 4096 characters;requestIda non-empty string of at most 128, which you generate. - The wallet checks
event.originagainst a fixed allowlist, shows the user the message and your origin in its Sign panel, and when the user signs, posts back toevent.sourceeither{ type: 'DIVI_SIGN_RESPONSE', payload: { signature, address, requestId } }or{ type: 'DIVI_SIGN_CANCEL', payload: { requestId, reason } }(reason'user_rejected'by default). - Your page matches the
requestId, checksevent.originis the wallet's andevent.sourceis the window it opened, then verifies the signature exactly as in step 6.
In production the wallet answers only pages on https://services.divi.domains and https://vps1.divi.domains. The localhost and 127.0.0.1 dev origins on ports 3000, 5173 and 8080 are accepted only when the wallet itself is running on localhost. A request from any other origin is dropped with a console warning and no reply, so your page must time out on its own. The extension's allowlist, by contrast, lives in the user's settings and they can extend it.
Both mechanisms are described, with the reasons, in ADR 0001 (docs/decisions/0001-no-walletconnect.md). The order a dApp should try is: injected provider, then the web wallet bridge, then letting the user paste a signature they made by hand in any wallet (sign and verify).
If something goes wrong
| What you see | What it means |
|---|---|
window.irondivi is never defined | The page is not on an allowed origin: it must be https, or http://localhost / http://127.0.0.1, and the hostname must match the extension's allowed-sites list (default *.divi.domains, localhost, 127.0.0.1). file:// never works. Also make sure you listen for irondivi#initialized rather than checking once at load. |
Invalid origin | The background accepts plain http only for localhost. You are probably on http://127.0.0.1; use http://localhost instead. |
Not connected. Call connect() first. | Thrown by the provider before it even asks the extension: signMessage, signRawTransaction and sendTransaction need a successful connect() first. |
Site not connected. Please connect first. | The same check, made by the background. The user may have revoked your site in the popup since you connected; call connect() again. |
Connection rejected by user / Request rejected by user | The user pressed Reject in the popup. Offer to try again; don't loop. |
Connection request timed out / Request timed out | The popup sat unanswered for 5 minutes (the background's timer). The pending request is dropped. Ask again. |
Request timeout | The provider's own 5-minute timer in your page expired with no reply at all: the content script or background never answered. Reload the page; if the extension was just updated, reload it too. |
Wallet locked | The wallet locked while your request was pending; every pending request is rejected with this. Ask again once the user unlocks. |
Not authenticated | sendTransaction found no unlocked session. The user must enter their password in the extension popup (it stays unlocked for authTimeoutMinutes, default 5), then your page retries. |
No account selected | The extension has no wallet yet, or none selected. The user creates or picks one in the popup. |
Transaction exceeds 10% of balance (max: … DIVI) | The default per-transaction limit. Send less, or the user raises the limit in Settings. |
Site does not have send_transaction permission | The connection exists but without that permission. Today connect() always grants both permissions, so this only appears after the stored connection was edited. |
Wrong network: D… address when you expected y…, or getNetwork() says 'testnet' while the user is on mainnet | The user switches network in the popup header; your page cannot. Do not trust getNetwork() in this source (step 5); read request({ method: 'irondivi_getNetwork' }).settings.network or the address prefix, and refuse to send on the wrong network. |
Input N is not owned by this account | signRawTransaction: an index you asked it to sign spends a prevout that does not pay one of the account's addresses. |
Unknown method: … | request() only knows the eight irondivi_* names in step 10. |
Still stuck? Ask the Divi chatbot, or see all tutorials.
Next
- Sign in with Divi: turn step 6 into a login flow.
- Sign and verify a message: the same signature from every wallet's side.
- Talk to a Divi node from Python and JavaScript: everything else the public RPC proxies answer.
- Raw transactions and multisignature: build the hex for step 8.
- Testnet in the web wallet and the extension and the faucet: coins to test with.