Commit graph

70 commits

Author SHA1 Message Date
Håvard Kittelsen
b40da7230a feat: add the sign_message hdwalletv1 extension
Wires the Bitcoin Signed Message primitives into the protocol: a dapp can ask a
wallet to prove control of a key, and gets back a signature any third party can
check from the message and an address alone.

WIRE FORMAT

SignMessageRequest extends WcSignMessageRequest from @bch-wc2/interfaces — the
interface wallets already implement for WalletConnect — so the request object can
be handed straight to an existing WC2 signMessage handler. hdwalletv1 adds only
the optional key selection, mirroring how SignTransactionRequest wraps
WcSignTransactionRequest and adds inputPaths. That also brings `userPrompt` along,
which is dapp-supplied and unsigned; docs/wallet.md says to render it as
subordinate to the message, because presented as equal it lets a dapp caption
hostile text reassuringly.

Two key-selection modes, advertised separately from `schemes` because they are
independent capabilities — a wallet may sign with a dapp-named path yet have no
notion of a stable identity key, and a dapp that checked only for the extension
would find that out after the user clicked a login button:

  dapp_path      dapp sends path + addressIndex; needs that path's xpub
  wallet_choice  dapp sends neither; wallet picks and returns the address

wallet_choice exists because requiring an xpub to prove control of one key means
sharing the user's whole address history. It is the privacy-preserving option for
identity, and the one the WC2 interface already implies. A wallet advertising it
must choose deterministically or a returning user is unrecognisable.

The response is a discriminated union on `error`, so a caller cannot read
`.address` off a rejection and treat an empty string as an identity. publicKey and
address are required on success: under wallet_choice they are the dapp's only way
to learn which key answered.

WALLET SIDE

signMessage is optional; implementing it is what advertises the extension, so the
handshake cannot claim support an adapter does not have. An adapter that declares
the key itself wins — the automatic advertisement never overwrites it.

SignMessageResult carries only the signature. The public key and address are
recoverable from it and the manager derives them that way, so the three values
cannot disagree and an adapter cannot claim a proof about an address it did not
prove. The manager then compares the recovered key against the adapter's own key
for the path. Recovery alone cannot catch a signature over the wrong text — it
succeeds and yields some other key — so that comparison is what turns a wallet-side
derivation or encoding bug into an error at the call site rather than an opaque
rejection across the relay.

Requests are answered rather than dropped: an unsupported scheme, an unsupported
mode, a malformed request or a wallet with no signMessage all produce an error
response, checked before the user is prompted so nobody approves a signature we
cannot produce.

Dedup shares the sequence set with transaction signing. That is correct rather
than convenient: every sequence comes from one per-connection counter
(RelayClient.nextSequence), so a sequence identifies a request regardless of kind
— which is also what lets one sign_cancel cancel either.

DAPP SIDE

signMessage() resolves only after this library has verified the result: the
signature recovers over the message that was sent, publicKey is the key that
signed, address is that key's address, and — when the dapp named a path it can
derive — the signer is exactly the key it asked for. Anything inconsistent
rejects. Without that last check a wallet could answer with a signature from any
key and a naive dapp would accept it as the identity it asked about.

keyBinding reports whether that comparison happened, because "the wallet chose a
key" and "this is the key I asked for" are different claims and only one is an
identity the dapp selected. A derivable path with no xpub available is an error,
not an unchecked result.

No default timeout: cancellation is explicit via AbortSignal, matching
signTransaction. Picking a deadline for a user approving on a phone is worse than
letting the dapp decide.

EXTENSION SHAPE

Actions live in RelayMsgAction and are handled by the managers, rather than riding
the generic message events described in docs/extensions.md § 3. That is a new
pattern, not an existing convention — the only prior enum-plus-advertisement
capability is `chunk`, which is transport-level and outside the hdwalletv1
extension system entirely. It is documented as new under § First-party
extensions: third-party extensions define their own actions and are handled by the
host app; capabilities this library ships get manager support, because otherwise
every consumer hand-rolls the plumbing for a feature we already implement.

