@lydianpay/lydianconnect 1.2.1 → 1.4.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
@@ -43,6 +43,8 @@ lc.on((event) => {
43
43
 
44
44
  `connect({ walletId, chainId })` — `chainId` is the chain the user is paying on (a number like `137`, or a CAIP-2 id like `'eip155:137'`).
45
45
 
46
+ The WalletConnect client also advertises a return link derived from the page URL (origin + path, no query/hash); wallets may use it to bring the user back to the page after a deep-link approval.
47
+
46
48
  ## Supported wallets
47
49
 
48
50
  The canonical list of supported wallets lives in
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
  };
@@ -270,6 +304,84 @@ var BaseConnector = class {
270
304
  function deriveUrl() {
271
305
  return typeof window !== "undefined" ? window.location.origin : "";
272
306
  }
307
+ function deriveRedirectUrl() {
308
+ if (typeof window === "undefined") return "";
309
+ return window.location.origin + window.location.pathname;
310
+ }
311
+
312
+ // src/connectors/eip155/chain.ts
313
+ var CHAIN_SETTLE = { timeoutMs: 4e3, intervalMs: 100 };
314
+ function toCaip(chainId) {
315
+ if (!chainId) return "eip155:1";
316
+ const num = chainId.startsWith("0x") ? parseInt(chainId, 16) : Number(chainId);
317
+ return `eip155:${Number.isFinite(num) ? num : 1}`;
318
+ }
319
+ async function currentChain(rpc2) {
320
+ const raw = await rpc2({ method: "eth_chainId" }).catch(() => null);
321
+ return toCaip(typeof raw === "string" ? raw : null);
322
+ }
323
+ async function settleChain({
324
+ rpc: rpc2,
325
+ chainId,
326
+ timeoutMs,
327
+ intervalMs
328
+ }) {
329
+ const deadline = Date.now() + timeoutMs;
330
+ for (; ; ) {
331
+ const actual = await currentChain(rpc2);
332
+ if (actual === chainId || Date.now() >= deadline) return;
333
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
334
+ }
335
+ }
336
+ async function ensureChain({
337
+ rpc: rpc2,
338
+ chainId,
339
+ timeoutMs = CHAIN_SETTLE.timeoutMs,
340
+ intervalMs = CHAIN_SETTLE.intervalMs
341
+ }) {
342
+ const chainNumber = chainId ? Number(chainId.split(":")[1]) : NaN;
343
+ if (!Number.isFinite(chainNumber) || chainNumber <= 0) return;
344
+ const requested = `eip155:${chainNumber}`;
345
+ if (await currentChain(rpc2) === requested) return;
346
+ const switched = await rpc2({
347
+ method: "wallet_switchEthereumChain",
348
+ params: [{ chainId: `0x${chainNumber.toString(16)}` }]
349
+ }).then(
350
+ () => true,
351
+ // Chain not added to the wallet, or the user declined — the caller's
352
+ // chainChanged will reflect reality. No settle: the chain isn't moving.
353
+ () => false
354
+ );
355
+ if (switched) {
356
+ await settleChain({ rpc: rpc2, chainId: requested, timeoutMs, intervalMs });
357
+ }
358
+ }
359
+
360
+ // src/core/abort.ts
361
+ function withSignal(promise, signal, onAbort) {
362
+ if (!signal) return promise;
363
+ if (signal.aborted) {
364
+ onAbort?.();
365
+ return Promise.reject(new DOMException("Aborted", "AbortError"));
366
+ }
367
+ return new Promise((resolve, reject) => {
368
+ const abortHandler = () => {
369
+ onAbort?.();
370
+ reject(new DOMException("Aborted", "AbortError"));
371
+ };
372
+ signal.addEventListener("abort", abortHandler, { once: true });
373
+ promise.then(
374
+ (value) => {
375
+ signal.removeEventListener("abort", abortHandler);
376
+ resolve(value);
377
+ },
378
+ (error) => {
379
+ signal.removeEventListener("abort", abortHandler);
380
+ reject(error);
381
+ }
382
+ );
383
+ });
384
+ }
273
385
 
274
386
  // src/connectors/metamask/connector.ts
275
387
  var WALLET_ID = "metamask";
@@ -328,17 +440,23 @@ var MetaMaskConnector = class extends BaseConnector {
328
440
  );
329
441
  }
330
442
  async connect(req) {
331
- const provider = await this.getProvider();
332
- const accounts = await provider.request({
333
- method: "eth_requestAccounts"
443
+ return this.duringConnect(async () => {
444
+ const provider = await this.getProvider();
445
+ const accounts = await withSignal(
446
+ provider.request({ method: "eth_requestAccounts" }),
447
+ req.signal
448
+ );
449
+ const account = accounts?.[0];
450
+ if (!account) throw new Error("MetaMask returned no account");
451
+ await ensureChain({
452
+ rpc: rpcFor(provider),
453
+ chainId: requestedChain(req)
454
+ });
455
+ const chainId = toCaip(
456
+ await provider.request({ method: "eth_chainId" })
457
+ );
458
+ return this.toConnection(provider, account, chainId);
334
459
  });
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
460
  }
343
461
  async restore() {
344
462
  const provider = await this.getProvider().catch(() => null);
@@ -356,22 +474,6 @@ var MetaMaskConnector = class extends BaseConnector {
356
474
  this.provider = null;
357
475
  this.listenersBound = false;
358
476
  }
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
477
  toConnection(provider, account, chainId) {
376
478
  return {
377
479
  walletId: WALLET_ID,
@@ -393,20 +495,18 @@ var MetaMaskConnector = class extends BaseConnector {
393
495
  };
394
496
  }
395
497
  };
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}`;
498
+ function rpcFor(provider) {
499
+ return (args) => provider.request(args);
400
500
  }
401
501
 
402
502
  // src/connectors/coinbase/connector.ts
503
+ var WALLET_ID2 = "coinbase";
403
504
  function rpc(provider, method, params) {
404
505
  return provider.request({
405
506
  method,
406
507
  params
407
508
  });
408
509
  }
409
- var WALLET_ID2 = "coinbase";
410
510
  var CoinbaseConnector = class extends BaseConnector {
411
511
  constructor(app) {
412
512
  super();
@@ -443,7 +543,7 @@ var CoinbaseConnector = class extends BaseConnector {
443
543
  this.emit({
444
544
  type: "chainChanged",
445
545
  walletId: WALLET_ID2,
446
- chainId: toCaip2(String(hex))
546
+ chainId: toCaip(String(hex))
447
547
  });
448
548
  });
449
549
  provider.on("accountsChanged", (accounts) => {
@@ -458,13 +558,18 @@ var CoinbaseConnector = class extends BaseConnector {
458
558
  );
459
559
  }
460
560
  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);
561
+ return this.duringConnect(async () => {
562
+ const provider = await this.getProvider(evmChainIds(req));
563
+ const accounts = await rpc(provider, "eth_requestAccounts");
564
+ const account = accounts?.[0];
565
+ if (!account) throw new Error("Coinbase Wallet returned no account");
566
+ await ensureChain({
567
+ rpc: rpcFor2(provider),
568
+ chainId: requestedChain(req)
569
+ });
570
+ const chainId = toCaip(await rpc(provider, "eth_chainId"));
571
+ return this.toConnection(provider, account, chainId);
572
+ });
468
573
  }
469
574
  async restore() {
470
575
  const provider = await this.getProvider().catch(() => null);
@@ -474,7 +579,7 @@ var CoinbaseConnector = class extends BaseConnector {
474
579
  );
475
580
  const account = accounts?.[0];
476
581
  if (!account) return [];
477
- const chainId = toCaip2(
582
+ const chainId = toCaip(
478
583
  await rpc(provider, "eth_chainId").catch(() => "0x1")
479
584
  );
480
585
  return [this.toConnection(provider, account, chainId)];
@@ -485,20 +590,6 @@ var CoinbaseConnector = class extends BaseConnector {
485
590
  this.provider = null;
486
591
  this.listenersBound = false;
487
592
  }
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
593
  toConnection(provider, account, chainId) {
503
594
  return {
504
595
  walletId: WALLET_ID2,
@@ -518,10 +609,8 @@ var CoinbaseConnector = class extends BaseConnector {
518
609
  };
519
610
  }
520
611
  };
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}`;
612
+ function rpcFor2(provider) {
613
+ return ({ method, params }) => rpc(provider, method, params);
525
614
  }
526
615
  function evmChainIds(req) {
527
616
  const wanted = req.namespace.value.chains?.[0];
@@ -585,8 +674,8 @@ var InjectedConnector = class extends BaseConnector {
585
674
  const account = accounts?.[0];
586
675
  if (!account)
587
676
  throw new Error(`${req.walletId} extension returned no account`);
588
- await this.ensureChain(provider, req);
589
- const chainId = toCaip3(
677
+ await ensureChain({ rpc: rpcFor3(provider), chainId: requestedChain(req) });
678
+ const chainId = toCaip(
590
679
  await provider.request({ method: "eth_chainId" })
591
680
  );
592
681
  this.bindEvents(provider, req.walletId);
@@ -605,7 +694,7 @@ var InjectedConnector = class extends BaseConnector {
605
694
  const accounts = await provider.request({ method: "eth_accounts" }).catch(() => []);
606
695
  const account = accounts?.[0];
607
696
  if (!account) continue;
608
- const chainId = toCaip3(
697
+ const chainId = toCaip(
609
698
  await provider.request({ method: "eth_chainId" }).catch(() => "0x1")
610
699
  );
611
700
  this.bindEvents(provider, walletId);
@@ -624,7 +713,7 @@ var InjectedConnector = class extends BaseConnector {
624
713
  this.emit({
625
714
  type: "chainChanged",
626
715
  walletId,
627
- chainId: toCaip3(String(hex))
716
+ chainId: toCaip(String(hex))
628
717
  });
629
718
  });
630
719
  provider.on("accountsChanged", (accounts) => {
@@ -637,22 +726,6 @@ var InjectedConnector = class extends BaseConnector {
637
726
  () => this.emit({ type: "disconnect", walletId })
638
727
  );
639
728
  }
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
729
  toConnection(provider, walletId, account, chainId) {
657
730
  return {
658
731
  walletId,
@@ -676,29 +749,8 @@ var InjectedConnector = class extends BaseConnector {
676
749
  };
677
750
  }
678
751
  };
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
- });
752
+ function rpcFor3(provider) {
753
+ return (args) => provider.request(args);
702
754
  }
703
755
 
704
756
  // src/connectors/walletconnect/wallet-links.ts
@@ -1148,13 +1200,15 @@ function buildLink(base, uri) {
1148
1200
  const sep = base.includes("?") ? "&" : "?";
1149
1201
  return `${base}${sep}uri=${encodeURIComponent(uri)}`;
1150
1202
  }
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
- }
1203
+ function canOpen(link) {
1204
+ return !!(link?.scheme || link?.universal);
1205
+ }
1206
+ function openWallet(link, uri, via = "scheme") {
1207
+ if (typeof document === "undefined") return false;
1208
+ const base = via === "universal" ? link.universal : link.scheme ?? link.universal;
1209
+ if (!base) return false;
1210
+ openHref(buildLink(base, uri));
1211
+ return true;
1158
1212
  }
1159
1213
  function openWalletApp(link) {
1160
1214
  if (typeof document === "undefined") return;
@@ -1193,6 +1247,7 @@ function detectAppOpen(timeoutMs) {
1193
1247
 
1194
1248
  // src/connectors/walletconnect/connector.ts
1195
1249
  var STORAGE_KEY = "lydianconnect.wc.topics";
1250
+ var PING_TIMEOUT_MS = 3e3;
1196
1251
  var WalletConnectConnector = class extends BaseConnector {
1197
1252
  constructor(app, options) {
1198
1253
  super();
@@ -1203,6 +1258,9 @@ var WalletConnectConnector = class extends BaseConnector {
1203
1258
  // claims any wallet not served by an SDK connector
1204
1259
  this.client = null;
1205
1260
  this.topics = /* @__PURE__ */ new Map();
1261
+ /** walletId -> pairing URI of the connect currently awaiting approval, so a
1262
+ * host-driven open button can fire without the host tracking the URI itself. */
1263
+ this.pending = /* @__PURE__ */ new Map();
1206
1264
  this.links = { ...DEFAULT_WALLET_LINKS, ...options.walletLinks ?? {} };
1207
1265
  }
1208
1266
  isAvailable() {
@@ -1210,13 +1268,16 @@ var WalletConnectConnector = class extends BaseConnector {
1210
1268
  }
1211
1269
  async getClient() {
1212
1270
  if (this.client) return this.client;
1271
+ const redirectUrl = deriveRedirectUrl();
1213
1272
  const client = await SignClient__default.default.init({
1214
1273
  projectId: this.options.projectId,
1215
1274
  metadata: {
1216
1275
  name: this.app.appName,
1217
1276
  description: this.app.description ?? this.app.appName,
1218
1277
  url: deriveUrl(),
1219
- icons: this.app.icon ? [this.app.icon] : []
1278
+ icons: this.app.icon ? [this.app.icon] : [],
1279
+ // Wallets may use this to return the user after deep-link approvals.
1280
+ ...redirectUrl ? { redirect: { universal: redirectUrl } } : {}
1220
1281
  },
1221
1282
  relayUrl: this.options.relayUrl
1222
1283
  });
@@ -1246,35 +1307,37 @@ var WalletConnectConnector = class extends BaseConnector {
1246
1307
  }
1247
1308
  async connect(req) {
1248
1309
  const client = await this.getClient();
1249
- const reused = this.tryReuse(client, req);
1310
+ const reused = await this.tryReuse(client, req);
1250
1311
  if (reused) return reused;
1251
1312
  const { uri, approval } = await client.connect({
1252
1313
  requiredNamespaces: { [req.namespace.name]: req.namespace.value }
1253
1314
  });
1254
1315
  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
- }
1316
+ const link = isMobile() ? this.links[req.walletId] : void 0;
1317
+ this.emit({
1318
+ type: "display_uri",
1319
+ walletId: req.walletId,
1320
+ uri,
1321
+ deepLink: canOpen(link)
1322
+ });
1323
+ if (link && canOpen(link)) {
1324
+ this.pending.set(req.walletId, uri);
1325
+ openWallet(link, uri);
1326
+ void detectAppOpen(this.options.openTimeoutMs ?? 2e3).then((opened) => {
1327
+ if (!opened)
1328
+ this.emit({
1329
+ type: "wallet_open_failed",
1330
+ walletId: req.walletId,
1331
+ uri,
1332
+ store: link.store
1333
+ });
1334
+ });
1272
1335
  }
1273
1336
  const pairingTopic = utils.parseUri(uri).topic;
1274
- const session = await withSignal2(approval(), req.signal, () => {
1337
+ const session = await withSignal(approval(), req.signal, () => {
1275
1338
  void client.core.pairing.disconnect({ topic: pairingTopic }).catch(() => {
1276
1339
  });
1277
- });
1340
+ }).finally(() => this.pending.delete(req.walletId));
1278
1341
  const connection = this.toConnection(
1279
1342
  client,
1280
1343
  req.walletId,
@@ -1285,11 +1348,30 @@ var WalletConnectConnector = class extends BaseConnector {
1285
1348
  this.persist();
1286
1349
  return connection;
1287
1350
  }
1351
+ /**
1352
+ * Fire a wallet's deep link on demand. The host calls this from a real click,
1353
+ * which is the one context a mobile browser reliably allows a scheme
1354
+ * navigation from — see the note on {@link openWallet}.
1355
+ */
1356
+ openWallet(input) {
1357
+ const link = this.links[input.walletId];
1358
+ if (!link) return false;
1359
+ const uri = input.uri ?? this.pending.get(input.walletId);
1360
+ if (!uri) {
1361
+ openWalletApp(link);
1362
+ return !!link.native;
1363
+ }
1364
+ return openWallet(link, uri, input.via ?? "scheme");
1365
+ }
1288
1366
  async restore() {
1289
1367
  const client = await this.getClient();
1368
+ const checked = await Promise.all(
1369
+ [...this.topics].map(
1370
+ async ([walletId, topic]) => [walletId, await this.liveSession(client, topic)]
1371
+ )
1372
+ );
1290
1373
  const out = [];
1291
- for (const [walletId, topic] of [...this.topics]) {
1292
- const session = this.liveSession(client, topic);
1374
+ for (const [walletId, session] of checked) {
1293
1375
  if (!session) {
1294
1376
  this.topics.delete(walletId);
1295
1377
  continue;
@@ -1313,15 +1395,17 @@ var WalletConnectConnector = class extends BaseConnector {
1313
1395
  );
1314
1396
  }
1315
1397
  this.topics.clear();
1398
+ this.pending.clear();
1316
1399
  this.persist();
1317
1400
  }
1318
1401
  // --- helpers ---
1319
- tryReuse(client, req) {
1402
+ async tryReuse(client, req) {
1320
1403
  const topic = this.topics.get(req.walletId);
1321
1404
  if (!topic) return null;
1322
- const session = this.liveSession(client, topic);
1405
+ const session = await this.liveSession(client, topic);
1323
1406
  if (!session) {
1324
1407
  this.topics.delete(req.walletId);
1408
+ this.persist();
1325
1409
  return null;
1326
1410
  }
1327
1411
  const ns = session.namespaces[req.namespace.name];
@@ -1332,7 +1416,9 @@ var WalletConnectConnector = class extends BaseConnector {
1332
1416
  toConnection(client, walletId, session, namespace) {
1333
1417
  const ns = session.namespaces[namespace.name];
1334
1418
  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`);
1419
+ const approved = ns?.chains?.length ? ns.chains : (ns?.accounts ?? []).map((a) => a.split(":").slice(0, 2).join(":"));
1420
+ const wanted = namespace.value.chains?.[0];
1421
+ const chainId = wanted && approved.includes(wanted) ? wanted : approved[0] ?? `${namespace.name}:1`;
1336
1422
  const topic = session.topic;
1337
1423
  return {
1338
1424
  walletId,
@@ -1357,12 +1443,27 @@ var WalletConnectConnector = class extends BaseConnector {
1357
1443
  }
1358
1444
  };
1359
1445
  }
1360
- liveSession(client, topic) {
1446
+ /**
1447
+ * A stored session the wallet is still on the other end of. The local record
1448
+ * is not evidence — it survives the user clearing the dApp inside their
1449
+ * wallet — and reusing a dead one returns a Connection that looks healthy
1450
+ * while every request hangs. So the peer has to answer for it.
1451
+ */
1452
+ async liveSession(client, topic) {
1453
+ let session;
1361
1454
  try {
1362
- return client.session.get(topic);
1455
+ session = client.session.get(topic);
1363
1456
  } catch {
1364
1457
  return void 0;
1365
1458
  }
1459
+ let timer;
1460
+ const alive = await Promise.race([
1461
+ client.ping({ topic }).then(() => true).catch(() => false),
1462
+ new Promise((resolve) => {
1463
+ timer = setTimeout(() => resolve(false), PING_TIMEOUT_MS);
1464
+ })
1465
+ ]).finally(() => clearTimeout(timer));
1466
+ return alive ? session : void 0;
1366
1467
  }
1367
1468
  walletForTopic(topic) {
1368
1469
  for (const [walletId, t] of this.topics) if (t === topic) return walletId;
@@ -1393,30 +1494,6 @@ function firstNamespace(session) {
1393
1494
  value: { chains: v.chains, methods: v.methods, events: v.events }
1394
1495
  };
1395
1496
  }
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
1497
 
1421
1498
  // src/core/lydian-connect.ts
1422
1499
  function injectedRdnsMap(sdkServed) {
@@ -1462,6 +1539,20 @@ var LydianConnect = class {
1462
1539
  signal: input.signal
1463
1540
  });
1464
1541
  }
1542
+ /**
1543
+ * Open a wallet app, optionally handing it the pairing URI from `display_uri`.
1544
+ *
1545
+ * Call this from a real click handler. A mobile browser will suppress the same
1546
+ * navigation issued automatically after an async step, which is why the
1547
+ * `display_uri` event reports `deepLink` — that is the library asking the host
1548
+ * for a button to put this behind.
1549
+ *
1550
+ * Returns whether a link was fired. `false` means nothing is known for this
1551
+ * wallet on this device; show the QR code instead.
1552
+ */
1553
+ openWallet(input) {
1554
+ return this.manager.openWallet(input);
1555
+ }
1465
1556
  restore() {
1466
1557
  return this.manager.restore();
1467
1558
  }