@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 +26 -6
- package/dist/index.d.mts +147 -3
- package/dist/index.d.ts +147 -3
- package/dist/index.js +317 -7
- package/dist/index.mjs +316 -7
- package/package.json +1 -1
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:
|
|
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
|
-
|
|
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` |
|
|
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` |
|
|
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)
|
|
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
|
|
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
|
|
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 };
|