TESTS

24 wallet, 26 dapp, and 8 over a live relay. The integration test matters most:
NIP-17 gift wrapping, JSON encoding, relay storage and replay all sit between the
two sides, and it asserts the message arrives byte-identical, that a multi-byte
message is not re-encoded in transit, that a replayed request prompts once, and
that the resulting signature verifies from the address alone. makeTestAdapter
gained a real signMessage — it already holds HD keys, so there was nothing to
fake.

test-cli gains `--sign-message [dapp_path|wallet_choice]` and a wallet-side
approval path, so the flow can be driven by hand against a real wallet. It signs
a plain test message, not a login: a login needs a single-use nonce, a domain and
an expiry, and signing something that merely looks like one would be a bad
pattern to copy.

Docs: protocol.md, extensions.md, wallet.md, dapp.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 14:32:02 +02:00
Håvard Kittelsen
763754b30e feat(core): Bitcoin Signed Message primitives, checked against Electron Cash
Dapps have been asking for message signing to prove key control — identity
verification, SIWX-style login, and publishing a signed statement on chain. This
adds the construction and the verification; the protocol wiring follows
separately.

The known workaround is a dummy transaction: one input with a null outpoint, its
prevout script set to the P2PKH of the key being proven, one OP_RETURN output
carrying a server nonce, signed but never broadcast. It covers login, but not the
rest. A signature made that way is bound to a transaction, so verifying it means
rebuilding that exact transaction and knowing how it was serialised — it cannot
be published in an OP_RETURN and checked later by a third party holding only the
message, the signature and an address. It also asks a wallet to sign a real
transaction preimage, which is one bug away from signing a genuine spend.

So this produces the portable form: the standard "Bitcoin Signed Message"
construction. BCH wallets kept Bitcoin's magic string verbatim, so a signature
made here verifies in Electron Cash, Electrum and `bitcoin-cli verifymessage`.
The magic prefix also guarantees the digest can never coincide with a transaction
sighash, which makes signing a message categorically safer to approve than
signing a dummy transaction.

Address-level API, because that is what verification actually looks like
elsewhere: Electron Cash exposes only `verify_message(address, sig, message)`,
and a third party pulling a proof off an explorer has an address, not a public
key. verifyMessageSignatureForAddress accepts CashAddr with or without a prefix
and legacy base58, and rejects P2SH — no message signature can prove control of a
script hash.

recoverMessageSigner returns { publicKey, compressed } rather than a bare key.
The header byte declares which serialisation was used, and a key's compressed and
uncompressed forms hash to DIFFERENT addresses. Dropping that bit is how a
signature proving control of one address gets accepted as proof of another;
message-signing.test.ts pins the case in both directions.

signBitcoinMessage takes the private key as an argument and never retains it. It
exists so a wallet calls one function instead of reassembling the magic string,
both compactSize prefixes and the header byte — the parts third-party verifiers
check, and the parts covered by the tests here.

Testing: the byte layout is not asserted against our own reimplementation of the
spec, because that catches a coding mistake but not a misreading of it.

  message-signing.vectors.json holds 32 vectors generated by a real Electron Cash
  4.4.5 install (contrib/generate-message-signing-vectors.py) — two keys, both
  compression forms, eight messages including empty, multi-byte UTF-8, multi-line
  and the 252/253-byte compactSize boundary. Every preimage hash must match byte
  for byte, and every signature must verify. Signature bytes are NOT portable
  across implementations — Electron Cash and libauth derive the ECDSA nonce
  differently — so the reproducible quantity is the hash.

  message-signing.compat.test.ts drives `electron-cash verifymessage` directly,
  closing the loop that vectors cannot: that our OUTPUT is accepted. Not part of
  `npm test` (each assertion spawns a full Electron Cash process); run
  `npm run test:compat -w @wizardconnect/core`. Skips when Electron Cash is
  absent, so CI is unaffected.

77 tests, 426 in core.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 14:28:41 +02:00
jakobsn
167ec21474 Merge branch 'revert-e71f76c1' into 'master'
Revert "Merge branch 'randomTradeSummary' into 'master'"

See merge request riftenlabs/lib/wizardconnect!29
2026-05-11 08:48:04 +00:00
jakobsn
d580bb8927 Revert "Merge branch 'randomTradeSummary' into 'master'"
This reverts merge request !27
2026-05-11 08:46:49 +00:00
Dagur Valberg Johannsson
dc01931ad6 Merge branch 'react-dev' into 'master'
Fix issue with restoring session in React dev mode

See merge request riftenlabs/lib/wizardconnect!28
2026-05-09 18:34:46 +00:00
Dagur Valberg Johannsson
c2b13102b9
Fix issue with restoring session in React dev mode 2026-05-09 20:25:45 +02:00
jakobsn
e71f76c1d0 Merge branch 'randomTradeSummary' into 'master'
TxSummary

See merge request riftenlabs/lib/wizardconnect!27
2026-04-29 11:23:23 +00:00
jakobsn
6b6f0ec1d0 TxSummary 2026-04-29 11:23:23 +00:00
jakobsn
201e234bef Merge branch 'cauldron-191-cauldron-and-wizard-connect-thinks-i-am-connected-but-im-not' into 'master'
Active reconnect.

See merge request riftenlabs/lib/wizardconnect!25
2026-04-29 09:06:54 +00:00
jakobsn
443acccb80 Active reconnect. 2026-04-29 09:06:53 +00:00
jakobsn
896871c0d8 Merge branch 'chunk' into 'master'
Add chunk transport extension for oversized messages

