@lydianpay/lydianconnect 1.2.1 → 1.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/dist/index.cjs CHANGED
@@ -72,6 +72,14 @@ var ConnectManager = class {
72
72
  get(walletId) {
73
73
  return this.connections.get(walletId);
74
74
  }
75
+ /**
76
+ * Fire a wallet's deep link. Goes straight to the fallback: it is the only
77
+ * connector that owns a link table, and the only one that emits the
78
+ * `display_uri` a host is responding to when it calls this.
79
+ */
80
+ openWallet(input) {
81
+ return this.fallback.openWallet?.(input) ?? false;
82
+ }
75
83
  async disconnect(walletId) {
76
84
  if (walletId) {
77
85
  await this.connections.get(walletId)?.disconnect().catch(() => {
@@ -244,10 +252,15 @@ var WALLET_CATALOG = WALLETS.map((w) => ({
244
252
  }));
245
253
 
246
254
  // src/core/connector.ts
255
+ function requestedChain(req) {
256
+ return req.namespace.value.chains?.[0];
257
+ }
247
258
  var BaseConnector = class {
248
259
  constructor() {
249
260
  this.walletIds = [];
250
261
  this.handlers = /* @__PURE__ */ new Set();
262
+ /** Depth of in-flight connects; see {@link duringConnect}. */
263
+ this.connecting = 0;
251
264
  }
252
265
  servesNamespace(_namespaceName) {
253
266
  return true;
@@ -261,7 +274,28 @@ var BaseConnector = class {
261
274
  this.handlers.add(handler);
262
275
  return () => this.handlers.delete(handler);
263
276
  }
277
+ /**
278
+ * Run a connect with its handshake events suppressed.
279
+ *
280
+ * A connector that binds provider listeners before requesting accounts sees the
281
+ * wallet's own setup traffic — an initial `chainChanged`, and another when
282
+ * `ensureChain` switches. Forwarding those makes the host believe the user moved
283
+ * networks mid-connect. Nothing is lost by dropping them: connect() reads the
284
+ * authoritative account and chain when it settles, and only changes made after
285
+ * that are the user's.
286
+ */
287
+ async duringConnect(run) {
288
+ this.connecting++;
289
+ try {
290
+ return await run();
291
+ } finally {
292
+ this.connecting--;
293
+ }
294
+ }
264
295
  emit(event) {
296
+ if (this.connecting > 0 && (event.type === "chainChanged" || event.type === "accountsChanged")) {
297
+ return;
298
+ }
265
299
  for (const handler of this.handlers) handler(event);
266
300
  }
267
301
  };
@@ -271,6 +305,80 @@ function deriveUrl() {
271
305
  return typeof window !== "undefined" ? window.location.origin : "";
272
306
  }
273
307
 
308
+ // src/connectors/eip155/chain.ts
309
+ var CHAIN_SETTLE = { timeoutMs: 4e3, intervalMs: 100 };
310
+ function toCaip(chainId) {
311
+ if (!chainId) return "eip155:1";
312
+ const num = chainId.startsWith("0x") ? parseInt(chainId, 16) : Number(chainId);
313
+ return `eip155:${Number.isFinite(num) ? num : 1}`;
314
+ }
315
+ async function currentChain(rpc2) {
316
+ const raw = await rpc2({ method: "eth_chainId" }).catch(() => null);
317
+ return toCaip(typeof raw === "string" ? raw : null);
318
+ }
319
+ async function settleChain({
320
+ rpc: rpc2,
321
+ chainId,
322
+ timeoutMs,
323
+ intervalMs
324
+ }) {
325
+ const deadline = Date.now() + timeoutMs;
326
+ for (; ; ) {
327
+ const actual = await currentChain(rpc2);
328
+ if (actual === chainId || Date.now() >= deadline) return;
329
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
330
+ }
331
+ }
332
+ async function ensureChain({
333
+ rpc: rpc2,
334
+ chainId,
335
+ timeoutMs = CHAIN_SETTLE.timeoutMs,
336
+ intervalMs = CHAIN_SETTLE.intervalMs
337
+ }) {
338
+ const chainNumber = chainId ? Number(chainId.split(":")[1]) : NaN;
339
+ if (!Number.isFinite(chainNumber) || chainNumber <= 0) return;
340
+ const requested = `eip155:${chainNumber}`;
341
+ if (await currentChain(rpc2) === requested) return;
342
+ const switched = await rpc2({
343
+ method: "wallet_switchEthereumChain",
344
+ params: [{ chainId: `0x${chainNumber.toString(16)}` }]
345
+ }).then(
346
+ () => true,
347
+ // Chain not added to the wallet, or the user declined — the caller's
348
+ // chainChanged will reflect reality. No settle: the chain isn't moving.
349
+ () => false
350
+ );
351
+ if (switched) {
352
+ await settleChain({ rpc: rpc2, chainId: requested, timeoutMs, intervalMs });
353
+ }
354
+ }
355
+
356
+ // src/core/abort.ts
357
+ function withSignal(promise, signal, onAbort) {
358
+ if (!signal) return promise;
359
+ if (signal.aborted) {
360
+ onAbort?.();
361
+ return Promise.reject(new DOMException("Aborted", "AbortError"));
362
+ }
363
+ return new Promise((resolve, reject) => {
364
+ const abortHandler = () => {
365
+ onAbort?.();
366
+ reject(new DOMException("Aborted", "AbortError"));
367
+ };
368
+ signal.addEventListener("abort", abortHandler, { once: true });
369
+ promise.then(
370
+ (value) => {
371
+ signal.removeEventListener("abort", abortHandler);
372
+ resolve(value);
373
+ },
374
+ (error) => {
375
+ signal.removeEventListener("abort", abortHandler);
376
+ reject(error);
377
+ }
378
+ );
379
+ });
380
+ }
381
+
274
382
  // src/connectors/metamask/connector.ts
275
383
  var WALLET_ID = "metamask";
276
384
  var MetaMaskConnector = class extends BaseConnector {
@@ -328,17 +436,23 @@ var MetaMaskConnector = class extends BaseConnector {
328
436
  );
329
437
  }
330
438
  async connect(req) {
331
- const provider = await this.getProvider();
332
- const accounts = await provider.request({
333
- method: "eth_requestAccounts"
439
+ return this.duringConnect(async () => {
440
+ const provider = await this.getProvider();
441
+ const accounts = await withSignal(
442
+ provider.request({ method: "eth_requestAccounts" }),
443
+ req.signal
444
+ );
445
+ const account = accounts?.[0];
446
+ if (!account) throw new Error("MetaMask returned no account");
447
+ await ensureChain({
448
+ rpc: rpcFor(provider),
449
+ chainId: requestedChain(req)
450
+ });
451
+ const chainId = toCaip(
452
+ await provider.request({ method: "eth_chainId" })
453
+ );
454
+ return this.toConnection(provider, account, chainId);
334
455
  });
335
- const account = accounts?.[0];
336
- if (!account) throw new Error("MetaMask returned no account");
337
- await this.ensureChain(provider, req);
338
- const chainId = toCaip(
339
- await provider.request({ method: "eth_chainId" })
340
- );
341
- return this.toConnection(provider, account, chainId);
342
456
  }
343
457
  async restore() {
344
458
  const provider = await this.getProvider().catch(() => null);
@@ -356,22 +470,6 @@ var MetaMaskConnector = class extends BaseConnector {
356
470
  this.provider = null;
357
471
  this.listenersBound = false;
358
472
  }
359
- /** Best-effort switch to the requested chain; leaves the wallet as-is if it's unknown to it. */
360
- async ensureChain(provider, req) {
361
- const wanted = req.namespace.value.chains?.[0];
362
- const wantedNum = wanted ? Number(wanted.split(":")[1]) : NaN;
363
- if (!Number.isFinite(wantedNum) || wantedNum <= 0) return;
364
- const current = parseInt(
365
- await provider.request({ method: "eth_chainId" }).catch(() => "0x0") ?? "0x0",
366
- 16
367
- );
368
- if (current === wantedNum) return;
369
- await provider.request({
370
- method: "wallet_switchEthereumChain",
371
- params: [{ chainId: `0x${wantedNum.toString(16)}` }]
372
- }).catch(() => {
373
- });
374
- }
375
473
  toConnection(provider, account, chainId) {
376
474
  return {
377
475
  walletId: WALLET_ID,
@@ -393,20 +491,18 @@ var MetaMaskConnector = class extends BaseConnector {
393
491
  };
394
492
  }
395
493
  };
396
- function toCaip(hexChainId) {
397
- if (!hexChainId) return "eip155:1";
398
- const num = hexChainId.startsWith("0x") ? parseInt(hexChainId, 16) : Number(hexChainId);
399
- return `eip155:${Number.isFinite(num) ? num : 1}`;
494
+ function rpcFor(provider) {
495
+ return (args) => provider.request(args);
400
496
  }
401
497
 
402
498
  // src/connectors/coinbase/connector.ts
499
+ var WALLET_ID2 = "coinbase";
403
500
  function rpc(provider, method, params) {
404
501
  return provider.request({
405
502
  method,
406
503
  params
407
504
  });
408
505
  }
409
- var WALLET_ID2 = "coinbase";
410
506
  var CoinbaseConnector = class extends BaseConnector {
411
507
  constructor(app) {
412
508
  super();
@@ -443,7 +539,7 @@ var CoinbaseConnector = class extends BaseConnector {
443
539
  this.emit({
444
540
  type: "chainChanged",
445
541
  walletId: WALLET_ID2,
446
- chainId: toCaip2(String(hex))
542
+ chainId: toCaip(String(hex))
447
543
  });
448
544
  });
449
545
  provider.on("accountsChanged", (accounts) => {
@@ -458,13 +554,18 @@ var CoinbaseConnector = class extends BaseConnector {
458
554
  );
459
555
  }
460
556
  async connect(req) {
461
- const provider = await this.getProvider(evmChainIds(req));
462
- const accounts = await rpc(provider, "eth_requestAccounts");
463
- const account = accounts?.[0];
464
- if (!account) throw new Error("Coinbase Wallet returned no account");
465
- await this.ensureChain(provider, req);
466
- const chainId = toCaip2(await rpc(provider, "eth_chainId"));
467
- return this.toConnection(provider, account, chainId);
557
+ return this.duringConnect(async () => {
558
+ const provider = await this.getProvider(evmChainIds(req));
559
+ const accounts = await rpc(provider, "eth_requestAccounts");
560
+ const account = accounts?.[0];
561
+ if (!account) throw new Error("Coinbase Wallet returned no account");
562
+ await ensureChain({
563
+ rpc: rpcFor2(provider),
564
+ chainId: requestedChain(req)
565
+ });
566
+ const chainId = toCaip(await rpc(provider, "eth_chainId"));
567
+ return this.toConnection(provider, account, chainId);
568
+ });
468
569
  }
469
570
  async restore() {
470
571
  const provider = await this.getProvider().catch(() => null);
@@ -474,7 +575,7 @@ var CoinbaseConnector = class extends BaseConnector {
474
575
  );
475
576
  const account = accounts?.[0];
476
577
  if (!account) return [];
477
- const chainId = toCaip2(
578
+ const chainId = toCaip(
478
579
  await rpc(provider, "eth_chainId").catch(() => "0x1")
479
580
  );
480
581
  return [this.toConnection(provider, account, chainId)];
@@ -485,20 +586,6 @@ var CoinbaseConnector = class extends BaseConnector {
485
586
  this.provider = null;
486
587
  this.listenersBound = false;
487
588
  }
488
- async ensureChain(provider, req) {
489
- const wanted = req.namespace.value.chains?.[0];
490
- const wantedNum = wanted ? Number(wanted.split(":")[1]) : NaN;
491
- if (!Number.isFinite(wantedNum) || wantedNum <= 0) return;
492
- const current = parseInt(
493
- await rpc(provider, "eth_chainId").catch(() => "0x0") ?? "0x0",
494
- 16
495
- );
496
- if (current === wantedNum) return;
497
- await rpc(provider, "wallet_switchEthereumChain", [
498
- { chainId: `0x${wantedNum.toString(16)}` }
499
- ]).catch(() => {
500
- });
501
- }
502
589
  toConnection(provider, account, chainId) {
503
590
  return {
504
591
  walletId: WALLET_ID2,
@@ -518,10 +605,8 @@ var CoinbaseConnector = class extends BaseConnector {
518
605
  };
519
606
  }
520
607
  };
521
- function toCaip2(hexChainId) {
522
- if (!hexChainId) return "eip155:1";
523
- const num = hexChainId.startsWith("0x") ? parseInt(hexChainId, 16) : Number(hexChainId);
524
- return `eip155:${Number.isFinite(num) ? num : 1}`;
608
+ function rpcFor2(provider) {
609
+ return ({ method, params }) => rpc(provider, method, params);
525
610
  }
526
611
  function evmChainIds(req) {
527
612
  const wanted = req.namespace.value.chains?.[0];
@@ -585,8 +670,8 @@ var InjectedConnector = class extends BaseConnector {
585
670
  const account = accounts?.[0];
586
671
  if (!account)
587
672
  throw new Error(`${req.walletId} extension returned no account`);
588
- await this.ensureChain(provider, req);
589
- const chainId = toCaip3(
673
+ await ensureChain({ rpc: rpcFor3(provider), chainId: requestedChain(req) });
674
+ const chainId = toCaip(
590
675
  await provider.request({ method: "eth_chainId" })
591
676
  );
592
677
  this.bindEvents(provider, req.walletId);
@@ -605,7 +690,7 @@ var InjectedConnector = class extends BaseConnector {
605
690
  const accounts = await provider.request({ method: "eth_accounts" }).catch(() => []);
606
691
  const account = accounts?.[0];
607
692
  if (!account) continue;
608
- const chainId = toCaip3(
693
+ const chainId = toCaip(
609
694
  await provider.request({ method: "eth_chainId" }).catch(() => "0x1")
610
695
  );
611
696
  this.bindEvents(provider, walletId);
@@ -624,7 +709,7 @@ var InjectedConnector = class extends BaseConnector {
624
709
  this.emit({
625
710
  type: "chainChanged",
626
711
  walletId,
627
- chainId: toCaip3(String(hex))
712
+ chainId: toCaip(String(hex))
628
713
  });
629
714
  });
630
715
  provider.on("accountsChanged", (accounts) => {
@@ -637,22 +722,6 @@ var InjectedConnector = class extends BaseConnector {
637
722
  () => this.emit({ type: "disconnect", walletId })
638
723
  );
639
724
  }
640
- /** Best-effort switch to the requested chain; leaves the wallet as-is if it's unknown to it. */
641
- async ensureChain(provider, req) {
642
- const wanted = req.namespace.value.chains?.[0];
643
- const wantedNum = wanted ? Number(wanted.split(":")[1]) : NaN;
644
- if (!Number.isFinite(wantedNum) || wantedNum <= 0) return;
645
- const current = parseInt(
646
- await provider.request({ method: "eth_chainId" }).catch(() => "0x0") ?? "0x0",
647
- 16
648
- );
649
- if (current === wantedNum) return;
650
- await provider.request({
651
- method: "wallet_switchEthereumChain",
652
- params: [{ chainId: `0x${wantedNum.toString(16)}` }]
653
- }).catch(() => {
654
- });
655
- }
656
725
  toConnection(provider, walletId, account, chainId) {
657
726
  return {
658
727
  walletId,
@@ -676,29 +745,8 @@ var InjectedConnector = class extends BaseConnector {
676
745
  };
677
746
  }
678
747
  };
679
- function toCaip3(hexChainId) {
680
- if (!hexChainId) return "eip155:1";
681
- const num = hexChainId.startsWith("0x") ? parseInt(hexChainId, 16) : Number(hexChainId);
682
- return `eip155:${Number.isFinite(num) ? num : 1}`;
683
- }
684
- function withSignal(promise, signal) {
685
- if (!signal) return promise;
686
- if (signal.aborted)
687
- return Promise.reject(new DOMException("Aborted", "AbortError"));
688
- return new Promise((resolve, reject) => {
689
- const onAbort = () => reject(new DOMException("Aborted", "AbortError"));
690
- signal.addEventListener("abort", onAbort, { once: true });
691
- promise.then(
692
- (value) => {
693
- signal.removeEventListener("abort", onAbort);
694
- resolve(value);
695
- },
696
- (error) => {
697
- signal.removeEventListener("abort", onAbort);
698
- reject(error);
699
- }
700
- );
701
- });
748
+ function rpcFor3(provider) {
749
+ return (args) => provider.request(args);
702
750
  }
703
751
 
704
752
  // src/connectors/walletconnect/wallet-links.ts
@@ -1148,13 +1196,15 @@ function buildLink(base, uri) {
1148
1196
  const sep = base.includes("?") ? "&" : "?";
1149
1197
  return `${base}${sep}uri=${encodeURIComponent(uri)}`;
1150
1198
  }
1151
- function openWallet(link, uri) {
1152
- if (typeof document === "undefined") return;
1153
- if (link.scheme) {
1154
- openHref(buildLink(link.scheme, uri));
1155
- } else if (link.universal) {
1156
- openHref(buildLink(link.universal, uri));
1157
- }
1199
+ function canOpen(link) {
1200
+ return !!(link?.scheme || link?.universal);
1201
+ }
1202
+ function openWallet(link, uri, via = "scheme") {
1203
+ if (typeof document === "undefined") return false;
1204
+ const base = via === "universal" ? link.universal : link.scheme ?? link.universal;
1205
+ if (!base) return false;
1206
+ openHref(buildLink(base, uri));
1207
+ return true;
1158
1208
  }
1159
1209
  function openWalletApp(link) {
1160
1210
  if (typeof document === "undefined") return;
@@ -1193,6 +1243,7 @@ function detectAppOpen(timeoutMs) {
1193
1243
 
1194
1244
  // src/connectors/walletconnect/connector.ts
1195
1245
  var STORAGE_KEY = "lydianconnect.wc.topics";
1246
+ var PING_TIMEOUT_MS = 3e3;
1196
1247
  var WalletConnectConnector = class extends BaseConnector {
1197
1248
  constructor(app, options) {
1198
1249
  super();
@@ -1203,6 +1254,9 @@ var WalletConnectConnector = class extends BaseConnector {
1203
1254
  // claims any wallet not served by an SDK connector
1204
1255
  this.client = null;
1205
1256
  this.topics = /* @__PURE__ */ new Map();
1257
+ /** walletId -> pairing URI of the connect currently awaiting approval, so a
1258
+ * host-driven open button can fire without the host tracking the URI itself. */
1259
+ this.pending = /* @__PURE__ */ new Map();
1206
1260
  this.links = { ...DEFAULT_WALLET_LINKS, ...options.walletLinks ?? {} };
1207
1261
  }
1208
1262
  isAvailable() {
@@ -1246,35 +1300,37 @@ var WalletConnectConnector = class extends BaseConnector {
1246
1300
  }
1247
1301
  async connect(req) {
1248
1302
  const client = await this.getClient();
1249
- const reused = this.tryReuse(client, req);
1303
+ const reused = await this.tryReuse(client, req);
1250
1304
  if (reused) return reused;
1251
1305
  const { uri, approval } = await client.connect({
1252
1306
  requiredNamespaces: { [req.namespace.name]: req.namespace.value }
1253
1307
  });
1254
1308
  if (!uri) throw new Error("WalletConnect did not return a pairing URI");
1255
- this.emit({ type: "display_uri", walletId: req.walletId, uri });
1256
- if (isMobile()) {
1257
- const link = this.links[req.walletId];
1258
- if (link) {
1259
- openWallet(link, uri);
1260
- void detectAppOpen(this.options.openTimeoutMs ?? 2e3).then(
1261
- (opened) => {
1262
- if (!opened)
1263
- this.emit({
1264
- type: "wallet_open_failed",
1265
- walletId: req.walletId,
1266
- uri,
1267
- store: link.store
1268
- });
1269
- }
1270
- );
1271
- }
1309
+ const link = isMobile() ? this.links[req.walletId] : void 0;
1310
+ this.emit({
1311
+ type: "display_uri",
1312
+ walletId: req.walletId,
1313
+ uri,
1314
+ deepLink: canOpen(link)
1315
+ });
1316
+ if (link && canOpen(link)) {
1317
+ this.pending.set(req.walletId, uri);
1318
+ openWallet(link, uri);
1319
+ void detectAppOpen(this.options.openTimeoutMs ?? 2e3).then((opened) => {
1320
+ if (!opened)
1321
+ this.emit({
1322
+ type: "wallet_open_failed",
1323
+ walletId: req.walletId,
1324
+ uri,
1325
+ store: link.store
1326
+ });
1327
+ });
1272
1328
  }
1273
1329
  const pairingTopic = utils.parseUri(uri).topic;
1274
- const session = await withSignal2(approval(), req.signal, () => {
1330
+ const session = await withSignal(approval(), req.signal, () => {
1275
1331
  void client.core.pairing.disconnect({ topic: pairingTopic }).catch(() => {
1276
1332
  });
1277
- });
1333
+ }).finally(() => this.pending.delete(req.walletId));
1278
1334
  const connection = this.toConnection(
1279
1335
  client,
1280
1336
  req.walletId,
@@ -1285,11 +1341,30 @@ var WalletConnectConnector = class extends BaseConnector {
1285
1341
  this.persist();
1286
1342
  return connection;
1287
1343
  }
1344
+ /**
1345
+ * Fire a wallet's deep link on demand. The host calls this from a real click,
1346
+ * which is the one context a mobile browser reliably allows a scheme
1347
+ * navigation from — see the note on {@link openWallet}.
1348
+ */
1349
+ openWallet(input) {
1350
+ const link = this.links[input.walletId];
1351
+ if (!link) return false;
1352
+ const uri = input.uri ?? this.pending.get(input.walletId);
1353
+ if (!uri) {
1354
+ openWalletApp(link);
1355
+ return !!link.native;
1356
+ }
1357
+ return openWallet(link, uri, input.via ?? "scheme");
1358
+ }
1288
1359
  async restore() {
1289
1360
  const client = await this.getClient();
1361
+ const checked = await Promise.all(
1362
+ [...this.topics].map(
1363
+ async ([walletId, topic]) => [walletId, await this.liveSession(client, topic)]
1364
+ )
1365
+ );
1290
1366
  const out = [];
1291
- for (const [walletId, topic] of [...this.topics]) {
1292
- const session = this.liveSession(client, topic);
1367
+ for (const [walletId, session] of checked) {
1293
1368
  if (!session) {
1294
1369
  this.topics.delete(walletId);
1295
1370
  continue;
@@ -1313,15 +1388,17 @@ var WalletConnectConnector = class extends BaseConnector {
1313
1388
  );
1314
1389
  }
1315
1390
  this.topics.clear();
1391
+ this.pending.clear();
1316
1392
  this.persist();
1317
1393
  }
1318
1394
  // --- helpers ---
1319
- tryReuse(client, req) {
1395
+ async tryReuse(client, req) {
1320
1396
  const topic = this.topics.get(req.walletId);
1321
1397
  if (!topic) return null;
1322
- const session = this.liveSession(client, topic);
1398
+ const session = await this.liveSession(client, topic);
1323
1399
  if (!session) {
1324
1400
  this.topics.delete(req.walletId);
1401
+ this.persist();
1325
1402
  return null;
1326
1403
  }
1327
1404
  const ns = session.namespaces[req.namespace.name];
@@ -1332,7 +1409,9 @@ var WalletConnectConnector = class extends BaseConnector {
1332
1409
  toConnection(client, walletId, session, namespace) {
1333
1410
  const ns = session.namespaces[namespace.name];
1334
1411
  const account = (ns?.accounts ?? []).map((a) => a.split(":")[2]).find((a) => !!a) ?? "";
1335
- const chainId = ns?.chains?.[0] ?? (ns?.accounts?.[0] ? ns.accounts[0].split(":").slice(0, 2).join(":") : `${namespace.name}:1`);
1412
+ const approved = ns?.chains?.length ? ns.chains : (ns?.accounts ?? []).map((a) => a.split(":").slice(0, 2).join(":"));
1413
+ const wanted = namespace.value.chains?.[0];
1414
+ const chainId = wanted && approved.includes(wanted) ? wanted : approved[0] ?? `${namespace.name}:1`;
1336
1415
  const topic = session.topic;
1337
1416
  return {
1338
1417
  walletId,
@@ -1357,12 +1436,27 @@ var WalletConnectConnector = class extends BaseConnector {
1357
1436
  }
1358
1437
  };
1359
1438
  }
1360
- liveSession(client, topic) {
1439
+ /**
1440
+ * A stored session the wallet is still on the other end of. The local record
1441
+ * is not evidence — it survives the user clearing the dApp inside their
1442
+ * wallet — and reusing a dead one returns a Connection that looks healthy
1443
+ * while every request hangs. So the peer has to answer for it.
1444
+ */
1445
+ async liveSession(client, topic) {
1446
+ let session;
1361
1447
  try {
1362
- return client.session.get(topic);
1448
+ session = client.session.get(topic);
1363
1449
  } catch {
1364
1450
  return void 0;
1365
1451
  }
1452
+ let timer;
1453
+ const alive = await Promise.race([
1454
+ client.ping({ topic }).then(() => true).catch(() => false),
1455
+ new Promise((resolve) => {
1456
+ timer = setTimeout(() => resolve(false), PING_TIMEOUT_MS);
1457
+ })
1458
+ ]).finally(() => clearTimeout(timer));
1459
+ return alive ? session : void 0;
1366
1460
  }
1367
1461
  walletForTopic(topic) {
1368
1462
  for (const [walletId, t] of this.topics) if (t === topic) return walletId;
@@ -1393,30 +1487,6 @@ function firstNamespace(session) {
1393
1487
  value: { chains: v.chains, methods: v.methods, events: v.events }
1394
1488
  };
1395
1489
  }
1396
- function withSignal2(promise, signal, onAbort) {
1397
- if (!signal) return promise;
1398
- if (signal.aborted) {
1399
- onAbort();
1400
- return Promise.reject(new DOMException("Aborted", "AbortError"));
1401
- }
1402
- return new Promise((resolve, reject) => {
1403
- const abortHandler = () => {
1404
- onAbort();
1405
- reject(new DOMException("Aborted", "AbortError"));
1406
- };
1407
- signal.addEventListener("abort", abortHandler, { once: true });
1408
- promise.then(
1409
- (v) => {
1410
- signal.removeEventListener("abort", abortHandler);
1411
- resolve(v);
1412
- },
1413
- (e) => {
1414
- signal.removeEventListener("abort", abortHandler);
1415
- reject(e);
1416
- }
1417
- );
1418
- });
1419
- }
1420
1490
 
1421
1491
  // src/core/lydian-connect.ts
1422
1492
  function injectedRdnsMap(sdkServed) {
@@ -1462,6 +1532,20 @@ var LydianConnect = class {
1462
1532
  signal: input.signal
1463
1533
  });
1464
1534
  }
1535
+ /**
1536
+ * Open a wallet app, optionally handing it the pairing URI from `display_uri`.
1537
+ *
1538
+ * Call this from a real click handler. A mobile browser will suppress the same
1539
+ * navigation issued automatically after an async step, which is why the
1540
+ * `display_uri` event reports `deepLink` — that is the library asking the host
1541
+ * for a button to put this behind.
1542
+ *
1543
+ * Returns whether a link was fired. `false` means nothing is known for this
1544
+ * wallet on this device; show the QR code instead.
1545
+ */
1546
+ openWallet(input) {
1547
+ return this.manager.openWallet(input);
1548
+ }
1465
1549
  restore() {
1466
1550
  return this.manager.restore();
1467
1551
  }