Builds on the previous commit, which correctly identified the race and the shape
of the answer. Three things needed to change before it could ship, and the reason
each matters is easy to miss.
DEFER THE TEARDOWN, NOT THE REGISTRY
The previous commit moved everything into clean(), including
this.connections.delete() and the connectionsChanged emit. That leaves the
connection in the map until the publish settles, so after disconnect() returns:
- getConnections() still lists a connection the user just disconnected;
- connect() matches by URI at the top of the function, so reconnecting to the
same URI hands back the dying connection — which clean() then deletes,
leaving the caller holding an id for something already gone.
Registry removal is now synchronous and only conn.cleanup() — the transport
teardown, the part that was killing the in-flight publish — is deferred.
BOUND THE WAIT
There was no timeout: if relay() never settles, clean() never runs and the
interval, the socket and the map entry leak permanently. That is not a corner
case. relay() enqueues rather than publishes when the client is not ready, so
disconnecting while the relay is unreachable — precisely when a user reaches for
disconnect — can hang forever. DISCONNECT_PUBLISH_TIMEOUT_MS bounds it: an
undelivered courtesy message is a smaller problem than a socket that never
closes.
RESTORE THE TYPES
doDisconnect had lost its parameter annotations and its `private` modifier, and
the message literal its DisconnectMessage type. Under "strict": true that is
TS7006 on both parameters — the package does not compile — and dropping `private`
widened the public API by accident. Formatting is back to the repo's prettier
config.
TESTS
None of the above is visible from the wallet's side: its own state is correct
either way and only the peer notices, which is how the original bug shipped.
disconnect.test.ts only ever covered dapp -> wallet.
disconnect-delivery.test.ts covers wallet -> dapp over a live relay. Verified by
reverting the fix: the two delivery tests time out after 20s, and the two
invariant tests — immediate registry removal, and reconnecting on the same URI —
pass either way, which is precisely why they are there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- `conn.cleanup()` calls `client.disconnect()` which closes the WebSocket
transport.
- In the old code, cleanup ran synchronously right after `conn.client.relay(disconnectMsg)`, so the transport was torn down before the async relay message could be transmitted. The error was silently swallowed by `.catch(() => {})` — the disconnect message was never actually sent to the dapp.
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).
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.
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.
This backward compatible change allows wallets/dapps to add additional
features outside the basic transaction signature support to the
hdwalletv1 protocol.
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.
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.
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.
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.
This solves an issue where wallet has to scan address ranges for each
derivation path and match it to locking script to figure out what
private key to use for signature.
This is a waste of effort and unnecessary complex for wallet
implementations since the dapp side already knows what inputs its using.