See merge request riftenlabs/lib/wizardconnect!24
2026-04-27 13:30:40 +00:00
Dagur Valberg Johannsson
b96cb8e151
Add chunk transport extension for oversized messages
Splits ProtocolMessages exceeding NIP-44's 65,535-byte plaintext ceiling
across multiple gift-wrapped events. Symmetric (both directions),
fire-and-forget, backward-compatible via a new transport-level
\`extensions\` field on \`dapp_ready\` and \`wallet_ready\`. Resolves the
\"Failed to swap: invalid plaintext size\" error on aggregated swap
requests and enables signed-tx responses up to the 1 MB BCH consensus
limit (~2 MB hex).
2026-04-21 14:58:12 +02:00
Dagur Valberg Johannsson
472b9b1ec5 Merge branch 'all-relay' into 'master'
Ensure messages are sent to all relays

See merge request riftenlabs/lib/wizardconnect!23
2026-04-16 11:39:22 +00:00
Dagur Valberg Johannsson
9028384177
Ensure messages are sent to all relays
To be compatible to wallets that only listen on 'relay.cauldron.quest',
we need to make sure we send our messages to all connected relay
servers.
2026-04-16 13:38:28 +02:00
Dagur Valberg Johannsson
053f515b8f Merge branch 'ci-fix' into 'master'
ci: Fix version comparison bug

See merge request riftenlabs/lib/wizardconnect!22
2026-04-14 07:16:18 +00:00
Dagur Valberg Johannsson
994853bf8e
ci: Fix version comparison bug
Auto-publishing had a version comparison bug, causing it to attempt to
republish old versions instead using next avaiable version number.
2026-04-14 09:15:19 +02:00
Dagur Valberg Johannsson
fbac14cd08 Merge branch 'redundant-relay' into 'master'
Add additional default relay

See merge request riftenlabs/lib/wizardconnect!21
2026-04-14 07:03:50 +00:00
Dagur Valberg Johannsson
c8e0a2d7cc
Tests for 22601be4 (tcp zombie) 2026-04-14 08:42:34 +02:00
Dagur Valberg Johannsson
ef56fb198c
Add additional default relay
For improved reliability, this adds an additional relay as a redundancy.
2026-04-14 08:42:34 +02:00
Dagur Valberg Johannsson
f9da868513 Merge branch 'tcp-zombie' into 'master'
Detect stale tcp connections to relay

See merge request riftenlabs/lib/wizardconnect!20
2026-04-08 09:30:09 +00:00
Dagur Valberg Johannsson
a6e4a36db5
npm audit fix 2026-04-08 11:05:16 +02:00
Dagur Valberg Johannsson
22601be42d
Detect stale tcp connections to relay
Enable nostr-tools' internal stale tcp connection detection
2026-04-08 09:47:50 +02:00
Dagur Valberg Johannsson
4bcbef2aae Merge branch 'nicer-react' into 'master'
Add session persistence and refactor dapp manager

See merge request riftenlabs/lib/wizardconnect!19
2026-04-03 15:15:26 +00:00
Dagur Valberg Johannsson
8cd22c34ba
Add session persistence and refactor dapp manager
Keep session persistance by default, such that when reloading a dapp,
the wizardconnect session is kept alive.

Improve signature request interface. Simplify the react API.
2026-04-03 17:11:18 +02:00
Dagur Valberg Johannsson
3af76d5653 Merge branch 'sleepingWizard' into 'master'
Dedup connection: conn.dappDiscovered = false;

See merge request riftenlabs/lib/wizardconnect!17
2026-04-03 12:35:50 +00:00
jakobsn
e852e70a8e Dedup connection: conn.dappDiscovered = false; 2026-04-03 12:35:50 +00:00
Dagur Valberg Johannsson
077236969a Merge branch 'resend-on-ready' into 'master'
Re-send signature request on ready

See merge request riftenlabs/lib/wizardconnect!18
2026-04-03 11:38:12 +00:00
Dagur Valberg Johannsson
3324cac4fb
npm audit fix 2026-04-03 13:22:40 +02:00
Dagur Valberg Johannsson
08ea3da6aa
Re-send signature request on ready
User may need some time to open their wallet to approve signature
request and miss it.

If we have a active request, re-send it if we see a
wallet_ready signal, suggesting the wallet was just opened.
2026-04-03 13:22:40 +02:00
Dagur Valberg Johannsson
72da1758b3 Merge branch 'bcr-stealth-naming' into 'master'
docs: standardize extension names to RPA and BCH Stealth Addresses

See merge request riftenlabs/lib/wizardconnect!16
2026-04-01 09:47:25 +00:00
CyberAshven
d1369f5867 docs: standardize extension names to BCR-agreed standard
Replace placeholder 'bip352' / 'bip47_rpa' with the finalized
extension names from the BCR post and BCH Stealth Protocol spec:

- bip352      → bch_stealth_bip352
- bip47_rpa   → rpa_bip47

Add spend_path / scan_path fields to each extension handshake
object, pointing to the hardened gate paths where the wallet
exports xpubs. Dapps derive the non-hardened /0 child locally.

Add rpa_spend / rpa_scan path names alongside stealth_spend /
stealth_scan in all examples. Update known extensions table with
full path information and BCR reference links.

Spec: https://bitcoincashresearch.org/t/ecdh-stealth-addresses-on-bitcoin-cash-implementation-code/1773/5

I have read the CLA Document and I hereby sign the CLA
2026-03-28 22:39:34 +03:00
Dagur Valberg Johannsson
8b6a2e275f Merge branch 'extensions' into 'master'
Add 'extensions' to hdwalletv1 protocol

See merge request riftenlabs/lib/wizardconnect!15
2026-03-26 12:43:05 +00:00
Dagur Valberg Johannsson
4d1ba8e207
Add 'extensions' to hdwalletv1 protocol
This backward compatible change allows wallets/dapps to add additional
features outside the basic transaction signature support to the
hdwalletv1 protocol.
2026-03-26 10:50:40 +01:00
Dagur Valberg Johannsson
40d5f218fe
npm audit fix 2026-03-26 08:56:48 +01:00
Dagur Valberg Johannsson
fcfcd64d14
Replace nostr-dev-kit -> nostr-tools
The package nostr-dev-kit was using a dependency (tseep) which
downstream would flag as not CSP-safe.

Additionally nostr-tools footprint is much smaller.
2026-03-26 08:46:26 +01:00
Dagur Valberg Johannsson
95bbf96065 Merge branch 'qr-dialog' into 'master'
QR dialog: Give "Copied!" feedback

See merge request riftenlabs/lib/wizardconnect!14
2026-03-23 08:27:38 +00:00
Dagur Valberg Johannsson
676512ea8d
QR dialog: Give "Copied!" feedback
Clicking anywhere on the URI row copies to clipboard (not just the icon).
Shows "Copied!" text for 2 seconds replacing the copy icon.
2026-03-23 09:26:18 +01:00
Dagur Valberg Johannsson
9593332525 Merge branch 'ser-helpers' into 'master'
Add serialization helpers to @wizardconnect/core

See merge request riftenlabs/lib/wizardconnect!13
2026-03-23 08:16:42 +00:00
Dagur Valberg Johannsson
889dd566d1
Add serialization helpers to @wizardconnect/core
Canonical encoding for BigInt (<bigint: Xn>) and Uint8Array (hex or
<Uint8Array: 0x...>) used in the relay protocol. Provides both
serialization (sourceOutputToRelay, transactionToHex) and deserialization
(parseExtendedJson, toUint8Array, toBigInt) so dapps and wallets
don't have to implement this independently.
2026-03-23 08:59:02 +01:00
Dagur Valberg Johannsson
a22d98a6b2 Merge branch 'doc-xpub' into 'master'
doc: An article on xpub sharing

See merge request riftenlabs/lib/wizardconnect!8
2026-03-23 07:45:38 +00:00
Dagur Valberg Johannsson
930f09a167 Merge branch 'session-path' into 'master'
Add proper auto-reconnect with @wizardconnect/react

See merge request riftenlabs/lib/wizardconnect!12
2026-03-23 07:44:43 +00:00
Dagur Valberg Johannsson
68581a4929
up to date, audited 269 packages in 1s
106 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
2026-03-23 08:43:44 +01:00
Dagur Valberg Johannsson
acab94dc68
ci: Build before running tests 2026-03-23 08:43:00 +01:00
Dagur Valberg Johannsson
cb4f1ee598
bug: Pass pubkey to initiateRelay on reconnect
Pass the pubkey if we already know it.
2026-03-23 08:34:51 +01:00
Dagur Valberg Johannsson
8467856975
Persist xpub paths in session storage for offline pubkey derivation
The useWizardConnect hook now saves PathXpub[] from wallet_ready into
the localStorage session. On auto-reconnect, restoreSessionPaths() is
called so getPubkey() works immediately without waiting for the wallet.
Corrupt cached paths are cleared with a warning.
2026-03-23 08:32:17 +01:00
Dagur Valberg Johannsson
add06b6476
Add getSessionPaths() and restoreSessionPaths() to DappConnectionManager
Allows dapps to cache the raw PathXpub[] from wallet_ready and restore
them on subsequent page loads. restoreSessionPaths decodes the xpub strings
and populates pubkeyState so getPubkey() works without waiting for the
wallet to reconnect. Throws on invalid xpub data.
2026-03-23 08:26:12 +01:00
Dagur Valberg Johannsson
33c45596ef Merge branch 'pkg-react' into 'master'
Add 'react' package with QR code and modal dialog

See merge request riftenlabs/lib/wizardconnect!11
2026-03-18 15:41:30 +00:00
Dagur Valberg Johannsson
45fd9f4cb5
Add 'react' package with QR code and modal dialog
Makes it easier for dapp developers to integrate WizardConnect and have
consistent user interface for it across dapps.
2026-03-18 16:34:41 +01:00
Dagur Valberg Johannsson
e2476626c2 Merge branch 'doc-sighash' into 'master'
Document SIGHASH_ALL requirement

See merge request riftenlabs/lib/wizardconnect!10
2026-03-18 09:36:10 +00:00
Dagur Valberg Johannsson
55ff82bbec
Document SIGHASH_ALL requirement 2026-03-18 10:31:06 +01:00