@qrlwallet/connect 3.2.0 → 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 +14 -3
- package/dist/index.d.mts +128 -2
- package/dist/index.d.ts +128 -2
- package/dist/index.js +280 -7
- package/dist/index.mjs +280 -7
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -554,6 +554,7 @@ var KeyExchange = class extends EventEmitter {
|
|
|
554
554
|
import { io } from "socket.io-client";
|
|
555
555
|
import EventEmitter2 from "eventemitter3";
|
|
556
556
|
var PENDING_JOIN_TIMEOUT_MS = 2e4;
|
|
557
|
+
var SEND_FLUSH_TIMEOUT_MS = 600;
|
|
557
558
|
var SocketClient = class extends EventEmitter2 {
|
|
558
559
|
constructor(relayUrl, clientType) {
|
|
559
560
|
super();
|
|
@@ -744,6 +745,48 @@ var SocketClient = class extends EventEmitter2 {
|
|
|
744
745
|
);
|
|
745
746
|
});
|
|
746
747
|
}
|
|
748
|
+
/**
|
|
749
|
+
* Emit an event and resolve once the relay acks it, or after a bounded
|
|
750
|
+
* flush window. Lets a caller await transmission before tearing the
|
|
751
|
+
* socket down.
|
|
752
|
+
*/
|
|
753
|
+
flushEmit(event, payload) {
|
|
754
|
+
return new Promise((resolve) => {
|
|
755
|
+
if (!this.socket?.connected) {
|
|
756
|
+
resolve();
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
let settled = false;
|
|
760
|
+
const done = () => {
|
|
761
|
+
if (settled) return;
|
|
762
|
+
settled = true;
|
|
763
|
+
resolve();
|
|
764
|
+
};
|
|
765
|
+
const timer = setTimeout(done, SEND_FLUSH_TIMEOUT_MS);
|
|
766
|
+
this.socket.emit(event, payload, () => {
|
|
767
|
+
clearTimeout(timer);
|
|
768
|
+
done();
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Explicitly terminate the channel on the relay (durable tombstone), as
|
|
774
|
+
* opposed to the transient leaveChannel(). Used when the session is dead
|
|
775
|
+
* in a way an encrypted TERMINATE cannot communicate (AEAD desync: the
|
|
776
|
+
* peer could not open it). Resolves once the close is flushed or the
|
|
777
|
+
* bounded window elapses, so the caller can safely disconnect afterwards.
|
|
778
|
+
*/
|
|
779
|
+
closeChannel() {
|
|
780
|
+
const channelId = this.channelId;
|
|
781
|
+
this.channelId = null;
|
|
782
|
+
if (this.pendingJoin) {
|
|
783
|
+
clearTimeout(this.pendingJoin.watchdog);
|
|
784
|
+
this.pendingJoin.reject(new Error("Channel closed before join completed"));
|
|
785
|
+
this.pendingJoin = null;
|
|
786
|
+
}
|
|
787
|
+
if (!this.socket?.connected || !channelId) return Promise.resolve();
|
|
788
|
+
return this.flushEmit("close_channel", { channelId });
|
|
789
|
+
}
|
|
747
790
|
/**
|
|
748
791
|
* Leave the current channel.
|
|
749
792
|
*/
|
|
@@ -1044,11 +1087,13 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1044
1087
|
this.connectedAccounts = [];
|
|
1045
1088
|
this.unresponsiveTimer = null;
|
|
1046
1089
|
this.reconnectProbeTimer = null;
|
|
1090
|
+
this.restoreInFlight = null;
|
|
1047
1091
|
this.failedReconnects = 0;
|
|
1048
1092
|
this.walletPresent = false;
|
|
1049
1093
|
this.pendingRestore = null;
|
|
1050
1094
|
this.messageQueue = Promise.resolve();
|
|
1051
1095
|
this.outboundQueue = Promise.resolve();
|
|
1096
|
+
this.consecutiveDecryptFailures = 0;
|
|
1052
1097
|
this.dappMetadata = options.dappMetadata;
|
|
1053
1098
|
this.relayUrl = options.relayUrl ?? DEFAULT_RELAY_URL;
|
|
1054
1099
|
this.chainId = options.chainId ?? "0x0";
|
|
@@ -1167,6 +1212,7 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1167
1212
|
this.clearReconnectProbe();
|
|
1168
1213
|
this.pendingRestore = null;
|
|
1169
1214
|
this.walletPresent = false;
|
|
1215
|
+
this.consecutiveDecryptFailures = 0;
|
|
1170
1216
|
if (this.keyExchange) {
|
|
1171
1217
|
this.keyExchange.reset();
|
|
1172
1218
|
} else {
|
|
@@ -1205,9 +1251,18 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1205
1251
|
* Returns false if there is nothing to restore.
|
|
1206
1252
|
*/
|
|
1207
1253
|
async reconnect() {
|
|
1254
|
+
if (this.restoreInFlight) return this.restoreInFlight;
|
|
1255
|
+
const task = this.reconnectNow().finally(() => {
|
|
1256
|
+
this.restoreInFlight = null;
|
|
1257
|
+
});
|
|
1258
|
+
this.restoreInFlight = task;
|
|
1259
|
+
return task;
|
|
1260
|
+
}
|
|
1261
|
+
async reconnectNow() {
|
|
1208
1262
|
if (!this.pendingRestore) return false;
|
|
1209
1263
|
this.setStatus("reconnecting" /* RECONNECTING */);
|
|
1210
1264
|
this.walletPresent = false;
|
|
1265
|
+
this.consecutiveDecryptFailures = 0;
|
|
1211
1266
|
try {
|
|
1212
1267
|
const session = await KeyExchange.sessionFromPersisted(this.pendingRestore.keyExchange);
|
|
1213
1268
|
this.keyExchange = new KeyExchange(true, session);
|
|
@@ -1283,15 +1338,53 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1283
1338
|
}
|
|
1284
1339
|
}
|
|
1285
1340
|
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Make sure the socket is open and joined to the paired session's channel
|
|
1343
|
+
* so an outbound request can be routed - or relay-buffered if the wallet is
|
|
1344
|
+
* currently absent (its socket dies within seconds of the app
|
|
1345
|
+
* backgrounding; the relay holds channel traffic for it). Revives the
|
|
1346
|
+
* socket after a probe-timeout teardown and hydrates a cold stored
|
|
1347
|
+
* session. Returns false when there is no session to revive (never paired,
|
|
1348
|
+
* explicit disconnect, or the channel was tombstoned).
|
|
1349
|
+
*/
|
|
1350
|
+
async ensureChannelJoined() {
|
|
1351
|
+
if (this.keyExchange?.areKeysExchanged()) {
|
|
1352
|
+
if (this.socketClient.isConnected() && this.socketClient.getChannelId()) {
|
|
1353
|
+
return true;
|
|
1354
|
+
}
|
|
1355
|
+
this.setStatus("reconnecting" /* RECONNECTING */);
|
|
1356
|
+
this.walletPresent = false;
|
|
1357
|
+
return this.joinAndSettle();
|
|
1358
|
+
}
|
|
1359
|
+
if (this.pendingRestore) {
|
|
1360
|
+
return this.reconnect();
|
|
1361
|
+
}
|
|
1362
|
+
return false;
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* True while a pairing exists (handshake complete, session keys in
|
|
1366
|
+
* memory), even when the wallet's socket is momentarily out of the
|
|
1367
|
+
* channel. Survives the reconnect-probe teardown; false after an explicit
|
|
1368
|
+
* disconnect or a relay tombstone.
|
|
1369
|
+
*/
|
|
1370
|
+
isPaired() {
|
|
1371
|
+
return this.keyExchange?.areKeysExchanged() ?? false;
|
|
1372
|
+
}
|
|
1373
|
+
/** True while the relay roster last showed the wallet in the channel. */
|
|
1374
|
+
isWalletPresent() {
|
|
1375
|
+
return this.walletPresent;
|
|
1376
|
+
}
|
|
1286
1377
|
/**
|
|
1287
1378
|
* A session was terminated for good (wallet sent a relay 'close', or the
|
|
1288
1379
|
* join ack reported a tombstone). Clear local state and surface
|
|
1289
1380
|
* DISCONNECTED so the consumer drops to a fresh-pairing UI.
|
|
1290
1381
|
*/
|
|
1291
1382
|
handleSessionTerminated() {
|
|
1383
|
+
this.emit("session_terminated");
|
|
1292
1384
|
this.clearReconnectProbe();
|
|
1293
1385
|
this.clearUnresponsiveTimer();
|
|
1294
1386
|
this.walletPresent = false;
|
|
1387
|
+
this.consecutiveDecryptFailures = 0;
|
|
1295
1388
|
this.pendingRestore = null;
|
|
1296
1389
|
this.keyExchange = null;
|
|
1297
1390
|
this.socketClient.leaveChannel();
|
|
@@ -1320,22 +1413,28 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1320
1413
|
}
|
|
1321
1414
|
}
|
|
1322
1415
|
/**
|
|
1323
|
-
* Send a JSON-RPC request to the wallet
|
|
1416
|
+
* Send a JSON-RPC request to the wallet. Returns the outbound send promise
|
|
1417
|
+
* (settled on the relay's ack) so callers that must sequence on delivery -
|
|
1418
|
+
* e.g. the provider's wallet-wake redirect, which cannot navigate away
|
|
1419
|
+
* before the ciphertext reaches the relay - can await it. Callers may also
|
|
1420
|
+
* ignore it: failures are logged here either way.
|
|
1324
1421
|
*/
|
|
1325
1422
|
sendJsonRpc(request) {
|
|
1326
1423
|
if (!this.keyExchange?.areKeysExchanged()) {
|
|
1327
1424
|
throw new Error("Not connected: key exchange not complete");
|
|
1328
1425
|
}
|
|
1329
|
-
this.sendEncrypted({
|
|
1426
|
+
const sent = this.sendEncrypted({
|
|
1330
1427
|
type: "jsonrpc" /* JSONRPC */,
|
|
1331
1428
|
jsonrpc: "2.0",
|
|
1332
1429
|
id: request.id,
|
|
1333
1430
|
method: request.method,
|
|
1334
1431
|
params: request.params
|
|
1335
|
-
})
|
|
1432
|
+
});
|
|
1433
|
+
sent.catch((err) => {
|
|
1336
1434
|
error("ConnectionManager", "Failed to send JSON-RPC:", err);
|
|
1337
1435
|
});
|
|
1338
1436
|
this.startUnresponsiveTimer();
|
|
1437
|
+
return sent;
|
|
1339
1438
|
}
|
|
1340
1439
|
getStatus() {
|
|
1341
1440
|
return this.status;
|
|
@@ -1449,8 +1548,19 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1449
1548
|
}
|
|
1450
1549
|
}
|
|
1451
1550
|
if (typeof message === "string" && this.keyExchange?.areKeysExchanged()) {
|
|
1551
|
+
let decrypted;
|
|
1552
|
+
try {
|
|
1553
|
+
decrypted = await this.keyExchange.decryptMessage(message);
|
|
1554
|
+
} catch (err) {
|
|
1555
|
+
error("ConnectionManager", "Failed to decrypt message:", err);
|
|
1556
|
+
this.consecutiveDecryptFailures++;
|
|
1557
|
+
if (this.consecutiveDecryptFailures >= _ConnectionManager.MAX_DECRYPT_FAILURES) {
|
|
1558
|
+
await this.teardownDesyncedSession();
|
|
1559
|
+
}
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
this.consecutiveDecryptFailures = 0;
|
|
1452
1563
|
try {
|
|
1453
|
-
const decrypted = await this.keyExchange.decryptMessage(message);
|
|
1454
1564
|
await this.persistSession();
|
|
1455
1565
|
const parsed = JSON.parse(decrypted);
|
|
1456
1566
|
if (isRecord(parsed)) {
|
|
@@ -1459,10 +1569,24 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1459
1569
|
warn("ConnectionManager", "Dropping non-object decrypted payload");
|
|
1460
1570
|
}
|
|
1461
1571
|
} catch (err) {
|
|
1462
|
-
error("ConnectionManager", "Failed to
|
|
1572
|
+
error("ConnectionManager", "Failed to handle decrypted message:", err);
|
|
1463
1573
|
}
|
|
1464
1574
|
}
|
|
1465
1575
|
}
|
|
1576
|
+
/**
|
|
1577
|
+
* The receive stream is cryptographically unrecoverable: the peer's send
|
|
1578
|
+
* counter is ahead of our recv counter for good (relay buffer TTL/cap
|
|
1579
|
+
* dropped a ciphertext), so every future message would fail its tag.
|
|
1580
|
+
* An encrypted TERMINATE cannot communicate this (the desynced peer could
|
|
1581
|
+
* not open it either); tombstone the channel on the relay instead so the
|
|
1582
|
+
* wallet learns the pairing is dead even if it only re-joins later, then
|
|
1583
|
+
* clear local state and surface the standard terminated teardown.
|
|
1584
|
+
*/
|
|
1585
|
+
async teardownDesyncedSession() {
|
|
1586
|
+
warn("ConnectionManager", "AEAD stream desynced beyond recovery; terminating session");
|
|
1587
|
+
await this.socketClient.closeChannel();
|
|
1588
|
+
this.handleSessionTerminated();
|
|
1589
|
+
}
|
|
1466
1590
|
async handleSynAck(msg) {
|
|
1467
1591
|
if (!this.keyExchange) return;
|
|
1468
1592
|
this.setStatus("key_exchange" /* KEY_EXCHANGE */);
|
|
@@ -1516,6 +1640,7 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1516
1640
|
}
|
|
1517
1641
|
case "terminate" /* TERMINATE */: {
|
|
1518
1642
|
log("ConnectionManager", "Received terminate from wallet");
|
|
1643
|
+
this.emit("session_terminated");
|
|
1519
1644
|
void this.disconnect();
|
|
1520
1645
|
break;
|
|
1521
1646
|
}
|
|
@@ -1635,6 +1760,12 @@ var _ConnectionManager = class _ConnectionManager extends EventEmitter3 {
|
|
|
1635
1760
|
}
|
|
1636
1761
|
};
|
|
1637
1762
|
_ConnectionManager.MAX_RECONNECT_FAILURES = 5;
|
|
1763
|
+
// Nonces derive from the recv counter and there is no gap tolerance, so a
|
|
1764
|
+
// genuinely desynced stream (peer advanced past us, e.g. the relay's
|
|
1765
|
+
// buffer TTL dropped a ciphertext) fails EVERY subsequent open. Two in a
|
|
1766
|
+
// row cannot happen on a healthy stream; requiring the second guards
|
|
1767
|
+
// against a one-off injected junk ciphertext killing a live session.
|
|
1768
|
+
_ConnectionManager.MAX_DECRYPT_FAILURES = 2;
|
|
1638
1769
|
var ConnectionManager = _ConnectionManager;
|
|
1639
1770
|
|
|
1640
1771
|
// src/utils/platform.ts
|
|
@@ -1692,6 +1823,16 @@ var QRL_CONNECT_PROVIDER_INFO = {
|
|
|
1692
1823
|
};
|
|
1693
1824
|
var EIP6963_ANNOUNCE_EVENT = "eip6963:announceProvider";
|
|
1694
1825
|
var EIP6963_REQUEST_EVENT = "eip6963:requestProvider";
|
|
1826
|
+
function isRecordObj(v) {
|
|
1827
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1828
|
+
}
|
|
1829
|
+
function parseInflightRecord(v) {
|
|
1830
|
+
if (!isRecordObj(v)) return null;
|
|
1831
|
+
const { id, method, ts } = v;
|
|
1832
|
+
if (typeof id !== "string" && typeof id !== "number") return null;
|
|
1833
|
+
if (typeof method !== "string" || typeof ts !== "number") return null;
|
|
1834
|
+
return { id, method, ts };
|
|
1835
|
+
}
|
|
1695
1836
|
var QRLConnectProvider = class extends EventEmitter4 {
|
|
1696
1837
|
constructor(options) {
|
|
1697
1838
|
super();
|
|
@@ -1700,6 +1841,7 @@ var QRLConnectProvider = class extends EventEmitter4 {
|
|
|
1700
1841
|
this.eip6963RequestListener = null;
|
|
1701
1842
|
this.resumeListener = null;
|
|
1702
1843
|
this.resumeDebounce = null;
|
|
1844
|
+
this.orphanedRequests = /* @__PURE__ */ new Map();
|
|
1703
1845
|
// Random per-instance prefix keeps request ids unique across page loads.
|
|
1704
1846
|
// A bare counter restarts at 1 on reload, and the relay buffers messages
|
|
1705
1847
|
// for 5 minutes, so a stale buffered response could otherwise be matched
|
|
@@ -1708,6 +1850,10 @@ var QRLConnectProvider = class extends EventEmitter4 {
|
|
|
1708
1850
|
this.requestCounter = 0;
|
|
1709
1851
|
this.isQRLConnect = true;
|
|
1710
1852
|
this.options = options;
|
|
1853
|
+
this.inflightKey = `${options.storageKey ?? `${STORAGE_KEY_PREFIX}:session`}:inflight`;
|
|
1854
|
+
for (const rec of this.readInflight()) {
|
|
1855
|
+
this.orphanedRequests.set(rec.id, { method: rec.method });
|
|
1856
|
+
}
|
|
1711
1857
|
if (options.debug) {
|
|
1712
1858
|
setDebug(true);
|
|
1713
1859
|
}
|
|
@@ -1820,6 +1966,16 @@ var QRLConnectProvider = class extends EventEmitter4 {
|
|
|
1820
1966
|
this.connectionManager.on("jsonrpc_response", (response) => {
|
|
1821
1967
|
const pending = this.pendingRequests.get(response.id);
|
|
1822
1968
|
if (!pending) {
|
|
1969
|
+
const orphan = this.orphanedRequests.get(response.id);
|
|
1970
|
+
if (orphan) {
|
|
1971
|
+
this.settleInflight(response.id);
|
|
1972
|
+
this.emit("late_response", {
|
|
1973
|
+
id: response.id,
|
|
1974
|
+
method: orphan.method,
|
|
1975
|
+
...response.error ? { error: response.error } : { result: response.result }
|
|
1976
|
+
});
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1823
1979
|
warn("Provider", `No pending request for id ${response.id}`);
|
|
1824
1980
|
return;
|
|
1825
1981
|
}
|
|
@@ -1848,6 +2004,13 @@ var QRLConnectProvider = class extends EventEmitter4 {
|
|
|
1848
2004
|
}
|
|
1849
2005
|
this.pendingRequests.clear();
|
|
1850
2006
|
});
|
|
2007
|
+
this.connectionManager.on("session_terminated", () => {
|
|
2008
|
+
for (const [, pending] of this.pendingRequests) {
|
|
2009
|
+
pending.reject(new Error("Session terminated by wallet"));
|
|
2010
|
+
}
|
|
2011
|
+
this.pendingRequests.clear();
|
|
2012
|
+
this.clearAllInflight();
|
|
2013
|
+
});
|
|
1851
2014
|
}
|
|
1852
2015
|
/**
|
|
1853
2016
|
* Generate a connection URI for QR code display or deep link redirect.
|
|
@@ -1884,7 +2047,13 @@ var QRLConnectProvider = class extends EventEmitter4 {
|
|
|
1884
2047
|
throw new Error(`Unsupported method: ${method}`);
|
|
1885
2048
|
}
|
|
1886
2049
|
if (this.connectionManager.getStatus() !== "connected" /* CONNECTED */) {
|
|
1887
|
-
|
|
2050
|
+
if (method === "qrl_requestAccounts" && this.connectionManager.isPaired() && this.connectionManager.getAccounts().length > 0) {
|
|
2051
|
+
return this.connectionManager.getAccounts();
|
|
2052
|
+
}
|
|
2053
|
+
const joined = await this.connectionManager.ensureChannelJoined();
|
|
2054
|
+
if (!joined) {
|
|
2055
|
+
throw new Error("Not connected to QRL Wallet");
|
|
2056
|
+
}
|
|
1888
2057
|
}
|
|
1889
2058
|
const id = `${this.requestIdPrefix}-${++this.requestCounter}`;
|
|
1890
2059
|
return new Promise((resolve, reject) => {
|
|
@@ -1907,20 +2076,109 @@ var QRLConnectProvider = class extends EventEmitter4 {
|
|
|
1907
2076
|
const originalReject = pending.reject;
|
|
1908
2077
|
pending.resolve = (result) => {
|
|
1909
2078
|
clearTimeout(timeout);
|
|
2079
|
+
this.settleInflight(id);
|
|
1910
2080
|
originalResolve(result);
|
|
1911
2081
|
};
|
|
1912
2082
|
pending.reject = (error2) => {
|
|
1913
2083
|
clearTimeout(timeout);
|
|
2084
|
+
this.settleInflight(id);
|
|
1914
2085
|
originalReject(error2);
|
|
1915
2086
|
};
|
|
1916
|
-
|
|
2087
|
+
if (RESTRICTED_METHODS.has(method)) {
|
|
2088
|
+
this.persistInflight(id, method);
|
|
2089
|
+
}
|
|
2090
|
+
const sent = this.connectionManager.sendJsonRpc({
|
|
1917
2091
|
jsonrpc: "2.0",
|
|
1918
2092
|
id,
|
|
1919
2093
|
method,
|
|
1920
2094
|
params
|
|
1921
2095
|
});
|
|
2096
|
+
sent.catch((err) => {
|
|
2097
|
+
const failed = this.pendingRequests.get(id);
|
|
2098
|
+
if (!failed) return;
|
|
2099
|
+
this.pendingRequests.delete(id);
|
|
2100
|
+
failed.reject(err instanceof Error ? err : new Error(String(err)));
|
|
2101
|
+
});
|
|
2102
|
+
if (this.shouldRedirectToWallet(method)) {
|
|
2103
|
+
void sent.then(
|
|
2104
|
+
() => attemptWalletRedirect(this.walletWakeUri()),
|
|
2105
|
+
() => void 0
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
1922
2108
|
});
|
|
1923
2109
|
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Redirect only for calls the user must approve, on mobile, and only when
|
|
2112
|
+
* the wallet is actually absent from the channel: right after pairing its
|
|
2113
|
+
* socket lingers and a pointless app flash would be jarring.
|
|
2114
|
+
*
|
|
2115
|
+
* qrl_requestAccounts is excluded even though it is restricted: dApps call
|
|
2116
|
+
* it on page load to restore a session, so it can run with no user gesture
|
|
2117
|
+
* (an unsolicited "Open in MyQRLWallet?" sheet on load) and pairing UIs
|
|
2118
|
+
* already own their own deep-link step.
|
|
2119
|
+
*/
|
|
2120
|
+
shouldRedirectToWallet(method) {
|
|
2121
|
+
return this.options.walletRedirectOnRequest !== false && RESTRICTED_METHODS.has(method) && method !== "qrl_requestAccounts" && isMobileBrowser() && !this.connectionManager.isWalletPresent();
|
|
2122
|
+
}
|
|
2123
|
+
// ── In-flight persistence (survive the return-redirect reload) ──────
|
|
2124
|
+
readInflight() {
|
|
2125
|
+
if (typeof localStorage === "undefined") return [];
|
|
2126
|
+
try {
|
|
2127
|
+
const raw = localStorage.getItem(this.inflightKey);
|
|
2128
|
+
if (!raw) return [];
|
|
2129
|
+
const parsed = JSON.parse(raw);
|
|
2130
|
+
if (!Array.isArray(parsed)) return [];
|
|
2131
|
+
const now = Date.now();
|
|
2132
|
+
const records = [];
|
|
2133
|
+
for (const v of parsed) {
|
|
2134
|
+
const rec = parseInflightRecord(v);
|
|
2135
|
+
if (rec && now - rec.ts <= REQUEST_TIMEOUT_MS) records.push(rec);
|
|
2136
|
+
}
|
|
2137
|
+
return records;
|
|
2138
|
+
} catch {
|
|
2139
|
+
return [];
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
writeInflight(records) {
|
|
2143
|
+
if (typeof localStorage === "undefined") return;
|
|
2144
|
+
try {
|
|
2145
|
+
if (records.length === 0) {
|
|
2146
|
+
localStorage.removeItem(this.inflightKey);
|
|
2147
|
+
} else {
|
|
2148
|
+
localStorage.setItem(this.inflightKey, JSON.stringify(records));
|
|
2149
|
+
}
|
|
2150
|
+
} catch {
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
persistInflight(id, method) {
|
|
2154
|
+
const records = this.readInflight().filter((r) => r.id !== id);
|
|
2155
|
+
records.push({ id, method, ts: Date.now() });
|
|
2156
|
+
this.writeInflight(records);
|
|
2157
|
+
}
|
|
2158
|
+
settleInflight(id) {
|
|
2159
|
+
this.orphanedRequests.delete(id);
|
|
2160
|
+
this.writeInflight(this.readInflight().filter((r) => r.id !== id));
|
|
2161
|
+
}
|
|
2162
|
+
clearAllInflight() {
|
|
2163
|
+
this.orphanedRequests.clear();
|
|
2164
|
+
this.writeInflight([]);
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Wake URI for the wallet app. Carries no pairing payload: the app
|
|
2168
|
+
* foregrounds on any `qrlconnect:` URL and re-joins its sessions (draining
|
|
2169
|
+
* the buffered request); the cid lets a future wallet jump straight to the
|
|
2170
|
+
* right approval.
|
|
2171
|
+
*
|
|
2172
|
+
* MUST stay query-only (no host/path). The mobile app routes deep links
|
|
2173
|
+
* with expo-router, which maps anything after the scheme to an in-app
|
|
2174
|
+
* route: `qrlconnect://resume?...` lands on the "+not-found" screen and
|
|
2175
|
+
* unmounts the WebView, so the wake achieves nothing. The hostless form
|
|
2176
|
+
* routes to the WebView tab exactly like the pairing URIs
|
|
2177
|
+
* (`qrlconnect://?q=...`), which is the whole point of the wake.
|
|
2178
|
+
*/
|
|
2179
|
+
walletWakeUri() {
|
|
2180
|
+
return `qrlconnect://?wake=${encodeURIComponent(this.connectionManager.getChannelId())}`;
|
|
2181
|
+
}
|
|
1924
2182
|
/**
|
|
1925
2183
|
* Get the current connection status.
|
|
1926
2184
|
*/
|
|
@@ -1951,6 +2209,19 @@ var QRLConnectProvider = class extends EventEmitter4 {
|
|
|
1951
2209
|
hasStoredSession() {
|
|
1952
2210
|
return this.connectionManager.hasStoredSession();
|
|
1953
2211
|
}
|
|
2212
|
+
/**
|
|
2213
|
+
* True while a pairing exists (keys exchanged), even when the wallet app
|
|
2214
|
+
* is backgrounded and its socket is out of the channel. Combine with
|
|
2215
|
+
* getStatus() to tell "wallet away, session intact" (paired + WAITING)
|
|
2216
|
+
* apart from "waiting for a first scan" in pairing UIs.
|
|
2217
|
+
*/
|
|
2218
|
+
isPaired() {
|
|
2219
|
+
return this.connectionManager.isPaired();
|
|
2220
|
+
}
|
|
2221
|
+
/** True while the wallet is actually present in the relay channel. */
|
|
2222
|
+
isWalletPresent() {
|
|
2223
|
+
return this.connectionManager.isWalletPresent();
|
|
2224
|
+
}
|
|
1954
2225
|
/**
|
|
1955
2226
|
* Reset the connection and start a fresh pairing with a new channel.
|
|
1956
2227
|
* Use this when the user wants to create a new connection instead of
|
|
@@ -1961,6 +2232,7 @@ var QRLConnectProvider = class extends EventEmitter4 {
|
|
|
1961
2232
|
pending.reject(new Error("Connection reset"));
|
|
1962
2233
|
}
|
|
1963
2234
|
this.pendingRequests.clear();
|
|
2235
|
+
this.clearAllInflight();
|
|
1964
2236
|
await this.connectionManager.resetForNewChannel();
|
|
1965
2237
|
return this.getConnectionURI();
|
|
1966
2238
|
}
|
|
@@ -1975,6 +2247,7 @@ var QRLConnectProvider = class extends EventEmitter4 {
|
|
|
1975
2247
|
pending.reject(new Error("Disconnected"));
|
|
1976
2248
|
}
|
|
1977
2249
|
this.pendingRequests.clear();
|
|
2250
|
+
this.clearAllInflight();
|
|
1978
2251
|
this.teardownResumeListeners();
|
|
1979
2252
|
await this.connectionManager.disconnect();
|
|
1980
2253
|
}
|