feat(connect): unblock WizardConnect end-to-end + honest sign preview
Two prior sessions and this one collectively unblocked WizardConnect for
chipnet beta. Landed here:
Licence trap resolved:
- @wizardconnect/* is LGPL-3.0-or-later (verified against gitlab
repo LICENSE.txt, README, Riften Labs announcement). "Proprietary"
in `npm view` is a missing `license` field artefact, not a term.
- WizardConnect_reference.md §0/§3.3, README wallet table, wz-adapter.ts
and wz-handler.ts headers corrected. Mainnet is no longer blocked;
§6 of the integration guide lists the remaining LGPL §4 obligations.
Wrapper unwrap bug fixed (responder.ts:279-310):
- SignTransactionRequest.transaction is a WcSignTransactionRequest
wrapper, not the tx. Old code passed the wrapper through as `tx`, so
every WZ sign request threw "malformed request — missing tx.inputs"
at signer.ts:136. Now unwrapped + hex-string variant decoded +
broadcast/userPrompt forwarded. Regression tests added.
Sign-preview honesty (AppsScreen.tsx + preflight.ts):
- Replaced hardcoded "✓ no token UTXOs spent" / "bchtest:qp2…7lm" fake
pre-flight block with real request-derived outputs, fee, and token
flags. Renders dApp's `userPrompt` verbatim per protocol.
- Hides the block when the tx can't be decoded rather than lying about
what was checked.
Dropped SDK events surfaced (wz-handler.ts + responder.ts + useConnect.ts):
- signCancelled + remoteDisconnect now flow through WzHandlerEventMap.
- Responder evicts pending WZ entries on cancel; new
ResponderEvents.onSignRequestCancel clears the approval screen if the
user was about to approve a withdrawn request.
WIF-restored wallets get an honest message (useConnect.ts):
- KeyStoreError('not-supported') from a WIF-restored wallet now logs a
quiet console info line instead of a red "WizardConnect not supported
by this build" banner. WC/CC remain available. Other failures still
surface normally.
Definition of Done (see WIZARDCONNECT-INTEGRATION-GUIDE.md §11) items
§1–§4 landed here. §5–§9 handed off to a fresh session via
NEXT-SESSION-PROMPT.md — includes Reown decoupling, LGPL §4(d)(1)
chunk split, session re-hydrate, on-device test against Silent Mode
registrar (with Cashonize v0.9.0 as comparison implementation), and
ADR-0044 for the Nostr transport.
Tests: 162/162 passing. tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d8f999effc
commit
6987591065
14 changed files with 1304 additions and 54 deletions
210
Parameters/WalletConnect/NEXT-SESSION-PROMPT.md
Normal file
210
Parameters/WalletConnect/NEXT-SESSION-PROMPT.md
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
# Prompt for a fresh session — WizardConnect completion (§5–§9)
|
||||
|
||||
_Written 2026-07-27. Paste this whole file to a new session. Everything the
|
||||
session needs is either in this prompt or referenced by path — do not read
|
||||
back the compacted-summary transcript from the earlier session._
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
WizardConnect (Nostr NIP-17 transport, `hdwalletv1` protocol, LGPL-3.0-or-later)
|
||||
is **already implemented + tested** in the Deviant wallet (`shells/app/`). A
|
||||
prior session traced every anchor against the working tree and wrote a
|
||||
definitive integration guide at
|
||||
`Parameters/WalletConnect/WIZARDCONNECT-INTEGRATION-GUIDE.md` — **read it
|
||||
first, verbatim.** The Definition-of-Done in §11 of that guide is the target.
|
||||
|
||||
The previous session (this one) landed §1–§4 of that DoD:
|
||||
|
||||
1. Corrected the licence-Proprietary trap in `WizardConnect_reference.md`
|
||||
§0/§3.3 (`Proprietary` → `LGPL-3.0-or-later`, with verified citations).
|
||||
2. Replaced the hardcoded fake pre-flight block in `AppsScreen.tsx` with
|
||||
real request-derived data (`shells/app/src/connect/preflight.ts` + a
|
||||
rewritten `SigningPreview`). Renders `userPrompt` verbatim per protocol.
|
||||
3. Surfaced `signCancelled` + `remoteDisconnect` from
|
||||
`WalletConnectionManager` through `wz-handler.ts`; wired the responder
|
||||
to evict pending WZ sign entries on cancel and forward a readable error
|
||||
on remote disconnect. Added `onSignRequestCancel` to `ResponderEvents`
|
||||
and threaded it through `useConnect.ts` so the approval screen clears
|
||||
itself when the dApp withdraws the request.
|
||||
4. Made the WIF-restored WZ-not-supported path honest: instead of a red
|
||||
error banner, `useConnect.ts` catches `KeyStoreError('not-supported')`
|
||||
specifically and logs a console `[connect]` info line explaining WC/CC
|
||||
remain available. Any other failure still surfaces normally.
|
||||
|
||||
All 162 shell tests pass. `tsc --noEmit` is clean. Nothing is committed —
|
||||
review the diff alongside the ~82 files of in-flight work already on
|
||||
`design/dex-p2p-architecture`.
|
||||
|
||||
---
|
||||
|
||||
## Your remit — DoD items §5 through §9 of the guide
|
||||
|
||||
Do these in order. Each is scoped to be one commit.
|
||||
|
||||
### §5 — Decouple WZ from the Reown project ID
|
||||
|
||||
`shells/app/src/connect/useConnect.ts:63-76` (`getWalletKit()`) throws when
|
||||
`VITE_WC_PROJECT_ID` is unset. `useConnect.ts:160` awaits `getWalletKit()`
|
||||
**before anything else**, so a missing Reown ID kills the Nostr path too —
|
||||
even though WizardConnect needs no Reown account at all. Refactor so:
|
||||
|
||||
- `wzHandler` initialises independently of `walletKit`.
|
||||
- If `PROJECT_ID` is missing, WC/CC are dark but WZ still pairs.
|
||||
- `ConnectState.missingProjectId` stays truthful; add a parallel
|
||||
`ConnectState.wzReady` (or fold into `ready`) so the UI can say
|
||||
"WalletConnect unavailable — Reown project ID missing; WizardConnect
|
||||
still works" instead of a monolithic "not ready".
|
||||
- Update `test/connect-responder-compat.test.ts` if the fake `WalletKit`
|
||||
path changes. Add a new test for the WZ-only path.
|
||||
|
||||
### §6 — LGPL §4(d)(1) build compliance + attribution
|
||||
|
||||
`shells/app/vite.config.ts:91` has `manualChunks` rules for react, mainnet-js,
|
||||
libauth, cashscript, qrcode — but no `@wizardconnect`. Fold in the
|
||||
three-line rule from the guide §6 so `@wizardconnect/*` becomes its own
|
||||
`vendor-wizardconnect` chunk (LGPL §4(d)(1) "suitable shared library
|
||||
mechanism"). Then:
|
||||
|
||||
- Ship LGPL-3.0 and GPL-3.0 licence texts as assets (they are referenced
|
||||
from LGPL-3.0 by transclusion — both are required). Put them under
|
||||
`shells/app/public/licenses/`.
|
||||
- Add an About / Licences screen entry (Settings tab is the current home
|
||||
for build info; add there if there is nowhere better). Line: "This app
|
||||
uses WizardConnect by Riften Labs (LGPL-3.0-or-later) — see /licenses/."
|
||||
- Cross-reference the guide §6 in the commit message. **Do these before
|
||||
any mainnet build.**
|
||||
|
||||
### §7.3 — Re-hydrate sessions on reload (affects WC + CC + WZ)
|
||||
|
||||
`shells/app/src/connect/responder.ts:126` keeps sessions in an in-memory
|
||||
`Map`. Nothing calls `walletKit.getActiveSessions()` on init anywhere in
|
||||
`shells/app/src/`, so WC sessions WalletKit restores from its own
|
||||
IndexedDB never reappear in the responder's map after a reload. Same shape
|
||||
of problem exists for WZ (the SDK persists completed sign sequences but
|
||||
not connections; the responder must remember and re-pair).
|
||||
|
||||
Design:
|
||||
|
||||
- On `createResponder` construction, call `walletKit.getActiveSessions()`
|
||||
and rebuild the `sessions` map + emit synthetic `onSessionProposal`-shaped
|
||||
entries OR expose a `listSessions()` that returns the reconciled list.
|
||||
- For WZ: connection state is not persisted by `@wizardconnect/wallet` —
|
||||
either persist the URI list ourselves (localStorage) and reconnect on
|
||||
init, OR document that WZ sessions are per-page-load (acceptable for v1,
|
||||
ADR-worthy).
|
||||
- Update `test/connect-responder-compat.test.ts` with a re-hydrate case.
|
||||
|
||||
This is the cross-cutting one — affects all three transports.
|
||||
|
||||
### §8 — On-device verification against Silent Mode registrar
|
||||
|
||||
**This step requires a phone + the user's participation** — write it up as
|
||||
the last thing, ask the user to run it, and capture the outcome in a
|
||||
snapshot. **Do not skip.** Nothing above has ever been exercised against a
|
||||
real dApp.
|
||||
|
||||
Comparison implementation to reference during debugging: **Cashonize v0.9.0+**
|
||||
(released 2026-07-25) is the second wallet to ship WizardConnect on
|
||||
chipnet. If Deviant's approval flow misbehaves, pairing the same Silent
|
||||
Mode registration URI to Cashonize tells you which side is wrong: two
|
||||
wallets that disagree with the same dApp on the same request is a
|
||||
protocol-boundary bug; one wallet disagreeing is a wallet-side bug. **Do
|
||||
not use Paytaca as the comparison** — no chipnet support.
|
||||
|
||||
Full setup + expected behaviour + what to report is in the guide §8. Test
|
||||
plan pinned there so it is not re-derived. Notable: **two approvals** on
|
||||
first sign is a one-off self-send required by CashToken genesis (needs an
|
||||
outpoint at index 0), not a bug — page states it up-front.
|
||||
|
||||
### §9 — ADR + wiki update
|
||||
|
||||
There is NO ADR for the Nostr transport in `wiki/decisions.md`. ADR-0043
|
||||
predates it and still describes `classifyUri` as returning `'wc' | 'cc' | null`.
|
||||
`wiki/wallet/connector.md:120` still lists "WizardConnect probe — pair the
|
||||
shell with Paytaca" as an open question — that is wrong on both counts
|
||||
(Paytaca has no chipnet support; the probe is done).
|
||||
|
||||
Write ADR-0044 (or the next free number — check `wiki/decisions.md`):
|
||||
|
||||
- Nostr transport as a parallel channel to WC2, deliberately not a WC2
|
||||
extension (transport is genuinely different).
|
||||
- Session lifecycle differences: no `session_proposal`, sessions are
|
||||
manufactured from the first sign request at
|
||||
`shells/app/src/connect/responder.ts:243-263`.
|
||||
- SIGHASH_UTXOS (0x61) as a non-negotiable protocol requirement.
|
||||
- LGPL §4 combined-work reasoning (why copyleft does not propagate).
|
||||
- Chipnet-first rollout, mainnet gated on §6 chunk-split + attribution.
|
||||
|
||||
Update ADR-0043 to reference ADR-0044 and correct the `classifyUri` claim.
|
||||
Update `wiki/wallet/connector.md` — replace the "probe with Paytaca"
|
||||
open question with "validated on-device against Silent Mode registrar,
|
||||
snapshot 0NNN" (whatever number §8 lands at). And update
|
||||
`Parameters/WalletConnect/README.md` if any status rows drift.
|
||||
|
||||
---
|
||||
|
||||
## Traps to avoid (from the guide §9)
|
||||
|
||||
- **`wiz:` routing lives in TWO files.** `classifier.ts:71` AND
|
||||
`qr/qr-uri.ts:55`. Deliberate — QR routing must not depend on the
|
||||
connector module. Change both when adding new schemes.
|
||||
- **`sighashType` applies to every input**, not per input. Don't try to
|
||||
make it per-input.
|
||||
- **`addressIndex` is `number` in `inputPaths` but `bigint` in `getPubkey`.**
|
||||
Convert at the boundary.
|
||||
- **`@wizardconnect/core` re-exports libauth.** Keep
|
||||
`@bitauth/libauth@3.1.0-next.8` version-locked with the SDK. Two copies
|
||||
of libauth in one bundle causes a nasty WASM init race.
|
||||
- **`isOwnedInput` is exported but not called by `coSignTransaction`.**
|
||||
Signer inlines an equivalent check with asymmetric behaviour: explicit
|
||||
`inputPaths` mismatch = hard throw; unmapped foreign input = silent skip.
|
||||
- **`getPublicKey` throws by design** (`wz-adapter.ts:139`). Don't "fix" it
|
||||
— the manager never calls it at 0.2.2. If you upgrade the SDK, verify.
|
||||
- **Two chat sessions may share this working tree.** Check
|
||||
`shells/app/WALLET-COORDINATION.md` before editing wallet files.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
After every change:
|
||||
|
||||
```bash
|
||||
cd shells/app
|
||||
npx tsc --noEmit
|
||||
npm test
|
||||
```
|
||||
|
||||
Target: still `162 passed`, plus any new tests you add. Any regression
|
||||
must be diagnosed, not skipped.
|
||||
|
||||
For §8 (on-device), verification is the user's report + a `snapshots/0NNN-*.md`
|
||||
entry with `domain: wallet` frontmatter.
|
||||
|
||||
---
|
||||
|
||||
## Files loaded in this session's context (for reference)
|
||||
|
||||
- `Parameters/WalletConnect/WIZARDCONNECT-INTEGRATION-GUIDE.md` (the DoD source)
|
||||
- `Parameters/WalletConnect/WizardConnect_reference.md` (licence fix landed;
|
||||
§5 dApp-side is the reference the guide points at)
|
||||
- `Parameters/WalletConnect/README.md` (routing rule + wallet table)
|
||||
- `shells/app/src/connect/{wz-adapter,wz-handler,responder,useConnect,preflight,index,signer,classifier}.ts`
|
||||
- `shells/app/src/screens/AppsScreen.tsx` (SigningPreview)
|
||||
- `shells/app/src/connect/preflight.ts` (new — request-derived pre-flight)
|
||||
- `src/wallet/keystore.ts` (HD interface + KeyStoreError codes)
|
||||
|
||||
---
|
||||
|
||||
## What NOT to do
|
||||
|
||||
- Do not re-litigate the "Proprietary vs LGPL" question — it is settled
|
||||
(see the guide §2 + §6 with three-way citation).
|
||||
- Do not add ambient `mkdir` / dir-exists checks; the memory dir is
|
||||
managed by the harness (per your user-global instructions).
|
||||
- Do not attempt on-device pairing yourself — the phone is the user's.
|
||||
Prepare the code, then hand off to the user with clear instructions.
|
||||
- Do not touch `dapp.entry.ts` unless §7.3 forces it — that file is used
|
||||
by the Checkers session and any bump requires re-vendoring on their side.
|
||||
|
|
@ -8,7 +8,8 @@ reasons (the first one we integrated). Each has its own deep-dive file:
|
|||
|---|---|---|
|
||||
| [`WalletConnect_integration_reference.md`](./WalletConnect_integration_reference.md) | ✅ **Implemented + on-device validated** (snapshot 0057) | wc2-bch-bcr over WC2 relay — pass-through co-sign, the broadest BCH wallet coverage |
|
||||
| [`CashConnect_reference.md`](./CashConnect_reference.md) | ✅ **Implemented (handshake, multi-protocol dispatch)** (snapshot 0152). Covenant co-sign blocked by V0 — routes through WC. **v-next `bch_executeAction` model is designed to fix this** (see doc §10). | **CashRPC** = the underlying protocol (transport-agnostic); **CashConnect** = CashRPC + WalletConnect v2. BCH community, @jimtendo proposal (Dec 2023 BCR forum), merged into Cashonize |
|
||||
| [`WizardConnect_reference.md`](./WizardConnect_reference.md) | ⏳ **Deferred** until a wallet-required dApp uses `inputPaths` (Path A) or the Nostr vision layer ships (Path B) | Two-layer situation: **shipping** = WC2 extension adding `inputPaths` HD-key metadata (Paytaca); **vision** = Nostr NIP-17 E2E stack. Both from Riften Labs |
|
||||
| [`WizardConnect_reference.md`](./WizardConnect_reference.md) | ⚠️ Research deep-dive, partly stale. **Its §0/§3.3 licence claim is WRONG** (it is LGPL-3.0, not proprietary) — see the guide below. | Nostr NIP-17 E2E transport + WC2-shape tx payload (`inputPaths` HD-key metadata). Riften Labs. Test wallet: **Cashonize v0.9.0+** (chipnet ✓); **Paytaca has no chipnet support**. |
|
||||
| [**`WIZARDCONNECT-INTEGRATION-GUIDE.md`**](./WIZARDCONNECT-INTEGRATION-GUIDE.md) | ✅ **Implemented (snapshots 0193/0194) + blocking bug fixed 2026-07-27.** Remaining work + the on-device test are listed in the guide. | **Read this first for anything WizardConnect.** Status, verified `file:line` map, protocol reference, the resolved LGPL question, traps, and a completion checklist. |
|
||||
|
||||
The runtime that uses all of the above lives at
|
||||
`shells/app/src/connect/` (the wallet-side responder, ADR-0043 + snapshot 0152)
|
||||
|
|
@ -30,7 +31,7 @@ files in this folder are **reference copies + dApp-developer documentation**.
|
|||
| **Wire encoding** | libauth `stringify`, JSON tree of EJSON markers | libauth `stringify`, single EJSON-string blob | Same as WC2 + `inputPaths` array | Nostr event |
|
||||
| **Covenant co-sign?** | ✅ Yes (validated 0057) | ❌ No at V0 (structural — see CC §6). **v-next: yes** via template-bound actions | Should work — pass-through inherits WC2 covenant support | TBD |
|
||||
| **Standard sends?** | ✅ | ✅ | ✅ | TBD |
|
||||
| **Wallets** | Cashonize, Paytaca, others | Cashonize | **Paytaca** | (future) |
|
||||
| **Wallets** | Cashonize, Paytaca, others | Cashonize | **Cashonize v0.9.0+** (chipnet ✓), Paytaca (**mainnet only — no chipnet**), Deviant | (future) |
|
||||
| **dApps** | Cauldron, BCH Bull, Tote/Checkers, Flipstarter, … | (CC's audience overlaps with WC) | Anything attaching `inputPaths` to WC2 tx objects | **ParyonUSD** |
|
||||
| **License (SDK)** | MIT | MIT | LGPL-3.0-or-later + CLA | Same |
|
||||
| **Reown project ID needed?** | Yes | Yes (same one) | Yes (same one — extends WC2) | No |
|
||||
|
|
|
|||
387
Parameters/WalletConnect/WIZARDCONNECT-INTEGRATION-GUIDE.md
Normal file
387
Parameters/WalletConnect/WIZARDCONNECT-INTEGRATION-GUIDE.md
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
# WizardConnect in Deviant — status, reference, and completion guide
|
||||
|
||||
**Last verified: 2026-07-27** against `@wizardconnect/core@0.2.4`,
|
||||
`@wizardconnect/wallet@0.2.2`, and the official docs at
|
||||
[docs.riftenlabs.com/wizardconnect](https://docs.riftenlabs.com/wizardconnect/).
|
||||
Every `file:line` below was checked against the working tree on that date.
|
||||
|
||||
> **Start here.** The integration is **already built and now unblocked.** It is not
|
||||
> a greenfield task. A blocking bug that made *every* sign request fail has been
|
||||
> fixed and regression-tested; the licence question that blocked mainnet has been
|
||||
> resolved. What remains is finishing work, listed in §7, plus one on-device test.
|
||||
|
||||
---
|
||||
|
||||
## 1. Status at a glance
|
||||
|
||||
| Piece | State |
|
||||
|---|---|
|
||||
| Nostr transport + protocol (`hdwalletv1`) | ✅ SDK does it — `@wizardconnect/wallet` |
|
||||
| `WalletAdapter` over Deviant's KeyStore | ✅ `wz-adapter.ts` |
|
||||
| Connection manager wrapper | ✅ `wz-handler.ts` |
|
||||
| URI routing (`wiz:` → `'wz'`) | ✅ `classifier.ts` + `qr/qr-uri.ts` |
|
||||
| Responder integration (sessions, sign events) | ✅ — **was broken, fixed 2026-07-27** |
|
||||
| Signing with `SIGHASH_ALL\|FORKID\|UTXOS` (0x61) | ✅ `signer.ts` — already supported |
|
||||
| HD paths `receive`/`change`/`defi` | ✅ `src/wallet/keystore.ts` |
|
||||
| Approval UI | ⚠️ works, but shows **hardcoded fake** pre-flight data (§7.1) |
|
||||
| Licence / mainnet | ✅ resolved — LGPL-3.0. One build change outstanding (§6) |
|
||||
| On-device verification | ❌ **never done** — the one thing nobody can skip (§8) |
|
||||
| ADR | ❌ none exists for the Nostr transport (§7.6) |
|
||||
|
||||
Tests: **162 passing**, `tsc --noEmit` clean (2026-07-27).
|
||||
|
||||
---
|
||||
|
||||
## 2. Why this stalled before
|
||||
|
||||
The earlier notes were half a picture for two reasons, both now removed:
|
||||
|
||||
1. **The licence was believed to block mainnet.** `wz-adapter.ts` and
|
||||
`wz-handler.ts` carried standing warnings that `@wizardconnect/*` was
|
||||
"proprietary-per-npm-view … mainnet ship needs explicit Riften Labs
|
||||
permission." That was wrong — see §6.
|
||||
2. **`WizardConnect_reference.md` §0/§3.3 still asserts "License: Proprietary …
|
||||
NOT LGPL as previous version of this doc claimed."** That "correction" was
|
||||
itself the error; the *earlier* version was right. **Do not trust §0/§3.3 of
|
||||
that file on licensing.** (`README.md` in this folder has it right.)
|
||||
|
||||
---
|
||||
|
||||
## 3. The file map (verified anchors)
|
||||
|
||||
### Wallet side — `shells/app/src/connect/`
|
||||
|
||||
| File | What it is |
|
||||
|---|---|
|
||||
| `wz-adapter.ts:117` | `createWizardConnectAdapter(opts): WalletAdapter` |
|
||||
| `wz-adapter.ts:89` | `buildXpubCache()` — pre-computes xpubs so the adapter's **sync** `getXpub` needs no I/O (KeyStore is async: biometrics, secure storage) |
|
||||
| `wz-adapter.ts:130` | `getRelayPrivateKey(uri)` = `sha256(sha256(utf8(uri)))` — deterministic per URI |
|
||||
| `wz-adapter.ts:139` | `getPublicKey()` deliberately **throws** — `WalletConnectionManager@0.2.2` never calls it |
|
||||
| `wz-adapter.ts:148` | `getXpub(path)` — reads the cache |
|
||||
| `wz-handler.ts:89` | `createWzHandler(opts): Promise<WzHandler>` |
|
||||
| `wz-handler.ts:100` | wraps `new WalletConnectionManager(adapter)` |
|
||||
| `wz-handler.ts:64` | the `WzHandler` interface — `pair`/`listConnections`/`approve`/`reject`/`disconnect`/`on`/`off` |
|
||||
| `responder.ts:279` | **the unwrap** (see §4) |
|
||||
| `responder.ts:255` | session chain, now from `allowedChains[0]` |
|
||||
| `responder.ts:135-136` | `wzPending` map + synthetic request ids from `1_000_000` |
|
||||
| `classifier.ts:33` | `export type Protocol = 'wc' \| 'cc' \| 'wz'` |
|
||||
| `classifier.ts:71-73` | `classifyUri` — `wiz:` → `'wz'` after lowercasing |
|
||||
| `signer.ts:51` | `WIZARDCONNECT_SIGHASH = allOutputs \| forkId \| utxos` (**0x61**) |
|
||||
| `signer.ts:57` | `ACCEPTED_SIGHASH = { 0x41, 0x61 }` — anything else throws |
|
||||
| `signer.ts:131` | `coSignTransaction(input)` |
|
||||
| `signer.ts:136` | the "malformed request" throw the bug used to hit |
|
||||
| `useConnect.ts:192` | conditional `createWzHandler(...)` |
|
||||
| `useConnect.ts:331` | passes `sighashType: WIZARDCONNECT_SIGHASH` |
|
||||
| `qr/qr-uri.ts:55` | **duplicate** `wiz:` routing — deliberately independent of the connector module. **Any new scheme must be added in both places.** |
|
||||
|
||||
### Core — `src/wallet/keystore.ts` (compiled to `dist/`, aliased `@core`)
|
||||
|
||||
| Anchor | What |
|
||||
|---|---|
|
||||
| `keystore.ts:265` | `HD_PATH_BRANCH = { receive: 0, change: 1, defi: 7 }` |
|
||||
| `keystore.ts:272` | `BCH_COIN_TYPE = 145` |
|
||||
| `keystore.ts:276` | `bip44PathFor(pathName, addressIndex, account = 0)` → `m/44'/145'/0'/<branch>/<index>` |
|
||||
| `keystore.ts:291` | `bip44BranchPathFor(pathName, account = 0)` → `m/44'/145'/0'/<branch>` — the depth `exportXpub` serialises at |
|
||||
| `keystore.ts:214` | `supportedPathNames()` — sync |
|
||||
| `keystore.ts:226` | `deriveChildKey(pathName, addressIndex)` |
|
||||
| `keystore.ts:247` | `exportXpub(pathName)` — branch-level xpub |
|
||||
|
||||
Three implementations, all complete: `WebKeyStore`, `CapacitorKeyStore`,
|
||||
`MultiKeyStore` (derives from the **primary** wallet's seed only).
|
||||
|
||||
### UI — `shells/app/src/screens/AppsScreen.tsx`
|
||||
|
||||
`SigningPreview` at **:674** is the approve screen; handlers `approveSign` :230 /
|
||||
`rejectSign` :239; router :258; QR entry :532.
|
||||
|
||||
---
|
||||
|
||||
## 4. The bug that was fixed (know this before touching `responder.ts`)
|
||||
|
||||
`SignTransactionRequest.transaction` **is not a transaction.** It is a
|
||||
`WcSignTransactionRequest` wrapper from `@bch-wc2/interfaces`:
|
||||
|
||||
```ts
|
||||
interface WcSignTransactionRequest {
|
||||
transaction: Transaction | string; // ← the actual tx (object OR hex)
|
||||
sourceOutputs: WcSourceOutput[];
|
||||
broadcast?: boolean;
|
||||
userPrompt?: string;
|
||||
}
|
||||
```
|
||||
|
||||
`sequence` and `inputPaths` live on the **outer** message; `sourceOutputs`,
|
||||
`broadcast` and `userPrompt` live **inside** the wrapper. The old code assigned
|
||||
the whole wrapper as `request.transaction` while simultaneously reaching *into*
|
||||
it for `sourceOutputs` — so `tx.inputs` was `undefined` and `coSignTransaction`
|
||||
threw *"malformed request — missing tx.inputs or sourceOutputs"* (`signer.ts:136`)
|
||||
on **every** WizardConnect request. Nothing could ever have been signed.
|
||||
|
||||
Now, at `responder.ts:279-310`, the responder:
|
||||
1. unwraps to the inner transaction;
|
||||
2. **decodes the hex-string variant** (`Transaction | string` — the signer cannot
|
||||
decode, so normalise here) and rejects undecodable input instead of surfacing
|
||||
it for approval;
|
||||
3. forwards `broadcast` and `userPrompt` (previously dropped, which is why every
|
||||
prompt read a generic "Approve transaction");
|
||||
4. takes the session chain from `allowedChains[0]`.
|
||||
|
||||
Regression tests: `test/connect-responder-compat.test.ts`, the four tests after
|
||||
`// ── Regression: the wrapper must be unwrapped ──`. They were verified to
|
||||
**fail against the old code** — reverting the mapping breaks three of them.
|
||||
|
||||
**Why the old tests missed it:** they asserted only `sessionId` / `id` / `kind`
|
||||
and never looked inside `request`. When adding coverage here, assert on the
|
||||
payload, not just the envelope.
|
||||
|
||||
---
|
||||
|
||||
## 5. Protocol reference (authoritative — do not re-derive)
|
||||
|
||||
Pinned against the shipped packages and the official docs.
|
||||
|
||||
| Question | Answer |
|
||||
|---|---|
|
||||
| Protocol name | `hdwalletv1` |
|
||||
| Transport | Nostr WebSocket relay — **not** WalletConnect |
|
||||
| Default relays | `wss://relay.riften.net:443` **and** `wss://relay.cauldron.quest:443` |
|
||||
| Encryption | NIP-17 gift-wrap (kind 14 rumor → 13 seal → 1059 wrap). Relay sees ciphertext only |
|
||||
| URI scheme | `wiz://?p=<pubkey>&s=<secret>`, plus QR-alphanumeric `WIZ://%3FP%3D…%26S%3D…`. Both start `wiz:` once lowercased |
|
||||
| Custom relay | `explicitRelayUrls`; when set, **only** that relay is used |
|
||||
| Reconnect | fixed `reconnectInterval` (default 5000 ms), no backoff |
|
||||
| Chunking | messages > NIP-44's 65 535-byte plaintext cap are split into `ChunkMessage`s (~30 000 B each) when both peers advertise `chunk` |
|
||||
| Handshake | `dapp_ready` ⇄ `wallet_ready`; wallet delivers `PathXpub[]`, dApp derives addresses locally |
|
||||
| Path names → branch | `receive`→0, `change`→1, `defi`→7 (Cauldron) |
|
||||
| xpub depth | at the **branch** node `m/44'/145'/0'/<branch>`; deriving child `n` equals `m/44'/145'/0'/<branch>/n` |
|
||||
| `inputPaths` | `[inputIndex, pathName, addressIndex][]` — `addressIndex` is a **`number`** here, but `bigint` in `getPubkey`. Convert at the boundary |
|
||||
| Sign response | `{ action, sequence, signedTransaction, error?, time }` — **no txid** |
|
||||
| Who broadcasts | **the dApp.** The wallet SDK has no broadcast step |
|
||||
| Message actions | `dapp_ready`, `wallet_ready`, `sign_transaction_request`, `sign_transaction_response`, `sign_cancel`, `disconnect`, `chunk`, `ping`, `pong` |
|
||||
| Message signing | **not supported** — `hdwalletv1` is transaction-signing only. Fall through to WC2 for `bch_signMessage` |
|
||||
| Session persistence | wallet side persists only *completed sign sequences*, not connections |
|
||||
| Replay defence | every message carries `time`; older-than-last-processed is discarded |
|
||||
|
||||
### The one non-negotiable
|
||||
|
||||
> "Wallets **MUST** sign every input with `SIGHASH_ALL | SIGHASH_FORKID |
|
||||
> SIGHASH_UTXOS` and MUST reject any request that would require different flags."
|
||||
|
||||
That is **0x61**, not WC2's 0x41. `SIGHASH_UTXOS` is what makes the `inputPaths`
|
||||
trust model safe: it commits to the exact set of UTXOs spent, so an input cannot
|
||||
be substituted after signing. Already handled — `signer.ts:51`, passed at
|
||||
`useConnect.ts:331`.
|
||||
|
||||
### Docs vs. shipped package — one real divergence
|
||||
|
||||
| | Published docs | `@wizardconnect/wallet@0.2.2` types |
|
||||
|---|---|---|
|
||||
| `getRelayPrivateKey` | `(): Uint8Array`, *"stability across restarts is not required"* | `(uri: string): Uint8Array`, *"should be deterministic per URI"* so reconnects keep the same Nostr identity and different dApps cannot correlate you |
|
||||
|
||||
**The package is what calls it.** Deviant already implements the URI-keyed form
|
||||
(`wz-adapter.ts:130`) — correct; leave it.
|
||||
|
||||
---
|
||||
|
||||
## 6. Licence — RESOLVED, one task outstanding
|
||||
|
||||
`@wizardconnect/{core,wallet,dapp}` are **LGPL-3.0-or-later**, free for
|
||||
commercial and non-commercial use. `npm view` renders "Proprietary" **only**
|
||||
because the packages omit the `license` field and exclude `LICENSE.txt` from
|
||||
their published `files`. Confirmed three independent ways:
|
||||
|
||||
1. Repo-root `LICENSE.txt` at `gitlab.com/riftenlabs/lib/wizardconnect` —
|
||||
verbatim LGPL-3.0, 7652 bytes, **no additional terms appended**.
|
||||
2. Repo `README.md`: *"LGPL-3.0-or-later. Free for commercial and non-commercial
|
||||
use. Modifications to the library must be released under the same license."*
|
||||
3. Riften Labs' own launch announcement: *"Open source (LGPL)"*.
|
||||
|
||||
**Mainnet is no longer blocked.** The headers in `wz-adapter.ts` and
|
||||
`wz-handler.ts` have been corrected.
|
||||
|
||||
We *link* the library and do not modify it, so this is LGPL **§4 Combined Work**
|
||||
and copyleft does **not** propagate to Deviant's own code. Obligations:
|
||||
|
||||
- **§4(a)** — say in-app that WizardConnect is used and is LGPL-covered
|
||||
(About/Licences screen).
|
||||
- **§4(b)(c)** — ship copies of **LGPL-3.0 and GPL-3.0** with the app.
|
||||
- **§4(d)** — let a user substitute their own build. Easiest is **§4(d)(1)**: a
|
||||
"suitable shared library mechanism" — keep `@wizardconnect/*` as its own chunk
|
||||
rather than inlined into a bundle with our code.
|
||||
|
||||
### ⚠️ Outstanding build task
|
||||
|
||||
`shells/app/vite.config.ts:91` has `manualChunks` rules for react, mainnet-js,
|
||||
libauth, cashscript and qrcode — but **none for `@wizardconnect`**, so it is
|
||||
currently folded into the app chunk. Add:
|
||||
|
||||
```ts
|
||||
if (id.includes('node_modules/@wizardconnect')) {
|
||||
// LGPL-3.0 §4(d)(1): keep the library a separately replaceable chunk.
|
||||
return 'vendor-wizardconnect';
|
||||
}
|
||||
```
|
||||
|
||||
Then add the two licence texts to the app's assets and a credit line to the
|
||||
About screen. Do this **before** any mainnet build.
|
||||
|
||||
Full analysis: SilentMode `Decentralized.DNS/WIZARDCONNECT-LICENSE-FINDING.md`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Remaining work, in the order worth doing it
|
||||
|
||||
### 7.1 Replace the fake pre-flight block *(highest value — it is misleading)*
|
||||
|
||||
`AppsScreen.tsx:708-714` renders a **hardcoded** "Pre-flight checks" section:
|
||||
literal `✓ no token UTXOs spent`, `✓ within normal range`, and a hardcoded
|
||||
recipient `bchtest:qp2…7lm`. None of it is computed from the request.
|
||||
|
||||
This is worse than showing nothing: it tells a user their transaction was checked
|
||||
when it was not, on the one screen where they decide whether to sign. Derive
|
||||
these from `req.request.transaction.outputs` / `sourceOutputs`, or delete the
|
||||
block. Also render `userPrompt` (now forwarded) verbatim — the protocol expects
|
||||
the wallet to show the dApp's own wording.
|
||||
|
||||
### 7.2 Surface the SDK events that are dropped
|
||||
|
||||
`wz-handler.ts:100-104` re-emits only `pendingSignRequest` and
|
||||
`connectionsChanged`. `WalletConnectionManager` also emits
|
||||
`connectionStatusChanged`, `remoteDisconnect`, `signCancelled`, and `message`.
|
||||
Without `signCancelled` the UI can leave a stale approval on screen after the
|
||||
dApp has cancelled — the user then approves something nobody is waiting for.
|
||||
|
||||
### 7.3 Re-hydrate sessions on reload *(affects WC and CC too)*
|
||||
|
||||
`responder.ts:126` keeps sessions in an in-memory `Map`. Nothing calls
|
||||
`walletKit.getActiveSessions()` anywhere in `src/`, so WC sessions WalletKit
|
||||
restores from its own IndexedDB never reappear in the responder's map after a
|
||||
reload. WZ has the same shape of problem (the SDK persists only completed sign
|
||||
sequences).
|
||||
|
||||
### 7.4 Fix the misleading WIF-restored-wallet path
|
||||
|
||||
`useConnect.ts:190` gates WZ on `keyStore.supportedPathNames()` including
|
||||
`receive`/`change`/`defi`. That array is **static**, so a WIF-restored wallet
|
||||
passes the gate, then fails inside `buildXpubCache` when `exportXpub` throws
|
||||
`'not-supported'`. The user is told *"WizardConnect not supported by this build"*
|
||||
when the truth is *"this wallet was imported from a WIF and has no HD tree."*
|
||||
|
||||
### 7.5 Decouple WZ from the Reown project ID
|
||||
|
||||
`useConnect.ts:160` awaits `getWalletKit()` before anything else, and
|
||||
`getWalletKit` throws when `VITE_WC_PROJECT_ID` is unset (`useConnect.ts:63-68`).
|
||||
So a missing Reown ID kills the **Nostr** path too — even though WizardConnect
|
||||
needs no Reown account at all. Make WZ initialise independently of WalletKit.
|
||||
|
||||
### 7.6 Write the ADR + refresh the wiki
|
||||
|
||||
There is **no ADR for the Nostr transport**. ADR-0043 (`wiki/decisions.md`)
|
||||
predates it and still describes `classifyUri` as returning `'wc' | 'cc' | null`.
|
||||
`wiki/wallet/connector.md:120` still lists "WizardConnect probe — pair the shell
|
||||
with Paytaca" as an open question. Two notes for whoever writes it:
|
||||
|
||||
- **Paytaca cannot be the test wallet — it has no chipnet support.** Use
|
||||
**Cashonize v0.9.0+** (2026-07-25 added WizardConnect; supports chipnet).
|
||||
- The transport is Nostr, so several WC2 assumptions do not apply: there is no
|
||||
`session_proposal` (pairing is unilateral — the responder *manufactures* a
|
||||
session on the first sign request, `responder.ts:243-259`), and there are no
|
||||
per-request ids (hence the synthetic counter at `responder.ts:136`).
|
||||
|
||||
### 7.7 Relay policy
|
||||
|
||||
`@wizardconnect/core` hardcodes the two default relays. Deviant supplies no
|
||||
override. Consider exposing `explicitRelayUrls` in settings — running your own
|
||||
relay is one of the protocol's selling points.
|
||||
|
||||
---
|
||||
|
||||
## 8. On-device verification (the step nobody can skip)
|
||||
|
||||
Nothing here has ever been exercised against a real dApp. There is now a live
|
||||
counterpart to test against: the Silent Mode name registrar, which speaks
|
||||
WizardConnect as a dApp and is chipnet-only.
|
||||
|
||||
**Setup**
|
||||
1. Deviant on **chipnet** with an HD wallet (not WIF-restored) and a few test coins.
|
||||
2. Open the Silent Mode register page (`silentmode.st/register.html`, or serve
|
||||
`D:\Dev\SilentMode\site` locally).
|
||||
3. Search a free name → *Connect a wallet — WizardConnect* → scan/paste the
|
||||
`wiz://` URI into Deviant's QR scanner.
|
||||
|
||||
**What should happen**
|
||||
- `wallet_ready` delivers three xpubs; the dApp derives addresses locally and
|
||||
reads your balance with no further round-trips.
|
||||
- You are asked for **one** approval — or **two** if your wallet holds no coin at
|
||||
output index 0. That second one is a one-off self-send, not a fee: CashTokens
|
||||
can only create a token category in a transaction that spends an outpoint at
|
||||
index 0. The page states which before you commit.
|
||||
- Approving mints a **name certificate** to your own address. The dApp
|
||||
broadcasts, not the wallet.
|
||||
|
||||
**What to report back**
|
||||
- Did the wallet accept `inputPaths`, and derive the right child key per input?
|
||||
- Did it sign with **0x61**? (`signer.ts` should; confirm end-to-end.)
|
||||
- Did `userPrompt` render verbatim on the approval screen?
|
||||
- Did the certificate land at *your* address? Check with
|
||||
`node Argus/src/lib/resolve.js <name>` in SilentMode, or navigate.st.
|
||||
|
||||
**A cheap first smoke test** that needs no dApp: pair, and confirm three xpubs
|
||||
reach the dApp and derive the addresses you expect. That alone exercises the
|
||||
relay, key exchange, NIP-17 wrap/unwrap, and `buildXpubCache`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Traps
|
||||
|
||||
- **`wiz:` routing lives in two files.** `classifier.ts:71` *and* `qr/qr-uri.ts:55`.
|
||||
Deliberate (QR routing must not depend on the connector module) — change both.
|
||||
- **`isOwnedInput` is exported but `coSignTransaction` does not call it.** It
|
||||
inlines an equivalent check at `signer.ts:207`, with asymmetric behaviour worth
|
||||
knowing: an explicit `inputPaths` mapping that does not match the source output
|
||||
is a **hard throw** that aborts the whole request; an unmapped foreign input is
|
||||
**silently skipped**. `isOwnedInput` is used only by tests.
|
||||
- **`sighashType` applies to every input**, not per input.
|
||||
- **`getPublicKey` throws by design** (`wz-adapter.ts:139`) — the manager never
|
||||
calls it at 0.2.2. If you upgrade the SDK, check that assumption first.
|
||||
- **The tx may arrive as hex.** Handled at `responder.ts:290` now; do not remove.
|
||||
- **`addressIndex` is `number` in `inputPaths` but `bigint` in `getPubkey`.**
|
||||
- **`@wizardconnect/core` re-exports libauth symbols.** Deviant's app shell has
|
||||
`@bitauth/libauth@3.1.0-next.8` — the same version the SDK expects. Keep them
|
||||
aligned; two copies of libauth in one bundle causes a genuinely nasty
|
||||
initialisation race (its WASM crypto uses top-level await, and a second copy
|
||||
can hand out `undefined` for `secp256k1` on cold load).
|
||||
|
||||
---
|
||||
|
||||
## 10. Cross-references
|
||||
|
||||
- `README.md` (this folder) — protocol comparison table. Licence row is correct here.
|
||||
- `WizardConnect_reference.md` — deep dive. **§0/§3.3 licensing is wrong** (§2 above).
|
||||
- `WalletConnect_integration_reference.md` — WC2. §5 wire format and §6.4 signing
|
||||
discipline apply to WizardConnect too; the tx payload is the same WC2 object.
|
||||
- `CashConnect_reference.md` — §6 explains why wallet-orchestrated protocols cannot
|
||||
carry structure-strict transactions. Same reason CashConnect cannot mint a name
|
||||
certificate, while WizardConnect can: pass-through vs. orchestration.
|
||||
- Official docs: [protocol](https://docs.riftenlabs.com/wizardconnect/protocol/) ·
|
||||
[wallet](https://docs.riftenlabs.com/wizardconnect/wallet/) ·
|
||||
[transport](https://docs.riftenlabs.com/wizardconnect/transport/) ·
|
||||
[pubkey derivation](https://docs.riftenlabs.com/wizardconnect/pubkey-derivation/) ·
|
||||
[dapp](https://docs.riftenlabs.com/wizardconnect/dapp/)
|
||||
- The dApp counterpart: SilentMode `Argus/src/lib/connect/wizardconnect.js`, and
|
||||
`Decentralized.DNS/REGISTRATION-STATUS.md` for how the registration flow uses it.
|
||||
|
||||
---
|
||||
|
||||
## 11. Definition of done
|
||||
|
||||
- [ ] A registration signed on-device through Deviant, certificate at the user's
|
||||
own address, name resolving from the chain (§8).
|
||||
- [ ] Approval screen shows real, request-derived data and the dApp's `userPrompt`
|
||||
(§7.1).
|
||||
- [ ] `signCancelled` and `remoteDisconnect` handled in the UI (§7.2).
|
||||
- [ ] Sessions survive an app reload (§7.3).
|
||||
- [ ] WIF-restored wallets get an honest message (§7.4).
|
||||
- [ ] WZ works without a Reown project ID (§7.5).
|
||||
- [ ] `vendor-wizardconnect` chunk + licence texts + About-screen credit (§6).
|
||||
- [ ] ADR written; ADR-0043 and `wiki/wallet/connector.md` updated (§7.6).
|
||||
- [ ] `WizardConnect_reference.md` §0/§3.3 licence claim corrected (§2).
|
||||
|
|
@ -6,6 +6,19 @@ part of this doc. Reference for evaluating it as a third Apps-tab protocol in
|
|||
the Deviant wallet alongside WalletConnect (`WalletConnect_integration_reference.md`)
|
||||
and CashConnect (`CashConnect_reference.md`). **Not yet implemented in Deviant.**_
|
||||
|
||||
> **⏱ Update 2026-07-25 — adoption has moved; older "Paytaca-only" facts below are stale.**
|
||||
> - **Cashonize v0.9.0** (released 2026-07-25, desktop Win/Linux + Android) **added
|
||||
> WizardConnect** — so wallet support is **no longer Paytaca-only**. The same
|
||||
> release ships an **upgraded CashConnect "now over Nostr"** and a unified
|
||||
> sessions view for all three connectors. Source: Cashonize v0.9.0 announcement
|
||||
> (TradingView/CoinMarketCal, 25 Jul 2026).
|
||||
> - **Broader adoption reported** (multiple wallets + dApps beyond Paytaca) but
|
||||
> **not yet independently verified per-wallet/per-dApp here** — re-pin against
|
||||
> the SDK + each wallet's release notes when confirming. Treat the §0/§0a tables
|
||||
> below as the 2026-06-29 source-pinned snapshot, corrected by this note.
|
||||
> - Notable second-order fact: **CashConnect moving to Nostr transport** narrows
|
||||
> the WC2-vs-CC-vs-WZ distinction — two of the three now ride Nostr.
|
||||
|
||||
**Sources (both partially describe the same thing — pinned against the
|
||||
installed source 2026-06-29):**
|
||||
- <https://docs.riftenlabs.com/wizardconnect/> — describes the **transport
|
||||
|
|
@ -61,12 +74,12 @@ client, not extend the existing WC handler with `inputPaths` support.
|
|||
| Sign-tx payload | `SignTransactionRequest.transaction: WcSignTransactionRequest` (imported from `@bch-wc2/interfaces`) + `inputPaths: [inputIndex, pathName, addressIndex][]` |
|
||||
| Message actions | `dapp_ready`, `wallet_ready`, `sign_transaction_request`, `sign_transaction_response`, `sign_cancel`, `disconnect`, `chunk`, `ping`, `pong` |
|
||||
| Reconnection | Auto — both sides reconnect independently (per Riften Labs docs) |
|
||||
| Wallet support | **Paytaca** (reference wallet) |
|
||||
| dApp support | **Cauldron** (per docs.riftenlabs.com/cauldron/), **ParyonUSD** |
|
||||
| Wallet support | **Paytaca** (reference wallet) + **Cashonize v0.9.0** (added 2026-07-25). No longer Paytaca-only; broader adoption reported, verify per-wallet. |
|
||||
| dApp support | **Cauldron** (per docs.riftenlabs.com/cauldron/), **ParyonUSD** — 2026-06-29 snapshot; likely broader now (unverified) |
|
||||
| SDK packages | `@wizardconnect/core@0.2.4`, `@wizardconnect/wallet@0.2.2`, `@wizardconnect/dapp@0.2.2`, `@wizardconnect/react@?` |
|
||||
| Dependencies of core | `@bch-wc2/interfaces`, `nostr-tools`, `@bitauth/libauth`, `eventemitter3`, `isomorphic-ws`, `lossless-json`, `ws` |
|
||||
| License | **Proprietary** (per `npm view @wizardconnect/core`) — NOT LGPL as previous version of this doc claimed |
|
||||
| Status in Deviant | **Not yet implemented.** Adapter design in §4 below. |
|
||||
| License | **LGPL-3.0-or-later** — free for commercial + non-commercial use. `npm view` shows "Proprietary" only because the packages omit the `license` field + exclude `LICENSE.txt` from `files`. Verified 2026-07-27 against `gitlab.com/riftenlabs/lib/wizardconnect` `LICENSE.txt` (verbatim LGPL, no additional terms), repo README, and Riften Labs' launch announcement. **Correction to earlier "Proprietary … NOT LGPL" claim in this doc — that "correction" was itself the error.** See `WIZARDCONNECT-INTEGRATION-GUIDE.md` §6 for the LGPL §4 obligations. |
|
||||
| Status in Deviant | **Implemented + fixed.** Adapter, handler, responder wiring, and unwrap bug fix all in `shells/app/src/connect/`. On-device pairing pending — see `WIZARDCONNECT-INTEGRATION-GUIDE.md` §8. |
|
||||
|
||||
## 0b. What CashScript itself says (verbatim summary)
|
||||
|
||||
|
|
@ -122,7 +135,7 @@ dApps to reason about purpose-of-address.
|
|||
|
||||
## 2. SDK packages
|
||||
|
||||
Four packages (**Proprietary license** — corrected 2026-06-29):
|
||||
Four packages (**LGPL-3.0-or-later** — corrected 2026-07-27; see §0 table):
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
|
|
@ -179,11 +192,19 @@ material a session may see.
|
|||
|
||||
### 3.3 License implications
|
||||
|
||||
**Proprietary.** Consuming the packages as a dependency is legally standard
|
||||
provided the terms allow (verify with npm-view of the LICENSE field before
|
||||
adopting — proprietary bundles sometimes have restrictions on production
|
||||
use, redistribution, or embedded shipping). The Deviant wallet is
|
||||
open-source; check the SDK's actual LICENSE file if adopting.
|
||||
**LGPL-3.0-or-later** (verified 2026-07-27 — see §0 table). Free for
|
||||
commercial + non-commercial use. We *link* the library and do not modify
|
||||
it, so this is LGPL **§4 Combined Work** — copyleft does **not** propagate
|
||||
to Deviant's own code. Obligations:
|
||||
|
||||
- **§4(a)** — attribute WizardConnect + LGPL in the About/Licences screen.
|
||||
- **§4(b)(c)** — ship copies of LGPL-3.0 and GPL-3.0 with the app.
|
||||
- **§4(d)(1)** — keep `@wizardconnect/*` a separately replaceable chunk
|
||||
(add `manualChunks` rule in `vite.config.ts`).
|
||||
|
||||
Full checklist in `WIZARDCONNECT-INTEGRATION-GUIDE.md` §6. **Mainnet is
|
||||
no longer blocked by licensing** (was previously flagged as blocked; that
|
||||
flag was based on the incorrect Proprietary reading).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -295,7 +316,204 @@ its swap + LP flows genuinely need HD-key-per-input mapping.
|
|||
|
||||
---
|
||||
|
||||
## 5. Why it's still interesting to track
|
||||
## 5. dApp-side integration reference (from docs.riftenlabs.com/wizardconnect/dapp/)
|
||||
|
||||
The reference above (§4) covers the **wallet-side** SDK — the surface Deviant
|
||||
implements to serve WizardConnect dApps. This section documents the
|
||||
**dApp-side** SDK — the surface a Deviant-facing dApp (Cauldron, or a
|
||||
Deviant-authored dApp like the tote) implements to pair with wallets.
|
||||
Pinned against the same package generation (`@wizardconnect/dapp@0.2.2`).
|
||||
|
||||
### 5.1 `DappConnectionManager` — the primary class
|
||||
|
||||
```ts
|
||||
constructor(
|
||||
dappName?: string,
|
||||
dappIcon?: string,
|
||||
options?: { session?: DappSessionOptions | false; },
|
||||
)
|
||||
```
|
||||
|
||||
Manages **one dApp↔wallet session** — handshake, xpub storage, sign-request
|
||||
round-trips. Extends `EventEmitter`.
|
||||
|
||||
### 5.2 Methods (grouped by purpose)
|
||||
|
||||
**Connection lifecycle:**
|
||||
- `updateConnection(client: RelayClient | null, status: RelayStatus): void`
|
||||
— called from the relay callback (`initiateDappRelay`), keeps the manager's
|
||||
internal relay-status view in sync.
|
||||
- `attachRelay(relay)` — bind the initialised relay to the manager.
|
||||
- `isWalletDiscovered(): boolean` — true once the wallet's `wallet_ready`
|
||||
message has been received. Used to gate address-derivation calls.
|
||||
- `sendDisconnect(message?: string): Promise<void>` — courtesy disconnect.
|
||||
- `clearStoredSession(): void` — wipes the persisted session.
|
||||
|
||||
**Sign requests (two levels):**
|
||||
- `signTransaction(request, options?): Promise<SignTransactionResponse>` —
|
||||
**the recommended high-level API.** Auto-fills `action`, `sequence`,
|
||||
`time`. Accepts `options.signal` for `AbortController` cancellation.
|
||||
- `sendSignRequest(request: SignTransactionRequest): Promise<SignTransactionResponse>`
|
||||
— low-level. Caller sets `sequence` (via `nextSequence()`) and `time`.
|
||||
- `sendSignCancel(sequence: number, reason?: string): Promise<void>` —
|
||||
cancel an in-flight request by sequence number.
|
||||
- `nextSequence(): number` — get the next monotonic sequence.
|
||||
|
||||
**Address derivation (via delegated `pubkeyState`):**
|
||||
- `getPubkey(childIndex: number, index: bigint): Uint8Array | undefined`
|
||||
- `getPubkeys(childIndex: number): Map<bigint, Uint8Array>`
|
||||
- `getAddressIndex(childIndex: number): bigint` / `setAddressIndex(...)`
|
||||
- `getIndexToUse(childIndex, options?): bigint` — smart index selection
|
||||
(dApps often want "next unused address"; this handles the bookkeeping).
|
||||
- `getIndexRange(childIndex): { min?: bigint; max?: bigint }`
|
||||
- `getXpubNode(childIndex): HdPublicNodeValid | undefined`
|
||||
|
||||
`childIndex` = the enum from `@wizardconnect/wallet` (`0` receive, `1`
|
||||
change, `7` defi/Cauldron). Same numbers as `bip44BranchPathFor` in
|
||||
Deviant's `src/wallet/keystore.ts`.
|
||||
|
||||
**Session cache:**
|
||||
- `getSessionPaths(): PathXpub[]`
|
||||
- `restoreSessionPaths(paths: PathXpub[]): void`
|
||||
|
||||
### 5.3 Events
|
||||
|
||||
- `walletready` — fires when the wallet sends `wallet_ready` (xpubs
|
||||
available). Payload includes `wallet_name` and the `PathXpub[]` list.
|
||||
- `messagesent` / `messagereceived` — protocol-level events for logging /
|
||||
debugging.
|
||||
- `disconnect` — wallet gone or protocol mismatch. Handler receives
|
||||
`(reason: DisconnectReason, message?: string)`.
|
||||
|
||||
### 5.4 Connection lifecycle (verbatim code from docs)
|
||||
|
||||
```ts
|
||||
// 1. Construct + initiate relay
|
||||
const dappMgr = new DappConnectionManager('My Dapp', 'https://…/icon.png');
|
||||
const relay = initiateDappRelay((payload) => {
|
||||
dappMgr.updateConnection(payload.client, payload.status);
|
||||
});
|
||||
dappMgr.attachRelay(relay);
|
||||
console.log('Scan this URI:', relay.uri); // wiz://?p=…&s=…
|
||||
|
||||
// 2. Wait for wallet
|
||||
dappMgr.on('walletready', (msg) => {
|
||||
console.log('Wallet:', msg.wallet_name);
|
||||
// xpubs now available — dApp can derive addresses locally
|
||||
});
|
||||
|
||||
// 3. Derive addresses (any time after walletready)
|
||||
const RECEIVE = 0;
|
||||
const pubkey = dappMgr.getPubkey(RECEIVE, 0n);
|
||||
|
||||
// 4. Sign a transaction (high-level, recommended)
|
||||
const response = await dappMgr.signTransaction({
|
||||
transaction: {
|
||||
transaction: txHex,
|
||||
sourceOutputs,
|
||||
userPrompt: 'Confirm swap',
|
||||
broadcast: true,
|
||||
},
|
||||
inputPaths: [[0, 'receive', 0], [1, 'defi', 5]],
|
||||
});
|
||||
console.log('Signed tx:', response.signedTransaction);
|
||||
|
||||
// 5. Disconnect
|
||||
dappMgr.clearStoredSession();
|
||||
await dappMgr.sendDisconnect('user closed the tab');
|
||||
relay.cleanup();
|
||||
```
|
||||
|
||||
### 5.5 Cancellation (AbortController)
|
||||
|
||||
```ts
|
||||
const controller = new AbortController();
|
||||
try {
|
||||
const response = await dappMgr.signTransaction(
|
||||
{ transaction: {...}, inputPaths: [...] },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') console.log('Cancelled');
|
||||
}
|
||||
```
|
||||
|
||||
Under the hood: `signTransaction` sends the request, then races the
|
||||
promise against the abort signal. On abort, calls `sendSignCancel(seq)`.
|
||||
|
||||
### 5.6 Session persistence
|
||||
|
||||
**Enabled by default.** The manager auto-restores xpub paths at construction
|
||||
and saves wallet metadata + xpubs on `wallet_ready`.
|
||||
|
||||
Config options:
|
||||
|
||||
| Option | Effect |
|
||||
|---|---|
|
||||
| `session` omitted | Default — localStorage, key `wizardconnect-session` |
|
||||
| `session: { key: 'my-app-session' }` | Custom localStorage key |
|
||||
| `session: { storage: myCustomStorage }` | Custom storage backend (must expose `getItem` / `setItem` / `removeItem`) |
|
||||
| `session: false` | Disable persistence entirely |
|
||||
|
||||
Manual access if you need to interoperate with the persistence layer:
|
||||
|
||||
```ts
|
||||
const session = loadSession(); // standalone loader
|
||||
if (session?.walletPublicKey) {
|
||||
const relay = initiateDappRelay(callback, {
|
||||
existingCredentials: session,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 5.7 Reconnection semantics
|
||||
|
||||
When the relay's status transitions to `'connected'`, the manager sends a
|
||||
fresh `dapp_ready`. `walletDiscovered` state **persists across reconnects**.
|
||||
Any pending sign requests are automatically **re-sent on `wallet_ready`**,
|
||||
keeping the caller's promise valid — the dApp doesn't need to manually
|
||||
retry sign requests during a transient disconnect.
|
||||
|
||||
### 5.8 React integration (`@wizardconnect/react`)
|
||||
|
||||
Hook: `useWizardConnect({ dappName })` returns `{ manager, ... }`.
|
||||
|
||||
```tsx
|
||||
const wc = useWizardConnect({ dappName: 'My Dapp' });
|
||||
|
||||
useEffect(() => {
|
||||
if (!wc.manager) return;
|
||||
const wallet = new MyWalletAdapter(wc.manager);
|
||||
// dispatch to store …
|
||||
}, [wc.manager]);
|
||||
```
|
||||
|
||||
The hook manages session persistence, auto-reconnect, and cleanup
|
||||
automatically. Suitable when a Deviant-authored dApp is built as a React
|
||||
SPA.
|
||||
|
||||
### 5.9 Implications for Deviant proposer (`src/connect/proposer.ts`)
|
||||
|
||||
The Deviant proposer library today wraps WalletConnect v2's dApp-side
|
||||
sign-client. **If a Deviant-authored dApp (tote, escrow, etc.) needs to
|
||||
support WizardConnect wallets**, the natural extension pattern is:
|
||||
|
||||
- Add a `WzSession` class in the proposer that wraps `DappConnectionManager`
|
||||
(mirrors the existing `WcSession` / `CcSession` pattern in
|
||||
`spike/wc-connect/`).
|
||||
- Expose `signTransaction(request, options?)` on the session; the caller
|
||||
builds a WC2-shape tx + `inputPaths` list and gets a signed hex back.
|
||||
- Reuse Deviant's `WcTransactionObject` builder — the `transaction` +
|
||||
`sourceOutputs` payload is identical between WC and WizardConnect.
|
||||
Only the transport wrapper differs.
|
||||
|
||||
Checkers explicitly asked about this in
|
||||
`Checkers-response-to-instructions.md` §Ask 2. If the Deviant proposer
|
||||
grows a WzSession, Checkers can vendor it and skip reimplementing.
|
||||
|
||||
---
|
||||
|
||||
## 6. Why it's still interesting to track
|
||||
|
||||
- The **xpub-handshake-then-derive-locally** model is genuinely better
|
||||
than WC2's per-address pattern. If the BCH dApp ecosystem migrates
|
||||
|
|
@ -308,7 +526,7 @@ its swap + LP flows genuinely need HD-key-per-input mapping.
|
|||
|
||||
---
|
||||
|
||||
## 6. Pinned facts (2026-06-29 tarball inspection) + remaining open items
|
||||
## 7. Pinned facts (2026-06-29 tarball inspection) + remaining open items
|
||||
|
||||
**Pinned from `@wizardconnect/core@0.2.4`:**
|
||||
- URI scheme: **`wiz://`** (QR-safe form `WIZ://%3F...`)
|
||||
|
|
@ -338,7 +556,7 @@ its swap + LP flows genuinely need HD-key-per-input mapping.
|
|||
|
||||
---
|
||||
|
||||
## 7. Cross-references
|
||||
## 8. Cross-references
|
||||
|
||||
- `WalletConnect_integration_reference.md` — WC2 reference (this folder)
|
||||
- `CashConnect_reference.md` — CC reference (this folder)
|
||||
|
|
|
|||
27
Parameters/WalletConnect/WizardConnect_sources.md
Normal file
27
Parameters/WalletConnect/WizardConnect_sources.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
Introducing ✨🔮 WizardConnect 🔮✨
|
||||
|
||||
WizardConnect is a brand new, Bitcoin Cash-first wallet connection protocol built from the ground up by @RiftenLabs 🪄
|
||||
|
||||
WizardConnect is:
|
||||
🔮 Bitcoin Cash first
|
||||
🔮 Open source (LGPL)
|
||||
🔮 Fast, stable & reliable
|
||||
🔮 Full HD-wallet compatable
|
||||
🔮 Supports multiple addresses
|
||||
🔮 Secure & decentralized using Nostr
|
||||
🔮 E2E encrypted (run on any relay you want)
|
||||
🔮 Future-proof and designed for multisig, NFTs & more
|
||||
|
||||
WalletConnect was never made for BCH. WizardConnect is.
|
||||
|
||||
Documentation:
|
||||
🔮 Relay transport Guide:
|
||||
https://docs.riftenlabs.com/wizardconnect/transport/
|
||||
🔮 Wallet Integration Guide:
|
||||
https://docs.riftenlabs.com/wizardconnect/dapp/
|
||||
🔮 Protocol Specification:
|
||||
https://docs.riftenlabs.com/wizardconnect/wallet/
|
||||
🔮 All technical deep-dives:
|
||||
https://docs.riftenlabs.com/wizardconnect/protocol/
|
||||
|
||||
The WizardConnect protocol is fully open source and built with love for Bitcoin Cash, by @RiftenLabs 🪄
|
||||
|
|
@ -125,6 +125,10 @@ export type SignRequestPreview =
|
|||
export interface ResponderEvents {
|
||||
onSessionProposal: (proposal: SessionProposal) => void;
|
||||
onSignRequest: (request: SignRequestPreview) => void;
|
||||
/** The dApp cancelled a pending sign request the wallet already surfaced.
|
||||
* UI must clear the approval screen if it is showing this request — an
|
||||
* approve tap afterwards would ship a signature nobody is waiting for. */
|
||||
onSignRequestCancel: (requestId: number, reason?: string) => void;
|
||||
onSessionDelete: (sessionId: string) => void;
|
||||
onError: (error: Error) => void;
|
||||
}
|
||||
|
|
|
|||
144
shells/app/src/connect/preflight.ts
Normal file
144
shells/app/src/connect/preflight.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/**
|
||||
* preflight.ts — derive user-visible pre-flight facts from a sign-tx request.
|
||||
*
|
||||
* The SigningPreview screen used to render hardcoded strings ("no token UTXOs
|
||||
* spent", "within normal range", a literal `bchtest:qp2…7lm` recipient) —
|
||||
* misleading on the one screen where a user decides whether to sign. This
|
||||
* module computes those facts from the actual request instead.
|
||||
*
|
||||
* Everything is defensive: any decode failure returns `null`/omission rather
|
||||
* than throwing, so a malformed field never blocks the preview. The rendered
|
||||
* block should hide any row it doesn't have data for.
|
||||
*
|
||||
* Pure — no I/O, no libauth WASM required beyond the address encoder.
|
||||
*/
|
||||
import { lockingBytecodeToCashAddress } from '@bitauth/libauth';
|
||||
import type { BchSignTxRequest } from './index';
|
||||
|
||||
export interface PreflightOutput {
|
||||
/** Best-effort recipient. Cashaddr when the locking bytecode decodes cleanly,
|
||||
* the hex of the bytecode otherwise (op_return, non-standard, etc.). */
|
||||
readonly recipient: string;
|
||||
/** Output value in bits (= satoshis; naming per project convention). */
|
||||
readonly bits: number;
|
||||
/** True if this output carries a CashToken. */
|
||||
readonly hasToken: boolean;
|
||||
}
|
||||
|
||||
export interface Preflight {
|
||||
readonly outputs: readonly PreflightOutput[];
|
||||
/** Fee in bits. `null` when `sourceOutputs` is empty/missing — we cannot
|
||||
* compute (inputs − outputs) without knowing what's being spent. */
|
||||
readonly feeBits: number | null;
|
||||
/** True if any source output being spent carries a CashToken. */
|
||||
readonly anyTokenInput: boolean;
|
||||
/** True if any created output carries a CashToken. */
|
||||
readonly anyTokenOutput: boolean;
|
||||
/** Sum of output values, in bits. */
|
||||
readonly totalOutBits: number;
|
||||
}
|
||||
|
||||
interface MinimalOutput {
|
||||
lockingBytecode?: Uint8Array;
|
||||
valueSatoshis?: bigint;
|
||||
token?: unknown;
|
||||
}
|
||||
|
||||
interface MinimalTx {
|
||||
outputs?: readonly MinimalOutput[];
|
||||
}
|
||||
|
||||
/** The cashaddr prefix is chosen by network. Chipnet + testnet4 both use
|
||||
* `bchtest`; regtest uses `bchreg`; mainnet uses `bitcoincash`. */
|
||||
export type CashAddrPrefix = 'bitcoincash' | 'bchtest' | 'bchreg';
|
||||
|
||||
export function computePreflight(
|
||||
request: BchSignTxRequest,
|
||||
prefix: CashAddrPrefix = 'bchtest',
|
||||
): Preflight | null {
|
||||
const tx = request.transaction as MinimalTx | undefined;
|
||||
if (!tx || !Array.isArray(tx.outputs)) return null;
|
||||
|
||||
const outputs: PreflightOutput[] = [];
|
||||
let totalOut = 0n;
|
||||
let anyTokenOutput = false;
|
||||
|
||||
for (const o of tx.outputs) {
|
||||
const bytecode = o.lockingBytecode;
|
||||
const value = typeof o.valueSatoshis === 'bigint' ? o.valueSatoshis : 0n;
|
||||
totalOut += value;
|
||||
const hasToken = Boolean(o.token);
|
||||
if (hasToken) anyTokenOutput = true;
|
||||
|
||||
let recipient = '';
|
||||
if (bytecode instanceof Uint8Array) {
|
||||
const result = lockingBytecodeToCashAddress({
|
||||
bytecode,
|
||||
prefix,
|
||||
tokenSupport: hasToken,
|
||||
});
|
||||
if (typeof result === 'object' && 'address' in result) {
|
||||
recipient = result.address;
|
||||
} else {
|
||||
// Non-standard bytecode (op_return, custom covenant lock) — show
|
||||
// an unambiguous "script" marker + short hex fingerprint.
|
||||
recipient = 'script:' + bytesToShortHex(bytecode);
|
||||
}
|
||||
} else {
|
||||
recipient = '(missing lockingBytecode)';
|
||||
}
|
||||
|
||||
outputs.push({
|
||||
recipient,
|
||||
bits: Number(value),
|
||||
hasToken,
|
||||
});
|
||||
}
|
||||
|
||||
// Fee = inputs − outputs. `sourceOutputs` is authoritative for input values
|
||||
// in the WC2 wire format — a raw tx doesn't carry them (they're on-chain,
|
||||
// not in the tx). Without it we can't compute a fee honestly.
|
||||
let feeBits: number | null = null;
|
||||
let anyTokenInput = false;
|
||||
const sources = request.sourceOutputs;
|
||||
if (Array.isArray(sources) && sources.length > 0) {
|
||||
let totalIn = 0n;
|
||||
let sawValidValue = false;
|
||||
for (const s of sources) {
|
||||
const src = s as MinimalOutput;
|
||||
if (typeof src.valueSatoshis === 'bigint') {
|
||||
totalIn += src.valueSatoshis;
|
||||
sawValidValue = true;
|
||||
}
|
||||
if (src.token) anyTokenInput = true;
|
||||
}
|
||||
if (sawValidValue) {
|
||||
const fee = totalIn - totalOut;
|
||||
feeBits = Number(fee);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
outputs,
|
||||
feeBits,
|
||||
anyTokenInput,
|
||||
anyTokenOutput,
|
||||
totalOutBits: Number(totalOut),
|
||||
};
|
||||
}
|
||||
|
||||
/** Elide a Uint8Array to `abcd…efgh` for a compact non-address display. */
|
||||
function bytesToShortHex(bin: Uint8Array): string {
|
||||
const hex = Array.from(bin, (b) => b.toString(16).padStart(2, '0')).join('');
|
||||
if (hex.length <= 10) return hex;
|
||||
return hex.slice(0, 4) + '…' + hex.slice(-4);
|
||||
}
|
||||
|
||||
/** Elide a cashaddr for compact display: `bchtest:qp2xxx…yylm`. */
|
||||
export function shortenCashAddr(addr: string): string {
|
||||
const colon = addr.indexOf(':');
|
||||
const head = colon >= 0 ? addr.slice(0, colon + 1) : '';
|
||||
const body = colon >= 0 ? addr.slice(colon + 1) : addr;
|
||||
if (body.length <= 10) return addr;
|
||||
return head + body.slice(0, 4) + '…' + body.slice(-4);
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
* so the active KeyStore (Capacitor secure-storage / WebKeyStore) stays in charge.
|
||||
*/
|
||||
import { WalletKit, type WalletKitTypes } from '@reown/walletkit';
|
||||
import { stringify } from '@bitauth/libauth';
|
||||
import { stringify, decodeTransaction, hexToBin } from '@bitauth/libauth';
|
||||
import {
|
||||
CC_EVENTS,
|
||||
CC_METHODS,
|
||||
|
|
@ -41,6 +41,7 @@ import { handleGetAddresses } from './wc-handler';
|
|||
import { buildCcSession, type CcSession } from './cc-handler';
|
||||
import type { WzHandler } from './wz-handler';
|
||||
import type { PendingSignRequest, RelayConnectionState } from '@wizardconnect/wallet';
|
||||
import type { DisconnectReason } from '@wizardconnect/core';
|
||||
import type {
|
||||
AccountIdentity,
|
||||
BchChain,
|
||||
|
|
@ -248,7 +249,11 @@ export function createResponder(config: ResponderConfig): Responder {
|
|||
sessions.set(topic, {
|
||||
topic,
|
||||
protocol: 'wz',
|
||||
chain: 'bch:bchtest', // WZ SignTx doesn't carry a chain — assume current network; wire from config in a follow-up.
|
||||
// hdwalletv1 sign requests carry no chain, so fall back to the one the
|
||||
// wallet is configured for. `useConnect` pins allowedChains to the
|
||||
// active network, so this is the current chain rather than a guess —
|
||||
// a mainnet build previously mislabelled every WZ session as chipnet.
|
||||
chain: allowedChains[0] ?? 'bch:bchtest',
|
||||
peer: {
|
||||
name: conn?.dappName || conn?.label || 'WizardConnect dApp',
|
||||
url: '',
|
||||
|
|
@ -266,6 +271,32 @@ export function createResponder(config: ResponderConfig): Responder {
|
|||
sequence: wzReq.request.sequence,
|
||||
});
|
||||
|
||||
// Unwrap. `SignTransactionRequest.transaction` is NOT the transaction —
|
||||
// it is a `WcSignTransactionRequest` wrapper `{ transaction, sourceOutputs,
|
||||
// broadcast?, userPrompt? }` (@bch-wc2/interfaces, re-exported by
|
||||
// @wizardconnect/core). Passing the wrapper through as `transaction` leaves
|
||||
// `tx.inputs` undefined and `coSignTransaction` rejects every WZ request as
|
||||
// malformed. `inputPaths`/`sequence` sit on the OUTER message, not the wrapper.
|
||||
const wc = wzReq.request.transaction as {
|
||||
transaction: unknown;
|
||||
sourceOutputs?: readonly unknown[];
|
||||
broadcast?: boolean;
|
||||
userPrompt?: string;
|
||||
};
|
||||
|
||||
// The wrapper's `transaction` is `Transaction | string`; dApps built with
|
||||
// cashscript send the decoded object, but hex is equally legal. Downstream
|
||||
// (signer, preview) expects the decoded form, so normalise here.
|
||||
let decoded = wc?.transaction;
|
||||
if (typeof decoded === 'string') {
|
||||
const result = decodeTransaction(hexToBin(decoded));
|
||||
if (typeof result === 'string') {
|
||||
void wzHandler.reject(wzReq.connectionId, wzReq.request.sequence, `malformed transaction: ${result}`);
|
||||
return;
|
||||
}
|
||||
decoded = result;
|
||||
}
|
||||
|
||||
// Surface as a SignRequestPreview to the UI, same shape as WC/CC.
|
||||
const preview: SignRequestPreview = {
|
||||
kind: 'sign-tx',
|
||||
|
|
@ -273,8 +304,10 @@ export function createResponder(config: ResponderConfig): Responder {
|
|||
sessionId: topic,
|
||||
appName: session.peer.name,
|
||||
request: {
|
||||
transaction: wzReq.request.transaction,
|
||||
sourceOutputs: (wzReq.request.transaction as { sourceOutputs?: readonly unknown[] })?.sourceOutputs ?? [],
|
||||
transaction: decoded,
|
||||
sourceOutputs: wc?.sourceOutputs ?? [],
|
||||
broadcast: wc?.broadcast,
|
||||
userPrompt: wc?.userPrompt,
|
||||
inputPaths: wzReq.request.inputPaths,
|
||||
} as BchSignTxRequest,
|
||||
};
|
||||
|
|
@ -292,6 +325,27 @@ export function createResponder(config: ResponderConfig): Responder {
|
|||
}
|
||||
});
|
||||
|
||||
// The dApp cancelled a pending sign_transaction_request before we responded.
|
||||
// Evict our pending entry so a late approval is dropped, and tell the UI to
|
||||
// clear its approval screen if it is showing this request.
|
||||
wzHandler.on('signCancelled', (connectionId: string, sequence: number, reason: string | undefined) => {
|
||||
for (const [requestId, entry] of wzPending.entries()) {
|
||||
if (entry.connectionId === connectionId && entry.sequence === sequence) {
|
||||
wzPending.delete(requestId);
|
||||
events.onSignRequestCancel(requestId, reason);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Remote peer closed the connection. `connectionsChanged` also fires and
|
||||
// handles the session eviction; here we just surface a readable reason so
|
||||
// the shell can flash a toast instead of a silent disappearance.
|
||||
wzHandler.on('remoteDisconnect', (connectionId: string, reason: DisconnectReason, message: string | undefined) => {
|
||||
const label = message ? `${reason}: ${message}` : reason;
|
||||
events.onError(new Error(`WizardConnect dApp closed the connection (${connectionId.slice(0, 8)}…): ${label}`));
|
||||
});
|
||||
|
||||
wzHandler.on('error', (err: Error) => events.onError(toError(err)));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -180,15 +180,20 @@ export function useConnect(
|
|||
|
||||
const allowedChains: readonly BchChain[] = [networkToChain(currentNetwork)];
|
||||
|
||||
// WizardConnect handler — instantiate iff a KeyStore with HD support is
|
||||
// available. Failure to build it is non-fatal (WC/CC still work); we
|
||||
// record the error and continue without the WZ transport.
|
||||
// WizardConnect handler — instantiate iff the active KeyStore has an
|
||||
// HD tree available. `supportedPathNames()` is static (from the
|
||||
// interface) so it includes receive/change/defi even for WIF-restored
|
||||
// wallets that cannot export xpubs — that array alone is not proof of
|
||||
// HD availability. `buildXpubCache` inside `createWzHandler` throws
|
||||
// KeyStoreError('not-supported') for WIF-restored wallets; translate
|
||||
// that specific case into an honest silent skip (WC/CC still work),
|
||||
// and only surface *other* failures as visible errors.
|
||||
let wzHandler: WzHandler | undefined;
|
||||
keyStoreRef.current = keyStore;
|
||||
if (keyStore) {
|
||||
try {
|
||||
const paths = keyStore.supportedPathNames();
|
||||
if (paths.includes('receive') && paths.includes('change') && paths.includes('defi')) {
|
||||
const paths = keyStore.supportedPathNames();
|
||||
if (paths.includes('receive') && paths.includes('change') && paths.includes('defi')) {
|
||||
try {
|
||||
wzHandler = await createWzHandler({
|
||||
keyStore,
|
||||
walletName: DEVIANT_METADATA.name,
|
||||
|
|
@ -209,11 +214,21 @@ export function useConnect(
|
|||
});
|
||||
if (cancelled) return;
|
||||
wzHandlerRef.current = wzHandler;
|
||||
} catch (e) {
|
||||
const err = e as { code?: string };
|
||||
if (err?.code === 'not-supported') {
|
||||
// Valid wallet configuration (WIF-restored), not an error. Log
|
||||
// to the console for support diagnostics but keep the UI clean —
|
||||
// showing a red banner here would misrepresent the situation as
|
||||
// a broken build. WalletConnect and CashConnect remain available.
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(
|
||||
'[connect] WizardConnect unavailable: this wallet was imported from a WIF and has no HD tree. WalletConnect and CashConnect remain available.',
|
||||
);
|
||||
} else {
|
||||
setError(e as Error);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Log to the error surface so it's visible, but don't fail the whole
|
||||
// responder init — WC/CC still work.
|
||||
setError(e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -224,6 +239,17 @@ export function useConnect(
|
|||
events: {
|
||||
onSessionProposal: (p) => { if (!cancelled) setProposal(p); },
|
||||
onSignRequest: (req) => { if (!cancelled) setSignRequest(req); },
|
||||
onSignRequestCancel: (requestId, reason) => {
|
||||
if (cancelled) return;
|
||||
// Clear the approval screen if it is showing the cancelled request
|
||||
// — approving a request the dApp has already withdrawn would ship a
|
||||
// signature to a peer that no longer expects it.
|
||||
const current = signRequestRef.current;
|
||||
if (current && current.id === requestId) {
|
||||
setSignRequest(null);
|
||||
if (reason) setError(new Error(`Sign request cancelled by dApp: ${reason}`));
|
||||
}
|
||||
},
|
||||
onSessionDelete: (topic) => {
|
||||
if (cancelled) return;
|
||||
setSessions((prev) => prev.filter((s) => s.topic !== topic));
|
||||
|
|
|
|||
|
|
@ -21,13 +21,30 @@
|
|||
* per `hdwalletv1.js:PATH_DEFI = 'defi'`). We translate at the enum ↔ string
|
||||
* boundary.
|
||||
*
|
||||
* ⚠ LICENSE STATUS (2026-07-04): @wizardconnect/{core,wallet} publish
|
||||
* with no LICENSE file, no `license` field in package.json, and no LICENSE
|
||||
* at their gitlab. Community reports treat it as "open, no license" — which
|
||||
* is technically copyright-restricted by default. Community usage precedent
|
||||
* (Cauldron, Paytaca, Selene ship with it) makes chipnet-beta consumption
|
||||
* defensible. Before mainnet ship, get an explicit permission from Riften
|
||||
* Labs (Dagur / H4lvor). Tracked in `Parameters/WalletConnect/WizardConnect_reference.md`.
|
||||
* LICENSE STATUS — RESOLVED (2026-07-27). The note here used to read "no license,
|
||||
* copyright-restricted by default, mainnet ship needs explicit permission." That
|
||||
* was wrong; the mainnet block can be lifted.
|
||||
*
|
||||
* @wizardconnect/{core,wallet,dapp} are **LGPL-3.0-or-later**. The npm packages
|
||||
* omit the `license` field and exclude LICENSE.txt from their published `files`,
|
||||
* which is the only reason `npm view` renders "Proprietary". The governing licence
|
||||
* is the repo-root LICENSE.txt at gitlab.com/riftenlabs/lib/wizardconnect —
|
||||
* verbatim LGPL-3.0 with no additional terms — the repo README says
|
||||
* "LGPL-3.0-or-later … Free for commercial and non-commercial use", and Riften
|
||||
* Labs' own launch announcement lists "Open source (LGPL)".
|
||||
*
|
||||
* What LGPL §4 obliges us to do, since we link the library and do not modify it
|
||||
* (our own code keeps its licence — copyleft does NOT propagate to it):
|
||||
* §4(a) state in-app that WizardConnect is used and is LGPL-covered;
|
||||
* §4(b)(c) ship copies of LGPL-3.0 + GPL-3.0 with the app;
|
||||
* §4(d) let the user substitute their own build. Easiest is §4(d)(1) — keep
|
||||
* @wizardconnect/* as its own chunk instead of inlining it into one
|
||||
* minified bundle. Verify the Vite build does not fold it in before
|
||||
* mainnet ship.
|
||||
*
|
||||
* Full analysis: SilentMode `Decentralized.DNS/WIZARDCONNECT-LICENSE-FINDING.md`.
|
||||
* Note `Parameters/WalletConnect/WizardConnect_reference.md` §0/§3.3 still carries
|
||||
* the old "Proprietary" claim and should be corrected.
|
||||
*/
|
||||
import {
|
||||
DerivationPath,
|
||||
|
|
|
|||
|
|
@ -19,10 +19,11 @@
|
|||
* union + a URI-to-transport router. Both are structurally similar to how
|
||||
* CashConnect was wired via `cc-handler.ts` and `useConnect.ts`.
|
||||
*
|
||||
* ⚠ LICENSE STATUS: see wz-adapter.ts header. @wizardconnect/{core,wallet} are
|
||||
* proprietary-per-npm-view but publish no LICENSE file. Community usage
|
||||
* precedent (Cauldron/Paytaca/Selene) supports chipnet-beta consumption;
|
||||
* mainnet ship needs explicit Riften Labs permission.
|
||||
* LICENSE STATUS — RESOLVED (2026-07-27): @wizardconnect/* is LGPL-3.0-or-later,
|
||||
* free for commercial use. "Proprietary" in `npm view` is an artefact of a missing
|
||||
* `license` field, not a licence term. Mainnet is no longer blocked; the remaining
|
||||
* obligations are attribution + shipping the licence texts + keeping the library a
|
||||
* separate chunk (LGPL §4a/4b/4d1). See the wz-adapter.ts header for the detail.
|
||||
*/
|
||||
import {
|
||||
WalletConnectionManager,
|
||||
|
|
@ -30,6 +31,7 @@ import {
|
|||
type PendingSignRequest,
|
||||
type SignTransactionResult,
|
||||
} from '@wizardconnect/wallet';
|
||||
import type { DisconnectReason } from '@wizardconnect/core';
|
||||
import type { SignTransactionRequest } from '@wizardconnect/core';
|
||||
import type { KeyStore } from '@core/wallet/keystore.js';
|
||||
import {
|
||||
|
|
@ -57,6 +59,13 @@ export interface WzHandlerOpts {
|
|||
export interface WzHandlerEventMap {
|
||||
pendingSignRequest: (req: PendingSignRequest) => void;
|
||||
connectionsChanged: (connections: readonly RelayConnectionState[]) => void;
|
||||
/** The dApp cancelled a pending sign request. The responder must evict the
|
||||
* matching pending entry so an approval taped through afterwards is dropped
|
||||
* rather than shipped to a dApp that no longer expects it. */
|
||||
signCancelled: (connectionId: string, sequence: number, reason: string | undefined) => void;
|
||||
/** The dApp closed its side of the connection. `connectionsChanged` also
|
||||
* fires; this event carries the reason for logging/user messaging. */
|
||||
remoteDisconnect: (connectionId: string, reason: DisconnectReason, message: string | undefined) => void;
|
||||
error: (error: Error) => void;
|
||||
}
|
||||
|
||||
|
|
@ -98,13 +107,16 @@ export async function createWzHandler(opts: WzHandlerOpts): Promise<WzHandler> {
|
|||
|
||||
const manager = new WalletConnectionManager(adapter);
|
||||
|
||||
// Simple multi-listener event bus. Only the three event kinds our interface
|
||||
// exposes are supported — the underlying manager has more (remoteDisconnect,
|
||||
// signCancelled, message, connectionStatusChanged) but we don't surface them
|
||||
// at v1; add them here when a caller needs one.
|
||||
// Multi-listener event bus. `connectionStatusChanged` and raw `message` from
|
||||
// the underlying manager are still not surfaced — add them if a UI need
|
||||
// arises. `signCancelled` and `remoteDisconnect` were added 2026-07-27
|
||||
// because their absence let the UI leave stale approvals on screen after the
|
||||
// dApp cancelled or closed the connection.
|
||||
const listeners: { [K in keyof WzHandlerEventMap]: Set<WzHandlerEventMap[K]> } = {
|
||||
pendingSignRequest: new Set(),
|
||||
connectionsChanged: new Set(),
|
||||
signCancelled: new Set(),
|
||||
remoteDisconnect: new Set(),
|
||||
error: new Set(),
|
||||
};
|
||||
|
||||
|
|
@ -131,6 +143,14 @@ export async function createWzHandler(opts: WzHandlerOpts): Promise<WzHandler> {
|
|||
emit('connectionsChanged', Object.values(manager.getConnections()));
|
||||
});
|
||||
|
||||
manager.on('signCancelled', (connectionId: string, sequence: number, reason: string | undefined) => {
|
||||
emit('signCancelled', connectionId, sequence, reason);
|
||||
});
|
||||
|
||||
manager.on('remoteDisconnect', (connectionId: string, reason: DisconnectReason, message: string | undefined) => {
|
||||
emit('remoteDisconnect', connectionId, reason, message);
|
||||
});
|
||||
|
||||
return {
|
||||
pair(uri: string): string {
|
||||
return manager.connect(uri);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { Network } from '../core/wallet';
|
|||
import { QrScanner } from '../qr/QrScanner';
|
||||
import { parseQrUri } from '../qr/qr-uri';
|
||||
import type { ConnectHandle } from '../connect/useConnect';
|
||||
import { computePreflight, shortenCashAddr, type Preflight, type CashAddrPrefix } from '../connect/preflight';
|
||||
|
||||
// Apps — dApp connectivity (Connected | Discover), a wallet-global network
|
||||
// switcher, and dual-protocol connect (WalletConnect + CashConnect, auto-
|
||||
|
|
@ -53,6 +54,17 @@ interface SigningRequest {
|
|||
summary: string;
|
||||
detail: string;
|
||||
sats?: number;
|
||||
/** Verbatim `userPrompt` the dApp attached to the request; the protocol
|
||||
* expects wallets to render it as-is. `null` for sign-message or when the
|
||||
* dApp didn't set one. */
|
||||
userPrompt?: string | null;
|
||||
/** Real, request-derived pre-flight facts. `null` for sign-message or when
|
||||
* the transaction couldn't be decoded — SigningPreview hides the block. */
|
||||
preflight?: Preflight | null;
|
||||
}
|
||||
|
||||
function networkToCashAddrPrefix(n: Network): CashAddrPrefix {
|
||||
return n === 'mainnet' ? 'bitcoincash' : 'bchtest';
|
||||
}
|
||||
|
||||
// ── Internal navigation ───────────────────────────────────────────────────
|
||||
|
|
@ -135,6 +147,7 @@ export function AppsScreen({
|
|||
detail: display.length > 200 ? display.slice(0, 200) + '…' : display,
|
||||
};
|
||||
}
|
||||
const preflight = computePreflight(r.request, networkToCashAddrPrefix(network));
|
||||
return {
|
||||
sessionId: r.sessionId,
|
||||
appName: r.appName,
|
||||
|
|
@ -143,8 +156,10 @@ export function AppsScreen({
|
|||
detail: r.request.broadcast
|
||||
? 'The dApp will broadcast this transaction after you sign.'
|
||||
: 'The dApp will assemble + broadcast after collecting your signature.',
|
||||
userPrompt: r.request.userPrompt ?? null,
|
||||
preflight,
|
||||
};
|
||||
}, [connect?.state.signRequest]);
|
||||
}, [connect?.state.signRequest, network]);
|
||||
|
||||
// When a proposal arrives, make sure we're on the app-preview view (covers
|
||||
// the case where the user navigated away while we were waiting).
|
||||
|
|
@ -678,8 +693,9 @@ function SigningPreview({
|
|||
onApprove: () => void;
|
||||
onReject: () => void;
|
||||
}) {
|
||||
const satsStr = req.sats ? req.sats.toLocaleString() + ' sat' : '';
|
||||
const satsStr = req.sats ? req.sats.toLocaleString() + ' bits' : '';
|
||||
const bchStr = req.sats ? (req.sats / 1e8).toFixed(5) + ' BCH' : '';
|
||||
const pf = req.preflight ?? null;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -697,7 +713,8 @@ function SigningPreview({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
{/* Summary — verbatim `userPrompt` from the dApp when present, per the
|
||||
protocol requirement that wallets render the dApp's own wording. */}
|
||||
<div className="value-region" style={{ marginBottom: 16 }}>
|
||||
<div className="pnv-label">Requested action</div>
|
||||
<div className="pnv" style={{ fontSize: 22 }}>{req.summary}</div>
|
||||
|
|
@ -705,13 +722,61 @@ function SigningPreview({
|
|||
{satsStr && <div className="pnv-raw">{satsStr}</div>}
|
||||
</div>
|
||||
|
||||
{/* Pre-flight */}
|
||||
<div className="sec-label">Pre-flight checks</div>
|
||||
<div className="card" style={{ marginBottom: 14 }}>
|
||||
<div className="brow"><span className="k">Outputs</span><span className="v" style={{ color: 'var(--acid)' }}>✓ no token UTXOs spent</span></div>
|
||||
<div className="brow"><span className="k">Fee</span><span className="v" style={{ color: 'var(--acid)' }}>✓ within normal range</span></div>
|
||||
<div className="brow" style={{ borderBottom: 0 }}><span className="k">Recipient</span><span className="v">bchtest:qp2…7lm</span></div>
|
||||
</div>
|
||||
{/* Pre-flight — computed from the request. Hidden entirely if the
|
||||
transaction couldn't be decoded (rather than showing a lie). */}
|
||||
{pf && (
|
||||
<>
|
||||
<div className="sec-label">Pre-flight</div>
|
||||
<div className="card" style={{ marginBottom: 14 }}>
|
||||
{pf.outputs.slice(0, 4).map((o, i, arr) => (
|
||||
<div
|
||||
key={i}
|
||||
className="brow"
|
||||
style={i === arr.length - 1 && pf.feeBits === null && !pf.anyTokenInput ? { borderBottom: 0 } : undefined}
|
||||
>
|
||||
<span className="k">
|
||||
{arr.length === 1 ? 'Recipient' : `Output ${i}`}
|
||||
{o.hasToken && ' · token'}
|
||||
</span>
|
||||
<span className="v" title={o.recipient}>
|
||||
{shortenCashAddr(o.recipient)}
|
||||
{o.bits > 0 && (
|
||||
<span style={{ marginLeft: 8, color: 'var(--muted)' }}>
|
||||
{o.bits.toLocaleString()} bits
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{pf.outputs.length > 4 && (
|
||||
<div className="brow">
|
||||
<span className="k">…{pf.outputs.length - 4} more output{pf.outputs.length - 4 === 1 ? '' : 's'}</span>
|
||||
<span className="v" />
|
||||
</div>
|
||||
)}
|
||||
{pf.feeBits !== null && (
|
||||
<div className="brow" style={!pf.anyTokenInput && !pf.anyTokenOutput ? { borderBottom: 0 } : undefined}>
|
||||
<span className="k">Fee</span>
|
||||
<span className="v">
|
||||
{pf.feeBits < 0
|
||||
? <span style={{ color: 'var(--warn, #e6a03a)' }}>outputs exceed inputs by {(-pf.feeBits).toLocaleString()} bits</span>
|
||||
: `${pf.feeBits.toLocaleString()} bits`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{(pf.anyTokenInput || pf.anyTokenOutput) && (
|
||||
<div className="brow" style={{ borderBottom: 0 }}>
|
||||
<span className="k">Tokens</span>
|
||||
<span className="v">
|
||||
{pf.anyTokenInput && 'spends token UTXOs'}
|
||||
{pf.anyTokenInput && pf.anyTokenOutput && ' · '}
|
||||
{pf.anyTokenOutput && 'creates token outputs'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<p className="lead-note">{req.detail}</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ function makeFakeWzHandler(): WzHandler & {
|
|||
const listeners: { [K in keyof WzHandlerEventMap]: Set<WzHandlerEventMap[K]> } = {
|
||||
pendingSignRequest: new Set(),
|
||||
connectionsChanged: new Set(),
|
||||
signCancelled: new Set(),
|
||||
remoteDisconnect: new Set(),
|
||||
error: new Set(),
|
||||
};
|
||||
const calls = {
|
||||
|
|
@ -132,6 +134,7 @@ function makeEvents(): ResponderEvents & {
|
|||
signRequests, sessionDeletes, errors,
|
||||
onSessionProposal() {},
|
||||
onSignRequest(req) { signRequests.push(req); },
|
||||
onSignRequestCancel() {},
|
||||
onSessionDelete(topic) { sessionDeletes.push(topic); },
|
||||
onError(e) { errors.push(e); },
|
||||
};
|
||||
|
|
@ -246,6 +249,80 @@ describe('multi-protocol responder — WZ pendingSignRequest flow', () => {
|
|||
expect(preview.id).toBeGreaterThan(1_000_000);
|
||||
});
|
||||
|
||||
// ── Regression: the wrapper must be unwrapped ────────────────────────────
|
||||
//
|
||||
// `SignTransactionRequest.transaction` is NOT a transaction. It is a
|
||||
// `WcSignTransactionRequest` wrapper — `{ transaction, sourceOutputs,
|
||||
// broadcast?, userPrompt? }`. Passing the wrapper straight through as
|
||||
// `request.transaction` leaves `tx.inputs` undefined, and coSignTransaction
|
||||
// rejects EVERY WizardConnect request with "malformed request — missing
|
||||
// tx.inputs or sourceOutputs". The earlier tests missed it because they only
|
||||
// asserted on sessionId/id/kind and never looked inside `request`.
|
||||
const wrapped = (over: Record<string, unknown> = {}): PendingSignRequest => ({
|
||||
connectionId: 'wz-conn-1',
|
||||
request: {
|
||||
action: 'sign_transaction_request' as never,
|
||||
time: Date.now(),
|
||||
sequence: 7,
|
||||
inputPaths: [[0, 'receive', 3]],
|
||||
transaction: {
|
||||
transaction: { version: 2, locktime: 0, inputs: [{ outpointIndex: 0 }], outputs: [{ valueSatoshis: 1000n }] },
|
||||
sourceOutputs: [{ lockingBytecode: new Uint8Array([0x76]), valueSatoshis: 5000n }],
|
||||
broadcast: true,
|
||||
userPrompt: 'Register alice.p2p',
|
||||
...over,
|
||||
} as never,
|
||||
},
|
||||
});
|
||||
|
||||
it('unwraps WcSignTransactionRequest so the signer receives a real transaction', () => {
|
||||
wzHandler.emit('pendingSignRequest', wrapped());
|
||||
const preview = events.signRequests[0] as { request: Record<string, any> };
|
||||
const tx = preview.request.transaction as Record<string, unknown>;
|
||||
|
||||
// The inner transaction, not the wrapper.
|
||||
expect(Array.isArray(tx.inputs)).toBe(true);
|
||||
expect(tx).not.toHaveProperty('sourceOutputs');
|
||||
expect(tx.version).toBe(2);
|
||||
|
||||
// The precise precondition coSignTransaction checks first (signer.ts:135).
|
||||
expect(Array.isArray(preview.request.sourceOutputs)).toBe(true);
|
||||
expect((tx.inputs as unknown[]).length).toBe(preview.request.sourceOutputs.length);
|
||||
});
|
||||
|
||||
it('forwards broadcast, userPrompt and inputPaths instead of dropping them', () => {
|
||||
wzHandler.emit('pendingSignRequest', wrapped());
|
||||
const preview = events.signRequests[0] as { request: Record<string, any> };
|
||||
// userPrompt is what the approval screen must render verbatim — dropping it
|
||||
// left every WizardConnect prompt reading a generic "Approve transaction".
|
||||
expect(preview.request.userPrompt).toBe('Register alice.p2p');
|
||||
expect(preview.request.broadcast).toBe(true);
|
||||
expect(preview.request.inputPaths).toEqual([[0, 'receive', 3]]);
|
||||
});
|
||||
|
||||
it('accepts the hex-string transaction variant, which the type also permits', () => {
|
||||
// WcSignTransactionRequest.transaction is `Transaction | string`. dApps built
|
||||
// with cashscript send the decoded object, but hex is equally legal and the
|
||||
// signer cannot decode it itself.
|
||||
const hex =
|
||||
'0200000001' + '00'.repeat(32) + '00000000' + '00' + 'ffffffff' +
|
||||
'01' + 'e803000000000000' + '00' + '00000000';
|
||||
wzHandler.emit('pendingSignRequest', wrapped({ transaction: hex }));
|
||||
const preview = events.signRequests[0] as { request: Record<string, any> };
|
||||
const tx = preview.request.transaction as Record<string, unknown>;
|
||||
expect(typeof tx).toBe('object');
|
||||
expect(Array.isArray(tx.inputs)).toBe(true);
|
||||
expect((tx.inputs as unknown[]).length).toBe(1);
|
||||
});
|
||||
|
||||
it('rejects an undecodable transaction rather than surfacing it for approval', () => {
|
||||
wzHandler.emit('pendingSignRequest', wrapped({ transaction: 'not-hex-at-all' }));
|
||||
expect(events.signRequests).toHaveLength(0);
|
||||
expect(wzHandler.calls.reject.length).toBe(1);
|
||||
expect(wzHandler.calls.reject[0][0]).toBe('wz-conn-1');
|
||||
expect(wzHandler.calls.reject[0][1]).toBe(7);
|
||||
});
|
||||
|
||||
it('approveSignRequest for a WZ request routes to wzHandler.approve with the correct (connId, sequence)', async () => {
|
||||
const wzReq: PendingSignRequest = {
|
||||
connectionId: 'wz-conn-1',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue