2026-03-26 10:50:40 +01:00
# Extensions
2026-04-21 14:50:54 +02:00
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 ](transport.md#transport-level-extensions ).
2026-03-26 10:50:40 +01:00
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:
```typescript
interface Hdwalletv1Session {
paths: PathXpub[];
extensions?: Record< string , unknown > ;
}
```
Each key in `extensions` is an extension name. Its **presence** indicates the wallet supports that
2026-03-28 22:39:34 +03:00
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.
2026-03-26 10:50:40 +01:00
Example:
```json
{
"paths": [
2026-03-28 22:39:34 +03:00
{ "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..." }
2026-03-26 10:50:40 +01:00
],
"extensions": {
2026-03-28 22:39:34 +03:00
"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'"
},
2026-03-26 10:50:40 +01:00
"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:
```json
{
"paths": [
2026-03-28 22:39:34 +03:00
{ "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..." }
2026-03-26 10:50:40 +01:00
],
"extensions": {
2026-03-28 22:39:34 +03:00
"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'"
}
2026-03-26 10:50:40 +01:00
}
}
```
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:
```typescript
// 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` :
```typescript
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` :
```typescript
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` :
```typescript
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:
```typescript
const adapter: WalletAdapter = {
// ... core implementation ...
getAdditionalPaths() {
return [
2026-03-28 22:39:34 +03:00
{ 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'") },
2026-03-26 10:50:40 +01:00
];
},
getExtensions() {
2026-03-28 22:39:34 +03:00
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'",
},
};
2026-03-26 10:50:40 +01:00
},
};
```
Add multislot extension: multiple sig/pubkey placeholders per input
Extend inputPaths tuples with an optional 4th `slot` element so a single
contract input can carry several sig/pubkey placeholders, each filled by
a different key. Gated behind a `multislot` capability the wallet
advertises in wallet_ready (session["hdwalletv1"].extensions.multislot);
dapps must not send slotted/repeated-index requests otherwise.
- core: widen inputPaths to [number, PathName, number, number?]; accept
3- or 4-tuples (non-negative integer slot) in isSignTransactionRequest;
add EXT_MULTISLOT constant.
- wallet: fix extractContractSighashBytes so a filled pubkey placeholder
is no longer mis-read as a signature (only signature-length pushes
carry a sighash flag); export validateSighashFlags / isP2PKH.
- docs: protocol.md (slot semantics, placeholder byte format, capability
negotiation, SIGHASH 0x41/0x61 reconciliation), extensions.md
(multislot), wallet.md, dapp.md.
- tests: multislot-signing.test.ts reference fill; sighash-validation
pubkey-placeholder regression + slot cases; validator slot accept/
reject; integration repeated-index-with-slots passthrough.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:21:24 +02:00
Whatever `getExtensions()` returns becomes `session["hdwalletv1"].extensions` verbatim in the
`wallet_ready` message — the `WalletConnectionManager` merges it in (it does not transform the keys).
That is the exact location the dapp reads (see "Discovering extensions" below). So returning
`{ multislot: {} }` is what the dapp sees as `session["hdwalletv1"].extensions.multislot` .
2026-03-26 10:50:40 +01:00
Wallets that don't implement these methods produce the same session as before (receive/change/defi
paths only, no extensions field).
Add multislot extension: multiple sig/pubkey placeholders per input
Extend inputPaths tuples with an optional 4th `slot` element so a single
contract input can carry several sig/pubkey placeholders, each filled by
a different key. Gated behind a `multislot` capability the wallet
advertises in wallet_ready (session["hdwalletv1"].extensions.multislot);
dapps must not send slotted/repeated-index requests otherwise.
- core: widen inputPaths to [number, PathName, number, number?]; accept
3- or 4-tuples (non-negative integer slot) in isSignTransactionRequest;
add EXT_MULTISLOT constant.
- wallet: fix extractContractSighashBytes so a filled pubkey placeholder
is no longer mis-read as a signature (only signature-length pushes
carry a sighash flag); export validateSighashFlags / isP2PKH.
- docs: protocol.md (slot semantics, placeholder byte format, capability
negotiation, SIGHASH 0x41/0x61 reconciliation), extensions.md
(multislot), wallet.md, dapp.md.
- tests: multislot-signing.test.ts reference fill; sighash-validation
pubkey-placeholder regression + slot cases; validator slot accept/
reject; integration repeated-index-with-slots passthrough.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:21:24 +02:00
> Version note: folding `getExtensions()` into the session was added in a later `@wizardconnect`
> release, together with the `EXT_MULTISLOT` constant and the optional `slot` (4th) element of the
> `inputPaths` tuple type. Packages from the `0.1.x` line ignore `getExtensions()` and type
> `inputPaths` as a 3-tuple without `EXT_MULTISLOT`, so an extension advertised that way silently
> fails to negotiate and a TypeScript consumer won't compile the 4-tuple. Confirm your
> `@wizardconnect/core` / `@wizardconnect/wallet` version actually ships these (or read the `slot`
> positionally and use the `"multislot"` string literal) before relying on them.
2026-03-26 10:50:40 +01:00
## Discovering extensions (dapp side)
Dapps check for extension support after receiving `wallet_ready` :
```typescript
manager.on("walletready", (msg) => {
const session = msg.session["hdwalletv1"] as Hdwalletv1Session;
2026-03-28 22:39:34 +03:00
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");
2026-03-26 10:50:40 +01:00
// enable stealth address features
} else {
2026-03-28 22:39:34 +03:00
// 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
2026-03-26 10:50:40 +01:00
}
});
```
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
2026-03-28 22:39:34 +03:00
| 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 ](https://bitcoincashresearch.org/t/ecdh-stealth-addresses-on-bitcoin-cash-implementation-code/1773/5 ) |
| `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 ](https://bitcoincashresearch.org/t/ecdh-stealth-addresses-on-bitcoin-cash-implementation-code/1773/5 ) |
| `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 |
Add multislot extension: multiple sig/pubkey placeholders per input
Extend inputPaths tuples with an optional 4th `slot` element so a single
contract input can carry several sig/pubkey placeholders, each filled by
a different key. Gated behind a `multislot` capability the wallet
advertises in wallet_ready (session["hdwalletv1"].extensions.multislot);
dapps must not send slotted/repeated-index requests otherwise.
- core: widen inputPaths to [number, PathName, number, number?]; accept
3- or 4-tuples (non-negative integer slot) in isSignTransactionRequest;
add EXT_MULTISLOT constant.
- wallet: fix extractContractSighashBytes so a filled pubkey placeholder
is no longer mis-read as a signature (only signature-length pushes
carry a sighash flag); export validateSighashFlags / isP2PKH.
- docs: protocol.md (slot semantics, placeholder byte format, capability
negotiation, SIGHASH 0x41/0x61 reconciliation), extensions.md
(multislot), wallet.md, dapp.md.
- tests: multislot-signing.test.ts reference fill; sighash-validation
pubkey-placeholder regression + slot cases; validator slot accept/
reject; integration repeated-index-with-slots passthrough.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:21:24 +02:00
| `multislot` | — | — | Wallet can fill **more than one** `sig` /`pubkey` placeholder per input, routed by the `slot` element of `inputPaths` . Needed for contracts that take multiple signatures from distinct keys in a single input. | Proposed |
2026-03-26 10:50:40 +01:00
See the discussions and specifications for each extension as they are formalized.
Add multislot extension: multiple sig/pubkey placeholders per input
Extend inputPaths tuples with an optional 4th `slot` element so a single
contract input can carry several sig/pubkey placeholders, each filled by
a different key. Gated behind a `multislot` capability the wallet
advertises in wallet_ready (session["hdwalletv1"].extensions.multislot);
dapps must not send slotted/repeated-index requests otherwise.
- core: widen inputPaths to [number, PathName, number, number?]; accept
3- or 4-tuples (non-negative integer slot) in isSignTransactionRequest;
add EXT_MULTISLOT constant.
- wallet: fix extractContractSighashBytes so a filled pubkey placeholder
is no longer mis-read as a signature (only signature-length pushes
carry a sighash flag); export validateSighashFlags / isP2PKH.
- docs: protocol.md (slot semantics, placeholder byte format, capability
negotiation, SIGHASH 0x41/0x61 reconciliation), extensions.md
(multislot), wallet.md, dapp.md.
- tests: multislot-signing.test.ts reference fill; sighash-validation
pubkey-placeholder regression + slot cases; validator slot accept/
reject; integration repeated-index-with-slots passthrough.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:21:24 +02:00
### multislot
By default an `inputPaths` entry is a 3-tuple `[inputIndex, pathName, addressIndex]` and the wallet
contributes exactly one signature (and at most one public key) per input. Some contracts need the
wallet to fill several `sig` /`pubkey` placeholders in a single input, each with a different key.
A wallet that supports this advertises `multislot` in its session extensions:
```json
{
"paths": [ /* ... */ ],
"extensions": { "multislot": {} }
}
```
The value is `{}` — no handshake data is needed; presence alone signals support.
When advertised, the dapp may add a fourth element, `slot` , to `inputPaths` entries and may list the
same `inputIndex` more than once (one entry per placeholder slot). The wallet fills the `slot` -th
sig placeholder and the `slot` -th pubkey placeholder of that input with the named key. See
[protocol.md § Multiple placeholders per input ](protocol.md#multiple-placeholders-per-input-the-slot-element )
for the placeholder byte format and exact slot semantics.
**A dapp MUST NOT** emit `slot > 0` (or repeat an `inputIndex` ) toward a wallet that has not
advertised `multislot` . A wallet that does not advertise it continues to receive only ordinary
3-tuple, single-signature requests, so it is unaffected. This negotiation is what prevents an
unaware wallet from silently filling only the first placeholder and returning an unspendable
transaction.