@qrlwallet/connect 3.1.1 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,10 +18,12 @@ All communication is end-to-end encrypted with ML-KEM-768 (FIPS 203) key encapsu
18
18
  npm install @qrlwallet/connect
19
19
  ```
20
20
 
21
+ Want a ready-made pairing dialog (QR code, deep link, copy-code fallback) instead of building your own? Add the framework-free [`@qrlwallet/connect-ui`](ui/) web component.
22
+
21
23
  ## Quick start
22
24
 
23
25
  ```typescript
24
- import { QRLConnect } from '@qrlwallet/connect';
26
+ import { QRLConnect, attemptWalletRedirect, getAppStoreUrl } from '@qrlwallet/connect';
25
27
 
26
28
  const qrl = new QRLConnect({
27
29
  dappMetadata: {
@@ -34,9 +36,16 @@ const qrl = new QRLConnect({
34
36
  const uri = await qrl.getConnectionURI();
35
37
 
36
38
  // Desktop: render as QR code (use any QR library)
37
- // Mobile: redirect to open the wallet app
39
+ // Mobile: try to open the wallet app, with a fallback for when it is
40
+ // not installed (a bare `window.location.href = uri` fails at the
41
+ // unknown protocol: silently on Android, blocking alert on iOS)
38
42
  if (qrl.isMobile()) {
39
- window.location.href = uri;
43
+ const opened = await attemptWalletRedirect(uri);
44
+ if (!opened) {
45
+ // Wallet app not installed (or the user dismissed the chooser):
46
+ // show your QR / copy-code pairing UI plus an install link.
47
+ console.log('Get MyQRLWallet:', getAppStoreUrl());
48
+ }
40
49
  } else {
41
50
  // render uri as QR code
42
51
  }
@@ -82,6 +91,8 @@ const qrl = new QRLConnect({
82
91
  relayUrl: 'https://qrlwallet.com', // default relay
83
92
  chainId: '0x0', // QRL chain ID
84
93
  autoReconnect: true, // reconnect on page load (default: true)
94
+ walletRedirectOnRequest: true, // mobile: deep-link the wallet app awake
95
+ // for approval requests (default: true)
85
96
  debug: false, // console logging
86
97
  });
87
98
  ```
@@ -99,6 +110,8 @@ The main class. Creates a connection manager and EIP-1193 provider.
99
110
  | `isMobile()` | Check if the user is on a mobile browser |
100
111
  | `getAppStoreUrl()` | Get the app store link for QRL Wallet |
101
112
  | `isConnected()` | Whether the wallet is connected |
113
+ | `isPaired()` | Whether a pairing exists, even while the wallet app is backgrounded |
114
+ | `isWalletPresent()` | Whether the wallet is currently live in the relay channel |
102
115
  | `getAccounts()` | Get connected accounts |
103
116
  | `getStatus()` | Current connection status |
104
117
  | `hasStoredSession()` | Check if a reconnectable session exists in local storage |
@@ -115,7 +128,7 @@ The main class. Creates a connection manager and EIP-1193 provider.
115
128
  | `accountsChanged` | `string[]` | Account list changed |
116
129
  | `chainChanged` | `string` | Chain switched |
117
130
  | `statusChanged` | `ConnectionStatus` | Intermediate lifecycle states (see values below) |
118
- | `connection_lost` | — | Emitted after 5 failed reconnection attempts |
131
+ | `connection_lost` | (none) | Emitted after 5 failed reconnection attempts |
119
132
 
120
133
  #### `ConnectionStatus` values
121
134
 
@@ -123,7 +136,7 @@ The main class. Creates a connection manager and EIP-1193 provider.
123
136
  |-------|---------|
124
137
  | `disconnected` | No active connection |
125
138
  | `connecting` | Connecting to relay server |
126
- | `waiting` | QR displayed, waiting for wallet to scan |
139
+ | `waiting` | Waiting for the wallet: a first scan, or (when `isPaired()`) a backgrounded wallet app to return |
127
140
  | `key_exchange` | Post-quantum key exchange in progress |
128
141
  | `connected` | Wallet connected, ready for requests |
129
142
  | `reconnecting` | Attempting to restore a previous session |
@@ -142,12 +155,19 @@ The main class. Creates a connection manager and EIP-1193 provider.
142
155
 
143
156
  Sessions persist in `localStorage` for 7 days. When a user returns to your dApp, the SDK can automatically reconnect without requiring a new QR scan.
144
157
 
158
+ A paired session also survives the wallet app being backgrounded or closed. On the same device at most one of the two apps is ever foregrounded, so the wallet's socket being absent is the normal steady state of a mobile flow, not an error:
159
+
160
+ - `request()` still works while the wallet is away: the relay buffers the encrypted request for up to 5 minutes and the wallet drains it when it re-joins.
161
+ - On mobile browsers, an approval-needing request also fires a `qrlconnect://` deep link that brings the wallet app to the foreground (disable with `walletRedirectOnRequest: false`). `qrl_requestAccounts` never redirects: dApps call it on page load without a user gesture.
162
+ - Use `isPaired()` to tell "wallet away, session intact" apart from "never paired" when `getStatus()` is not `connected`.
163
+
145
164
  Recommended lifecycle:
146
165
 
147
166
  - Use `hasStoredSession()` on page load to decide whether to show reconnect state/UI
148
167
  - Keep a single `QRLConnect` instance for the page lifetime
149
168
  - Use `newConnection()` when the user explicitly wants to pair a different wallet
150
169
  - Use `statusChanged` for UI state transitions instead of relying on internals
170
+ - On a `disconnect` event, check `hasStoredSession()`: if a session is still stored the wallet is merely unreachable (revived by any request); only re-pair when it is gone
151
171
 
152
172
  ## How the relay works
153
173
 
@@ -195,7 +215,7 @@ node test-e2e.mjs
195
215
 
196
216
  The `example/` directory contains a Vite test dApp with QR code generation, transaction sending, message signing, and read-only RPC calls.
197
217
 
198
- > **Want to try it without any setup?** The same example is hosted live at [zondscan.com/dapp-example](https://zondscan.com/dapp-example) — it pairs with the production relay and MyQRLWallet mobile app out of the box.
218
+ > **Want to try it without any setup?** The same example is hosted live at [zondscan.com/dapp-example](https://zondscan.com/dapp-example): it pairs with the production relay and MyQRLWallet mobile app out of the box.
199
219
 
200
220
  ```bash
201
221
  # 1. Build the SDK first (the example links to it locally)
package/dist/index.d.mts CHANGED
@@ -182,6 +182,22 @@ declare enum ConnectionStatus {
182
182
  CONNECTED = "connected",
183
183
  RECONNECTING = "reconnecting"
184
184
  }
185
+ /**
186
+ * A response for a request issued by a PREVIOUS page load. Same-device
187
+ * approvals bounce back via a URL open, which reloads the dApp page and
188
+ * destroys the awaiting promise; the SDK persists in-flight restricted
189
+ * request ids and surfaces their eventual responses through this event.
190
+ */
191
+ interface LateResponse {
192
+ id: string | number;
193
+ method: string;
194
+ result?: unknown;
195
+ error?: {
196
+ code: number;
197
+ message: string;
198
+ data?: unknown;
199
+ };
200
+ }
185
201
  /** EIP-1193 provider events */
186
202
  interface ProviderEvents {
187
203
  connect: (info: {
@@ -198,6 +214,7 @@ interface ProviderEvents {
198
214
  data: unknown;
199
215
  }) => void;
200
216
  statusChanged: (status: ConnectionStatus) => void;
217
+ late_response: (payload: LateResponse) => void;
201
218
  }
202
219
  /** JSON-RPC request */
203
220
  interface JsonRpcRequest {
@@ -297,6 +314,16 @@ interface QRLConnectOptions {
297
314
  announceProvider?: boolean;
298
315
  /** Override the EIP-6963 announce metadata (name / icon / rdns). */
299
316
  providerInfo?: EIP6963ProviderInfoOverride;
317
+ /**
318
+ * On mobile browsers, deep-link (`qrlconnect://resume`) to foreground the
319
+ * wallet app when a restricted request is made while the wallet is absent
320
+ * from the relay channel. A backgrounded wallet app loses its socket
321
+ * within seconds; the relay buffers the request, and without the redirect
322
+ * it would sit there until the user remembers to switch apps on their
323
+ * own. Defaults to true. Set false when the dApp runs its own
324
+ * "open wallet" UX.
325
+ */
326
+ walletRedirectOnRequest?: boolean;
300
327
  }
301
328
 
302
329
  /**
@@ -322,6 +349,8 @@ declare class QRLConnectProvider extends EventEmitter<ProviderEvents> {
322
349
  private eip6963RequestListener;
323
350
  private resumeListener;
324
351
  private resumeDebounce;
352
+ private inflightKey;
353
+ private orphanedRequests;
325
354
  private readonly requestIdPrefix;
326
355
  private requestCounter;
327
356
  readonly isQRLConnect = true;
@@ -364,6 +393,36 @@ declare class QRLConnectProvider extends EventEmitter<ProviderEvents> {
364
393
  method: string;
365
394
  params?: unknown[];
366
395
  }): Promise<unknown>;
396
+ /**
397
+ * Redirect only for calls the user must approve, on mobile, and only when
398
+ * the wallet is actually absent from the channel: right after pairing its
399
+ * socket lingers and a pointless app flash would be jarring.
400
+ *
401
+ * qrl_requestAccounts is excluded even though it is restricted: dApps call
402
+ * it on page load to restore a session, so it can run with no user gesture
403
+ * (an unsolicited "Open in MyQRLWallet?" sheet on load) and pairing UIs
404
+ * already own their own deep-link step.
405
+ */
406
+ private shouldRedirectToWallet;
407
+ private readInflight;
408
+ private writeInflight;
409
+ private persistInflight;
410
+ private settleInflight;
411
+ private clearAllInflight;
412
+ /**
413
+ * Wake URI for the wallet app. Carries no pairing payload: the app
414
+ * foregrounds on any `qrlconnect:` URL and re-joins its sessions (draining
415
+ * the buffered request); the cid lets a future wallet jump straight to the
416
+ * right approval.
417
+ *
418
+ * MUST stay query-only (no host/path). The mobile app routes deep links
419
+ * with expo-router, which maps anything after the scheme to an in-app
420
+ * route: `qrlconnect://resume?...` lands on the "+not-found" screen and
421
+ * unmounts the WebView, so the wake achieves nothing. The hostless form
422
+ * routes to the WebView tab exactly like the pairing URIs
423
+ * (`qrlconnect://?q=...`), which is the whole point of the wake.
424
+ */
425
+ private walletWakeUri;
367
426
  /**
368
427
  * Get the current connection status.
369
428
  */
@@ -384,6 +443,15 @@ declare class QRLConnectProvider extends EventEmitter<ProviderEvents> {
384
443
  * Check if a stored session exists that can be reconnected.
385
444
  */
386
445
  hasStoredSession(): boolean;
446
+ /**
447
+ * True while a pairing exists (keys exchanged), even when the wallet app
448
+ * is backgrounded and its socket is out of the channel. Combine with
449
+ * getStatus() to tell "wallet away, session intact" (paired + WAITING)
450
+ * apart from "waiting for a first scan" in pairing UIs.
451
+ */
452
+ isPaired(): boolean;
453
+ /** True while the wallet is actually present in the relay channel. */
454
+ isWalletPresent(): boolean;
387
455
  /**
388
456
  * Reset the connection and start a fresh pairing with a new channel.
389
457
  * Use this when the user wants to create a new connection instead of
@@ -415,6 +483,13 @@ interface ConnectionManagerEvents {
415
483
  chainId: string;
416
484
  }) => void;
417
485
  connection_lost: () => void;
486
+ /**
487
+ * The session is dead for good (wallet TERMINATE, relay 'close', or a
488
+ * tombstone observed on join). Distinct from a transient DISCONNECTED:
489
+ * in-flight requests can never be answered and must be failed promptly
490
+ * instead of running out the full request timeout.
491
+ */
492
+ session_terminated: () => void;
418
493
  error: (error: Error) => void;
419
494
  }
420
495
  declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
@@ -429,12 +504,15 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
429
504
  private storageKey;
430
505
  private unresponsiveTimer;
431
506
  private reconnectProbeTimer;
507
+ private restoreInFlight;
432
508
  private failedReconnects;
433
509
  private walletPresent;
434
510
  private pendingRestore;
435
511
  private messageQueue;
436
512
  private outboundQueue;
513
+ private consecutiveDecryptFailures;
437
514
  private static MAX_RECONNECT_FAILURES;
515
+ private static MAX_DECRYPT_FAILURES;
438
516
  constructor(options: {
439
517
  dappMetadata: DAppMetadata;
440
518
  relayUrl?: string | undefined;
@@ -453,6 +531,7 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
453
531
  * Returns false if there is nothing to restore.
454
532
  */
455
533
  reconnect(): Promise<boolean>;
534
+ private reconnectNow;
456
535
  /**
457
536
  * (Re)open the socket, join the persisted channel, drain buffered relay
458
537
  * messages, and settle status. Shared by the cold-restore reconnect() and
@@ -469,6 +548,25 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
469
548
  * own connect handler re-joins the channel and drains the relay buffer.
470
549
  */
471
550
  resume(): void;
551
+ /**
552
+ * Make sure the socket is open and joined to the paired session's channel
553
+ * so an outbound request can be routed - or relay-buffered if the wallet is
554
+ * currently absent (its socket dies within seconds of the app
555
+ * backgrounding; the relay holds channel traffic for it). Revives the
556
+ * socket after a probe-timeout teardown and hydrates a cold stored
557
+ * session. Returns false when there is no session to revive (never paired,
558
+ * explicit disconnect, or the channel was tombstoned).
559
+ */
560
+ ensureChannelJoined(): Promise<boolean>;
561
+ /**
562
+ * True while a pairing exists (handshake complete, session keys in
563
+ * memory), even when the wallet's socket is momentarily out of the
564
+ * channel. Survives the reconnect-probe teardown; false after an explicit
565
+ * disconnect or a relay tombstone.
566
+ */
567
+ isPaired(): boolean;
568
+ /** True while the relay roster last showed the wallet in the channel. */
569
+ isWalletPresent(): boolean;
472
570
  /**
473
571
  * A session was terminated for good (wallet sent a relay 'close', or the
474
572
  * join ack reported a tombstone). Clear local state and surface
@@ -478,9 +576,13 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
478
576
  private armReconnectProbe;
479
577
  private clearReconnectProbe;
480
578
  /**
481
- * Send a JSON-RPC request to the wallet (async, fire-and-forget).
579
+ * Send a JSON-RPC request to the wallet. Returns the outbound send promise
580
+ * (settled on the relay's ack) so callers that must sequence on delivery -
581
+ * e.g. the provider's wallet-wake redirect, which cannot navigate away
582
+ * before the ciphertext reaches the relay - can await it. Callers may also
583
+ * ignore it: failures are logged here either way.
482
584
  */
483
- sendJsonRpc(request: JsonRpcRequest): void;
585
+ sendJsonRpc(request: JsonRpcRequest): Promise<void>;
484
586
  getStatus(): ConnectionStatus;
485
587
  getAccounts(): string[];
486
588
  getChainId(): string;
@@ -504,6 +606,16 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
504
606
  disconnect(): Promise<void>;
505
607
  private enqueueRelayMessage;
506
608
  private handleRelayMessage;
609
+ /**
610
+ * The receive stream is cryptographically unrecoverable: the peer's send
611
+ * counter is ahead of our recv counter for good (relay buffer TTL/cap
612
+ * dropped a ciphertext), so every future message would fail its tag.
613
+ * An encrypted TERMINATE cannot communicate this (the desynced peer could
614
+ * not open it either); tombstone the channel on the relay instead so the
615
+ * wallet learns the pairing is dead even if it only re-joins later, then
616
+ * clear local state and surface the standard terminated teardown.
617
+ */
618
+ private teardownDesyncedSession;
507
619
  private handleSynAck;
508
620
  private handleDecryptedMessage;
509
621
  private sendPlaintext;
@@ -600,6 +712,20 @@ declare class SocketClient extends EventEmitter<SocketClientEvents> {
600
712
  success: boolean;
601
713
  buffered: boolean;
602
714
  }>;
715
+ /**
716
+ * Emit an event and resolve once the relay acks it, or after a bounded
717
+ * flush window. Lets a caller await transmission before tearing the
718
+ * socket down.
719
+ */
720
+ private flushEmit;
721
+ /**
722
+ * Explicitly terminate the channel on the relay (durable tombstone), as
723
+ * opposed to the transient leaveChannel(). Used when the session is dead
724
+ * in a way an encrypted TERMINATE cannot communicate (AEAD desync: the
725
+ * peer could not open it). Resolves once the close is flushed or the
726
+ * bounded window elapses, so the caller can safely disconnect afterwards.
727
+ */
728
+ closeChannel(): Promise<void>;
603
729
  /**
604
730
  * Leave the current channel.
605
731
  */
@@ -777,6 +903,24 @@ declare function isMobileBrowser(): boolean;
777
903
  * Falls back to app store URL after timeout.
778
904
  */
779
905
  declare function getAppStoreUrl(): string;
906
+ /**
907
+ * Attempt to open a `qrlconnect://` URI in the wallet app and report whether
908
+ * anything handled it.
909
+ *
910
+ * A custom-scheme navigation on a device without the app installed fails
911
+ * silently on Android and with a blocking alert on iOS Safari, leaving the
912
+ * page visible either way. When the app IS installed, the OS backgrounds the
913
+ * browser, firing `visibilitychange`/`pagehide` almost immediately. So:
914
+ * navigate, then resolve `true` as soon as the page hides, or `false` if it
915
+ * is still visible after `timeoutMs`. On `false`, show fallback pairing UI
916
+ * (QR / copy-code) and an install link (`getAppStoreUrl()`).
917
+ *
918
+ * Detection is heuristic by nature: a slow device may hide the page late
919
+ * (harmless, the wallet still opens), and it cannot distinguish "app missing"
920
+ * from "user dismissed the OS chooser". Treat `false` as "show a fallback",
921
+ * never as proof the app is absent.
922
+ */
923
+ declare function attemptWalletRedirect(uri: string, timeoutMs?: number): Promise<boolean>;
780
924
 
781
925
  declare const DEFAULT_RELAY_URL = "https://qrlwallet.com";
782
926
  declare const PROTOCOL_VERSION = 2;
@@ -795,4 +939,4 @@ declare const RESTRICTED_METHODS: Set<string>;
795
939
  /** RPC methods that can be auto-proxied without approval */
796
940
  declare const UNRESTRICTED_METHODS: Set<string>;
797
941
 
798
- export { type AckMessage, BLOB_LEN, CID_LEN, ConnectionManager, ConnectionStatus, type DAppMetadata, type DAppSession, DEFAULT_RELAY_URL, DIGEST_LEN, type Domain, type EIP6963ProviderInfoOverride, FP_LEN, type JsonRpcRequest, type JsonRpcResponse, KeyExchange, KeyExchangeMessageType, type Message, MessageType, PROTOCOL_VERSION, type ParsedURI, type PendingRequest, type PersistedSession, type ProviderEvents, QRLConnectProvider as QRLConnect, type QRLConnectOptions, QRLConnectProvider, QRL_CONNECT_PROVIDER_INFO, type QrlSignMessageParams, type QrlSignTypedDataParams, type QrlSignedResult, type QrlSignedTypedDataResult, type QrlTypedDataPayload, RESTRICTED_METHODS, type RelayMessage, SCHEME_TAG_MSG, SCHEME_TAG_TYPED, SCHEME_VERSION_MSG, SCHEME_VERSION_TYPED, type Session, SocketClient, type StructDef, type SynAckMessage, type TypeMap, type TypedDataPayload, type TypedField, UNRESTRICTED_METHODS, type VerifyMessageParams, type VerifyTypedDataParams, bytesToHex, cidFromString, cidRandom, cidToString, computeFingerprint, computeMessageDigest, computeTypedDataDigest, concatBytes, encodeField, encodeType, fingerprintEquals, generateConnectionURI, getAppStoreUrl, hashStruct, hexToBytes, isMobileBrowser, parseConnectionURI, typeHash, verifyMessage, verifyTypedData };
942
+ export { type AckMessage, BLOB_LEN, CID_LEN, ConnectionManager, ConnectionStatus, type DAppMetadata, type DAppSession, DEFAULT_RELAY_URL, DIGEST_LEN, type Domain, type EIP6963ProviderInfoOverride, FP_LEN, type JsonRpcRequest, type JsonRpcResponse, KeyExchange, KeyExchangeMessageType, type Message, MessageType, PROTOCOL_VERSION, type ParsedURI, type PendingRequest, type PersistedSession, type ProviderEvents, QRLConnectProvider as QRLConnect, type QRLConnectOptions, QRLConnectProvider, QRL_CONNECT_PROVIDER_INFO, type QrlSignMessageParams, type QrlSignTypedDataParams, type QrlSignedResult, type QrlSignedTypedDataResult, type QrlTypedDataPayload, RESTRICTED_METHODS, type RelayMessage, SCHEME_TAG_MSG, SCHEME_TAG_TYPED, SCHEME_VERSION_MSG, SCHEME_VERSION_TYPED, type Session, SocketClient, type StructDef, type SynAckMessage, type TypeMap, type TypedDataPayload, type TypedField, UNRESTRICTED_METHODS, type VerifyMessageParams, type VerifyTypedDataParams, attemptWalletRedirect, bytesToHex, cidFromString, cidRandom, cidToString, computeFingerprint, computeMessageDigest, computeTypedDataDigest, concatBytes, encodeField, encodeType, fingerprintEquals, generateConnectionURI, getAppStoreUrl, hashStruct, hexToBytes, isMobileBrowser, parseConnectionURI, typeHash, verifyMessage, verifyTypedData };
package/dist/index.d.ts CHANGED
@@ -182,6 +182,22 @@ declare enum ConnectionStatus {
182
182
  CONNECTED = "connected",
183
183
  RECONNECTING = "reconnecting"
184
184
  }
185
+ /**
186
+ * A response for a request issued by a PREVIOUS page load. Same-device
187
+ * approvals bounce back via a URL open, which reloads the dApp page and
188
+ * destroys the awaiting promise; the SDK persists in-flight restricted
189
+ * request ids and surfaces their eventual responses through this event.
190
+ */
191
+ interface LateResponse {
192
+ id: string | number;
193
+ method: string;
194
+ result?: unknown;
195
+ error?: {
196
+ code: number;
197
+ message: string;
198
+ data?: unknown;
199
+ };
200
+ }
185
201
  /** EIP-1193 provider events */
186
202
  interface ProviderEvents {
187
203
  connect: (info: {
@@ -198,6 +214,7 @@ interface ProviderEvents {
198
214
  data: unknown;
199
215
  }) => void;
200
216
  statusChanged: (status: ConnectionStatus) => void;
217
+ late_response: (payload: LateResponse) => void;
201
218
  }
202
219
  /** JSON-RPC request */
203
220
  interface JsonRpcRequest {
@@ -297,6 +314,16 @@ interface QRLConnectOptions {
297
314
  announceProvider?: boolean;
298
315
  /** Override the EIP-6963 announce metadata (name / icon / rdns). */
299
316
  providerInfo?: EIP6963ProviderInfoOverride;
317
+ /**
318
+ * On mobile browsers, deep-link (`qrlconnect://resume`) to foreground the
319
+ * wallet app when a restricted request is made while the wallet is absent
320
+ * from the relay channel. A backgrounded wallet app loses its socket
321
+ * within seconds; the relay buffers the request, and without the redirect
322
+ * it would sit there until the user remembers to switch apps on their
323
+ * own. Defaults to true. Set false when the dApp runs its own
324
+ * "open wallet" UX.
325
+ */
326
+ walletRedirectOnRequest?: boolean;
300
327
  }
301
328
 
302
329
  /**
@@ -322,6 +349,8 @@ declare class QRLConnectProvider extends EventEmitter<ProviderEvents> {
322
349
  private eip6963RequestListener;
323
350
  private resumeListener;
324
351
  private resumeDebounce;
352
+ private inflightKey;
353
+ private orphanedRequests;
325
354
  private readonly requestIdPrefix;
326
355
  private requestCounter;
327
356
  readonly isQRLConnect = true;
@@ -364,6 +393,36 @@ declare class QRLConnectProvider extends EventEmitter<ProviderEvents> {
364
393
  method: string;
365
394
  params?: unknown[];
366
395
  }): Promise<unknown>;
396
+ /**
397
+ * Redirect only for calls the user must approve, on mobile, and only when
398
+ * the wallet is actually absent from the channel: right after pairing its
399
+ * socket lingers and a pointless app flash would be jarring.
400
+ *
401
+ * qrl_requestAccounts is excluded even though it is restricted: dApps call
402
+ * it on page load to restore a session, so it can run with no user gesture
403
+ * (an unsolicited "Open in MyQRLWallet?" sheet on load) and pairing UIs
404
+ * already own their own deep-link step.
405
+ */
406
+ private shouldRedirectToWallet;
407
+ private readInflight;
408
+ private writeInflight;
409
+ private persistInflight;
410
+ private settleInflight;
411
+ private clearAllInflight;
412
+ /**
413
+ * Wake URI for the wallet app. Carries no pairing payload: the app
414
+ * foregrounds on any `qrlconnect:` URL and re-joins its sessions (draining
415
+ * the buffered request); the cid lets a future wallet jump straight to the
416
+ * right approval.
417
+ *
418
+ * MUST stay query-only (no host/path). The mobile app routes deep links
419
+ * with expo-router, which maps anything after the scheme to an in-app
420
+ * route: `qrlconnect://resume?...` lands on the "+not-found" screen and
421
+ * unmounts the WebView, so the wake achieves nothing. The hostless form
422
+ * routes to the WebView tab exactly like the pairing URIs
423
+ * (`qrlconnect://?q=...`), which is the whole point of the wake.
424
+ */
425
+ private walletWakeUri;
367
426
  /**
368
427
  * Get the current connection status.
369
428
  */
@@ -384,6 +443,15 @@ declare class QRLConnectProvider extends EventEmitter<ProviderEvents> {
384
443
  * Check if a stored session exists that can be reconnected.
385
444
  */
386
445
  hasStoredSession(): boolean;
446
+ /**
447
+ * True while a pairing exists (keys exchanged), even when the wallet app
448
+ * is backgrounded and its socket is out of the channel. Combine with
449
+ * getStatus() to tell "wallet away, session intact" (paired + WAITING)
450
+ * apart from "waiting for a first scan" in pairing UIs.
451
+ */
452
+ isPaired(): boolean;
453
+ /** True while the wallet is actually present in the relay channel. */
454
+ isWalletPresent(): boolean;
387
455
  /**
388
456
  * Reset the connection and start a fresh pairing with a new channel.
389
457
  * Use this when the user wants to create a new connection instead of
@@ -415,6 +483,13 @@ interface ConnectionManagerEvents {
415
483
  chainId: string;
416
484
  }) => void;
417
485
  connection_lost: () => void;
486
+ /**
487
+ * The session is dead for good (wallet TERMINATE, relay 'close', or a
488
+ * tombstone observed on join). Distinct from a transient DISCONNECTED:
489
+ * in-flight requests can never be answered and must be failed promptly
490
+ * instead of running out the full request timeout.
491
+ */
492
+ session_terminated: () => void;
418
493
  error: (error: Error) => void;
419
494
  }
420
495
  declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
@@ -429,12 +504,15 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
429
504
  private storageKey;
430
505
  private unresponsiveTimer;
431
506
  private reconnectProbeTimer;
507
+ private restoreInFlight;
432
508
  private failedReconnects;
433
509
  private walletPresent;
434
510
  private pendingRestore;
435
511
  private messageQueue;
436
512
  private outboundQueue;
513
+ private consecutiveDecryptFailures;
437
514
  private static MAX_RECONNECT_FAILURES;
515
+ private static MAX_DECRYPT_FAILURES;
438
516
  constructor(options: {
439
517
  dappMetadata: DAppMetadata;
440
518
  relayUrl?: string | undefined;
@@ -453,6 +531,7 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
453
531
  * Returns false if there is nothing to restore.
454
532
  */
455
533
  reconnect(): Promise<boolean>;
534
+ private reconnectNow;
456
535
  /**
457
536
  * (Re)open the socket, join the persisted channel, drain buffered relay
458
537
  * messages, and settle status. Shared by the cold-restore reconnect() and
@@ -469,6 +548,25 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
469
548
  * own connect handler re-joins the channel and drains the relay buffer.
470
549
  */
471
550
  resume(): void;
551
+ /**
552
+ * Make sure the socket is open and joined to the paired session's channel
553
+ * so an outbound request can be routed - or relay-buffered if the wallet is
554
+ * currently absent (its socket dies within seconds of the app
555
+ * backgrounding; the relay holds channel traffic for it). Revives the
556
+ * socket after a probe-timeout teardown and hydrates a cold stored
557
+ * session. Returns false when there is no session to revive (never paired,
558
+ * explicit disconnect, or the channel was tombstoned).
559
+ */
560
+ ensureChannelJoined(): Promise<boolean>;
561
+ /**
562
+ * True while a pairing exists (handshake complete, session keys in
563
+ * memory), even when the wallet's socket is momentarily out of the
564
+ * channel. Survives the reconnect-probe teardown; false after an explicit
565
+ * disconnect or a relay tombstone.
566
+ */
567
+ isPaired(): boolean;
568
+ /** True while the relay roster last showed the wallet in the channel. */
569
+ isWalletPresent(): boolean;
472
570
  /**
473
571
  * A session was terminated for good (wallet sent a relay 'close', or the
474
572
  * join ack reported a tombstone). Clear local state and surface
@@ -478,9 +576,13 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
478
576
  private armReconnectProbe;
479
577
  private clearReconnectProbe;
480
578
  /**
481
- * Send a JSON-RPC request to the wallet (async, fire-and-forget).
579
+ * Send a JSON-RPC request to the wallet. Returns the outbound send promise
580
+ * (settled on the relay's ack) so callers that must sequence on delivery -
581
+ * e.g. the provider's wallet-wake redirect, which cannot navigate away
582
+ * before the ciphertext reaches the relay - can await it. Callers may also
583
+ * ignore it: failures are logged here either way.
482
584
  */
483
- sendJsonRpc(request: JsonRpcRequest): void;
585
+ sendJsonRpc(request: JsonRpcRequest): Promise<void>;
484
586
  getStatus(): ConnectionStatus;
485
587
  getAccounts(): string[];
486
588
  getChainId(): string;
@@ -504,6 +606,16 @@ declare class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
504
606
  disconnect(): Promise<void>;
505
607
  private enqueueRelayMessage;
506
608
  private handleRelayMessage;
609
+ /**
610
+ * The receive stream is cryptographically unrecoverable: the peer's send
611
+ * counter is ahead of our recv counter for good (relay buffer TTL/cap
612
+ * dropped a ciphertext), so every future message would fail its tag.
613
+ * An encrypted TERMINATE cannot communicate this (the desynced peer could
614
+ * not open it either); tombstone the channel on the relay instead so the
615
+ * wallet learns the pairing is dead even if it only re-joins later, then
616
+ * clear local state and surface the standard terminated teardown.
617
+ */
618
+ private teardownDesyncedSession;
507
619
  private handleSynAck;
508
620
  private handleDecryptedMessage;
509
621
  private sendPlaintext;
@@ -600,6 +712,20 @@ declare class SocketClient extends EventEmitter<SocketClientEvents> {
600
712
  success: boolean;
601
713
  buffered: boolean;
602
714
  }>;
715
+ /**
716
+ * Emit an event and resolve once the relay acks it, or after a bounded
717
+ * flush window. Lets a caller await transmission before tearing the
718
+ * socket down.
719
+ */
720
+ private flushEmit;
721
+ /**
722
+ * Explicitly terminate the channel on the relay (durable tombstone), as
723
+ * opposed to the transient leaveChannel(). Used when the session is dead
724
+ * in a way an encrypted TERMINATE cannot communicate (AEAD desync: the
725
+ * peer could not open it). Resolves once the close is flushed or the
726
+ * bounded window elapses, so the caller can safely disconnect afterwards.
727
+ */
728
+ closeChannel(): Promise<void>;
603
729
  /**
604
730
  * Leave the current channel.
605
731
  */
@@ -777,6 +903,24 @@ declare function isMobileBrowser(): boolean;
777
903
  * Falls back to app store URL after timeout.
778
904
  */
779
905
  declare function getAppStoreUrl(): string;
906
+ /**
907
+ * Attempt to open a `qrlconnect://` URI in the wallet app and report whether
908
+ * anything handled it.
909
+ *
910
+ * A custom-scheme navigation on a device without the app installed fails
911
+ * silently on Android and with a blocking alert on iOS Safari, leaving the
912
+ * page visible either way. When the app IS installed, the OS backgrounds the
913
+ * browser, firing `visibilitychange`/`pagehide` almost immediately. So:
914
+ * navigate, then resolve `true` as soon as the page hides, or `false` if it
915
+ * is still visible after `timeoutMs`. On `false`, show fallback pairing UI
916
+ * (QR / copy-code) and an install link (`getAppStoreUrl()`).
917
+ *
918
+ * Detection is heuristic by nature: a slow device may hide the page late
919
+ * (harmless, the wallet still opens), and it cannot distinguish "app missing"
920
+ * from "user dismissed the OS chooser". Treat `false` as "show a fallback",
921
+ * never as proof the app is absent.
922
+ */
923
+ declare function attemptWalletRedirect(uri: string, timeoutMs?: number): Promise<boolean>;
780
924
 
781
925
  declare const DEFAULT_RELAY_URL = "https://qrlwallet.com";
782
926
  declare const PROTOCOL_VERSION = 2;
@@ -795,4 +939,4 @@ declare const RESTRICTED_METHODS: Set<string>;
795
939
  /** RPC methods that can be auto-proxied without approval */
796
940
  declare const UNRESTRICTED_METHODS: Set<string>;
797
941
 
798
- export { type AckMessage, BLOB_LEN, CID_LEN, ConnectionManager, ConnectionStatus, type DAppMetadata, type DAppSession, DEFAULT_RELAY_URL, DIGEST_LEN, type Domain, type EIP6963ProviderInfoOverride, FP_LEN, type JsonRpcRequest, type JsonRpcResponse, KeyExchange, KeyExchangeMessageType, type Message, MessageType, PROTOCOL_VERSION, type ParsedURI, type PendingRequest, type PersistedSession, type ProviderEvents, QRLConnectProvider as QRLConnect, type QRLConnectOptions, QRLConnectProvider, QRL_CONNECT_PROVIDER_INFO, type QrlSignMessageParams, type QrlSignTypedDataParams, type QrlSignedResult, type QrlSignedTypedDataResult, type QrlTypedDataPayload, RESTRICTED_METHODS, type RelayMessage, SCHEME_TAG_MSG, SCHEME_TAG_TYPED, SCHEME_VERSION_MSG, SCHEME_VERSION_TYPED, type Session, SocketClient, type StructDef, type SynAckMessage, type TypeMap, type TypedDataPayload, type TypedField, UNRESTRICTED_METHODS, type VerifyMessageParams, type VerifyTypedDataParams, bytesToHex, cidFromString, cidRandom, cidToString, computeFingerprint, computeMessageDigest, computeTypedDataDigest, concatBytes, encodeField, encodeType, fingerprintEquals, generateConnectionURI, getAppStoreUrl, hashStruct, hexToBytes, isMobileBrowser, parseConnectionURI, typeHash, verifyMessage, verifyTypedData };
942
+ export { type AckMessage, BLOB_LEN, CID_LEN, ConnectionManager, ConnectionStatus, type DAppMetadata, type DAppSession, DEFAULT_RELAY_URL, DIGEST_LEN, type Domain, type EIP6963ProviderInfoOverride, FP_LEN, type JsonRpcRequest, type JsonRpcResponse, KeyExchange, KeyExchangeMessageType, type Message, MessageType, PROTOCOL_VERSION, type ParsedURI, type PendingRequest, type PersistedSession, type ProviderEvents, QRLConnectProvider as QRLConnect, type QRLConnectOptions, QRLConnectProvider, QRL_CONNECT_PROVIDER_INFO, type QrlSignMessageParams, type QrlSignTypedDataParams, type QrlSignedResult, type QrlSignedTypedDataResult, type QrlTypedDataPayload, RESTRICTED_METHODS, type RelayMessage, SCHEME_TAG_MSG, SCHEME_TAG_TYPED, SCHEME_VERSION_MSG, SCHEME_VERSION_TYPED, type Session, SocketClient, type StructDef, type SynAckMessage, type TypeMap, type TypedDataPayload, type TypedField, UNRESTRICTED_METHODS, type VerifyMessageParams, type VerifyTypedDataParams, attemptWalletRedirect, bytesToHex, cidFromString, cidRandom, cidToString, computeFingerprint, computeMessageDigest, computeTypedDataDigest, concatBytes, encodeField, encodeType, fingerprintEquals, generateConnectionURI, getAppStoreUrl, hashStruct, hexToBytes, isMobileBrowser, parseConnectionURI, typeHash, verifyMessage, verifyTypedData };