Extensions
This document covers hdwalletv1 protocol-level extensions — optional capabilities that extend
the application protocol (extra path names, custom message actions, per-wallet features). For
transport-level capabilities that apply below the application protocol regardless of which
protocol is in use (chunking, future: compression), see
transport.md § Transport-level extensions.
The hdwalletv1 protocol supports optional extensions that let wallets and dapps negotiate additional capabilities beyond the core sign-transaction flow. Extensions are backward-compatible: existing wallets and dapps that don't know about extensions continue to work unchanged.
How extensions work
Extensions use three mechanisms, all of which are optional and additive:
1. Session extensions field
Wallets advertise supported extensions in the wallet_ready handshake via an extensions field
on the Hdwalletv1Session object:
interface Hdwalletv1Session {
paths: PathXpub[];
extensions?: Record<string, unknown>;
}
Each key in extensions is an extension name. Its presence indicates the wallet supports that
extension. The value carries extension-specific handshake data (derivation paths to the hardened
gate where the wallet exports xpubs), or {} if no data is needed.
Example:
{
"paths": [
{ "name": "receive", "xpub": "xpub6..." },
{ "name": "change", "xpub": "xpub6..." },
{ "name": "defi", "xpub": "xpub6..." },
{ "name": "stealth_spend", "xpub": "xpub6..." },
{ "name": "stealth_scan", "xpub": "xpub6..." },
{ "name": "rpa_spend", "xpub": "xpub6..." },
{ "name": "rpa_scan", "xpub": "xpub6..." }
],
"extensions": {
"bch_stealth_bip352": {
"spend_path": "m/352'/145'/0'/0'",
"scan_path": "m/352'/145'/0'/1'"
},
"rpa_bip47": {
"spend_path": "m/47'/145'/0'/0'",
"scan_path": "m/47'/145'/0'/1'"
},
"decrypt": { "public_key": "02abc...", "scheme": "ecies" }
}
}
2. Additional path names
PathName is an open string type. Wallets may include paths beyond the well-known
receive/change/defi set. Extension-defined paths are carried in the standard paths array:
{
"paths": [
{ "name": "receive", "xpub": "xpub6..." },
{ "name": "change", "xpub": "xpub6..." },
{ "name": "stealth_spend", "xpub": "xpub6..." },
{ "name": "stealth_scan", "xpub": "xpub6..." },
{ "name": "rpa_spend", "xpub": "xpub6..." },
{ "name": "rpa_scan", "xpub": "xpub6..." }
],
"extensions": {
"bch_stealth_bip352": {
"spend_path": "m/352'/145'/0'/0'",
"scan_path": "m/352'/145'/0'/1'"
},
"rpa_bip47": {
"spend_path": "m/47'/145'/0'/0'",
"scan_path": "m/47'/145'/0'/1'"
}
}
}
Dapps should ignore path names they do not recognize. The standard pubkey derivation logic
(DappPubkeyStateManager) automatically skips unknown paths.
3. Custom message actions
Extensions may define new message action strings beyond the well-known set. Custom messages follow
the standard ProtocolMessage shape (action + time) and use the existing relay transport.
Convention for request/response operations:
// Request (dapp → wallet):
{ action: "<operation>_request", sequence: number, time: number, ...params }
// Response (wallet → dapp):
{ action: "<operation>_response", sequence: number, time: number, ...result }
The sequence field ties responses to requests, matching the pattern used by
sign_transaction_request/sign_transaction_response.
Wallet side: custom messages are emitted via the "message" event on
WalletConnectionManager:
manager.on("message", (connectionId, msg) => {
if (msg.action === "decrypt_request") {
// handle decrypt request
}
});
Dapp side: all messages (including custom ones) are emitted via the "messagereceived" event
on DappConnectionManager:
manager.on("messagereceived", (msg) => {
if (msg.action === "decrypt_response") {
// handle decrypt response
}
});
Implementing an extension (wallet side)
Wallets advertise extensions by implementing optional methods on WalletAdapter:
interface WalletAdapter {
// ... core methods ...
/** Additional paths to include in the session (e.g. stealth_scan). */
getAdditionalPaths?(): PathXpub[];
/** Extension data for the session handshake. */
getExtensions?(): Record<string, unknown>;
}
Example:
const adapter: WalletAdapter = {
// ... core implementation ...
getAdditionalPaths() {
return [
{ name: "stealth_spend", xpub: deriveXpub("m/352'/145'/0'/0'") },
{ name: "stealth_scan", xpub: deriveXpub("m/352'/145'/0'/1'") },
{ name: "rpa_spend", xpub: deriveXpub("m/47'/145'/0'/0'") },
{ name: "rpa_scan", xpub: deriveXpub("m/47'/145'/0'/1'") },
];
},
getExtensions() {
return {
"bch_stealth_bip352": {
"spend_path": "m/352'/145'/0'/0'",
"scan_path": "m/352'/145'/0'/1'",
},
"rpa_bip47": {
"spend_path": "m/47'/145'/0'/0'",
"scan_path": "m/47'/145'/0'/1'",
},
};
},
};
Wallets that don't implement these methods produce the same session as before (receive/change/defi paths only, no extensions field).
Discovering extensions (dapp side)
Dapps check for extension support after receiving wallet_ready:
manager.on("walletready", (msg) => {
const session = msg.session["hdwalletv1"] as Hdwalletv1Session;
if (session.extensions?.bch_stealth_bip352) {
const scanPath = session.paths.find(p => p.name === "stealth_scan");
const spendPath = session.paths.find(p => p.name === "stealth_spend");
// enable stealth address features
} else {
// show: "Stealth payments require a wallet that supports BCH Stealth (BIP352)"
}
if (session.extensions?.rpa_bip47) {
const rpaSpend = session.paths.find(p => p.name === "rpa_spend");
const rpaScan = session.paths.find(p => p.name === "rpa_scan");
// enable RPA features
}
});
Dapps should degrade gracefully when an extension is absent. If a feature requires an extension the wallet doesn't support, inform the user rather than failing silently.
Known extensions
| Extension name | Path names | Hardened gate paths | Purpose | Status |
|---|---|---|---|---|
bch_stealth_bip352 |
stealth_spend, stealth_scan |
m/352'/145'/0'/0', m/352'/145'/0'/1' |
BCH stealth addresses (BIP352 structure). Wallet exports xpubs at hardened gates; dapp derives /0 child locally. |
Standard — BCR post |
rpa_bip47 |
rpa_spend, rpa_scan |
m/47'/145'/0'/0', m/47'/145'/0'/1' |
BIP47 reusable payment addresses. Wallet exports xpubs at hardened gates; dapp derives /0 child locally. |
Standard — BCR post |
decrypt |
— | — | Dapp-side encrypted storage. Wallet provides a public key; dapp encrypts data for storage and sends decrypt_request messages when the data is needed. |
Proposed |
See the discussions and specifications for each extension as they are formalized.