@bwalletx/connect 0.5.3 → 0.5.6

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +80 -8
  3. package/dist/bwalletx-connect.global.js +92 -25
  4. package/dist/bwalletx-connect.global.js.map +1 -1
  5. package/dist/{wallets-VOKIKBbO.d.ts → choice-DCh-tgpT.d.cts} +113 -5
  6. package/dist/{wallets-VOKIKBbO.d.cts → choice-DCh-tgpT.d.ts} +113 -5
  7. package/dist/chunk-34DKBQNN.js +222 -0
  8. package/dist/chunk-34DKBQNN.js.map +1 -0
  9. package/dist/{chunk-QRXFL66W.js → chunk-A2WLGS25.js} +6 -4
  10. package/dist/chunk-A2WLGS25.js.map +1 -0
  11. package/dist/chunk-IFGOAROM.js +255 -0
  12. package/dist/chunk-IFGOAROM.js.map +1 -0
  13. package/dist/chunk-LS6AYEBO.js +1886 -0
  14. package/dist/chunk-LS6AYEBO.js.map +1 -0
  15. package/dist/element-D9Zl-x12.d.cts +46 -0
  16. package/dist/element-D9Zl-x12.d.ts +46 -0
  17. package/dist/element.cjs +1221 -67
  18. package/dist/element.cjs.map +1 -1
  19. package/dist/element.d.cts +23 -28
  20. package/dist/element.d.ts +23 -28
  21. package/dist/element.js +29 -2
  22. package/dist/element.js.map +1 -1
  23. package/dist/index.cjs +1539 -449
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +185 -12
  26. package/dist/index.d.ts +185 -12
  27. package/dist/index.js +106 -12
  28. package/dist/index.js.map +1 -1
  29. package/dist/next.cjs +5 -3
  30. package/dist/next.cjs.map +1 -1
  31. package/dist/next.js +1 -1
  32. package/dist/react.cjs +1073 -194
  33. package/dist/react.cjs.map +1 -1
  34. package/dist/react.d.cts +75 -8
  35. package/dist/react.d.ts +75 -8
  36. package/dist/react.js +121 -40
  37. package/dist/react.js.map +1 -1
  38. package/dist/server.cjs +5 -3
  39. package/dist/server.cjs.map +1 -1
  40. package/dist/server.d.cts +8 -3
  41. package/dist/server.d.ts +8 -3
  42. package/dist/server.js +1 -1
  43. package/dist/shell-react.cjs.map +1 -1
  44. package/dist/shell-react.js.map +1 -1
  45. package/package.json +1 -1
  46. package/dist/chunk-IUDD7YTQ.js +0 -839
  47. package/dist/chunk-IUDD7YTQ.js.map +0 -1
  48. package/dist/chunk-PI42H3XY.js +0 -459
  49. package/dist/chunk-PI42H3XY.js.map +0 -1
  50. package/dist/chunk-QRXFL66W.js.map +0 -1
package/dist/react.cjs CHANGED
@@ -34,12 +34,16 @@ __export(react_exports, {
34
34
  BwalletxProvider: () => BwalletxProvider,
35
35
  BwalletxSignin: () => BwalletxSignin,
36
36
  PaymentError: () => PaymentError,
37
+ accountFor: () => accountFor,
38
+ autoChoice: () => autoChoice,
37
39
  brc100Option: () => brc100Option,
38
40
  default: () => react_default,
39
41
  discoverWallets: () => discoverWallets,
40
42
  handcashAdapter: () => handcashAdapter,
41
43
  pairingOption: () => pairingOption,
42
- useBwalletX: () => useBwalletX
44
+ signinStateFor: () => signinStateFor,
45
+ useBwalletX: () => useBwalletX,
46
+ useBwalletXAccount: () => useBwalletXAccount
43
47
  });
44
48
  module.exports = __toCommonJS(react_exports);
45
49
  var import_react = require("react");
@@ -83,10 +87,22 @@ function pairUrl(link) {
83
87
  const q = new URLSearchParams({ v: link.v, r: link.r, c: link.c, k: link.k, o: link.o, e: String(link.e) });
84
88
  return `https://${PAIR_HOST}/pair?${q}`;
85
89
  }
86
- var relaySocketUrl = (relay, channel, role, expiry) => {
90
+ var newClaim = () => b64url(crypto.getRandomValues(new Uint8Array(16)));
91
+ var WALLET_CLAIM_INFO = "bwallet-pair-wallet-claim-v1";
92
+ var hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
93
+ async function walletClaimFor(channel, sitePub) {
94
+ const d = await crypto.subtle.digest("SHA-256", enc.encode(`${WALLET_CLAIM_INFO}|${channel}|${sitePub}`));
95
+ return b64url(new Uint8Array(d));
96
+ }
97
+ async function claimDigest(claim) {
98
+ return hex(new Uint8Array(await crypto.subtle.digest("SHA-256", enc.encode(claim))));
99
+ }
100
+ var relaySocketUrl = (relay, channel, role, expiry, claim, walletClaimHash) => {
87
101
  const local = /^(localhost|127\.0\.0\.1)(:\d+)?$/.test(relay);
88
102
  const q = new URLSearchParams({ role });
89
103
  if (expiry) q.set("e", String(expiry));
104
+ if (claim) q.set("claim", claim);
105
+ if (walletClaimHash && role === "site") q.set("wc", walletClaimHash);
90
106
  return `${local ? "ws" : "wss"}://${relay}/v1/c/${channel}?${q}`;
91
107
  };
92
108
  async function deriveSession(mine, theirs, channel) {
@@ -184,6 +200,7 @@ function parseStored(raw) {
184
200
  if (typeof p.s !== "string" || !/^[0-9a-f]{64}$/i.test(p.s)) return null;
185
201
  if (typeof p.k !== "string" || !/^0[23][0-9a-f]{64}$/i.test(p.k)) return null;
186
202
  if (!n(p.sent) || !n(p.lastSeen) || !n(p.pairedAt)) return null;
203
+ if (p.sc !== void 0 && (typeof p.sc !== "string" || !/^[A-Za-z0-9_-]{22,64}$/.test(p.sc))) return null;
187
204
  return p;
188
205
  } catch {
189
206
  return null;
@@ -241,7 +258,12 @@ var Link = class {
241
258
  await this.opening;
242
259
  }
243
260
  reopen() {
244
- const ws = this.socket(relaySocketUrl(this.p.r, this.p.c, "site", Math.floor(Date.now() / 1e3) + QR_LIFETIME_S - 10));
261
+ if (!this.p.sc) {
262
+ this.p = { ...this.p, sc: newClaim() };
263
+ this.persist();
264
+ }
265
+ const e = Math.floor(Date.now() / 1e3) + QR_LIFETIME_S - 10;
266
+ const ws = this.socket(relaySocketUrl(this.p.r, this.p.c, "site", e, this.p.sc));
245
267
  return new Promise((resolve, reject) => {
246
268
  ws.onopen = () => {
247
269
  this.ws = ws;
@@ -335,11 +357,13 @@ function pairBwalletX(opts) {
335
357
  const siteKey = import_sdk2.PrivateKey.fromRandom();
336
358
  const channel = newChannel();
337
359
  const expiry = Math.floor(Date.now() / 1e3) + QR_LIFETIME_S;
338
- return new Promise((resolve, reject) => {
360
+ const siteClaim = newClaim();
361
+ const sitePub = siteKey.toPublicKey().toString();
362
+ return walletClaimFor(channel, sitePub).then(claimDigest).then((wc) => new Promise((resolve, reject) => {
339
363
  if (opts.signal?.aborted) return reject(new Error("cancelled"));
340
364
  let ws;
341
365
  try {
342
- ws = socket(relaySocketUrl(relay, channel, "site", expiry));
366
+ ws = socket(relaySocketUrl(relay, channel, "site", expiry, siteClaim, wc));
343
367
  } catch (e) {
344
368
  return reject(e);
345
369
  }
@@ -378,7 +402,7 @@ function pairBwalletX(opts) {
378
402
  if (msg.t === "ready") {
379
403
  settled = true;
380
404
  clearTimeout(timer);
381
- const p = { v: 1, c: channel, r: relay, s: siteKey.toHex(), k: walletKey, ...sealer.counters, pairedAt: Date.now() };
405
+ const p = { v: 1, c: channel, r: relay, s: siteKey.toHex(), k: walletKey, ...sealer.counters, pairedAt: Date.now(), sc: siteClaim };
382
406
  await Promise.resolve(storage.setItem(key, JSON.stringify(p))).catch(() => {
383
407
  });
384
408
  const link = new Link(p, sealer, storage, key, socket, opts.callTimeoutMs ?? 12e4);
@@ -388,8 +412,8 @@ function pairBwalletX(opts) {
388
412
  fail(new Error("Pairing was declined in bWalletX"));
389
413
  }
390
414
  });
391
- opts.onLink(pairUrl({ v: PAIR_VERSION, r: relay, c: channel, k: siteKey.toPublicKey().toString(), o: origin, e: expiry }));
392
- });
415
+ opts.onLink(pairUrl({ v: PAIR_VERSION, r: relay, c: channel, k: sitePub, o: origin, e: expiry }));
416
+ }));
393
417
  }
394
418
  async function restorePairing(opts = {}) {
395
419
  const storage = opts.storage === void 0 ? defaultStorage() : opts.storage ?? memoryStorage();
@@ -426,18 +450,88 @@ function declareWalletTheme(theme) {
426
450
  if (meta.content !== id) meta.content = id;
427
451
  }
428
452
 
453
+ // src/account.ts
454
+ var PAYMAIL_LOOKUP_URL = "https://pay.bwallet.space/api/paymail/lookup?key=";
455
+ var BAVATAR_URL = "https://chat.bwalletx.com/api/bavatar/";
456
+ function shortKey(key) {
457
+ return key.length > 12 ? `${key.slice(0, 6)}\u2026${key.slice(-4)}` : key;
458
+ }
459
+ function accountLabel(a) {
460
+ if (a.paymail && /@bwalletx\.com$/i.test(a.paymail)) return "$" + a.paymail.split("@")[0];
461
+ if (a.handle) return a.handle;
462
+ if (a.paymail) return a.paymail;
463
+ return a.identityKey ? shortKey(a.identityKey) : "";
464
+ }
465
+ function avatarUrlFor(identityKey) {
466
+ return identityKey && /^0[23][0-9a-fA-F]{64}$/.test(identityKey) ? `${BAVATAR_URL}${identityKey.toLowerCase()}.png` : null;
467
+ }
468
+ var cache = /* @__PURE__ */ new Map();
469
+ function lookupPaymail(identityKey, url = PAYMAIL_LOOKUP_URL) {
470
+ if (url === "off" || typeof fetch === "undefined") return Promise.resolve(null);
471
+ const k = `${url}|${identityKey}`;
472
+ let p = cache.get(k);
473
+ if (!p) {
474
+ p = fetch(url + encodeURIComponent(identityKey)).then((r) => r.ok ? r.json() : null).then((j) => j && typeof j.paymail === "string" ? j.paymail : null).catch(() => null);
475
+ cache.set(k, p);
476
+ }
477
+ return p;
478
+ }
479
+ function isUserRejection(e) {
480
+ const code = e?.code;
481
+ if (code === "ERR_PERMISSION_DENIED" || code === 4001) return true;
482
+ const msg = e instanceof Error ? e.message : String(e ?? "");
483
+ return /denied|reject|cancel|declin|refused|not now|user closed/i.test(msg);
484
+ }
485
+ function signInErrorText(e) {
486
+ if (isUserRejection(e)) return "Sign-in cancelled";
487
+ const code = e?.code;
488
+ if (code === "NOT_FOUND") return "bWalletX was not found in this browser";
489
+ const msg = e instanceof Error ? e.message : String(e ?? "");
490
+ if (/lock/i.test(msg)) return "Your wallet is locked. Unlock it and try again";
491
+ if (/timeout|timed out|expired/i.test(msg)) return "The wallet did not answer in time";
492
+ if (/origin/i.test(msg)) return "This page is not on the site\u2019s configured origin";
493
+ return "Sign-in failed";
494
+ }
495
+ async function copyText(text) {
496
+ try {
497
+ await navigator.clipboard.writeText(text);
498
+ return true;
499
+ } catch {
500
+ return false;
501
+ }
502
+ }
503
+ var IDENTITY_HINT_KEY = "bwx:identityKey";
504
+ function rememberedIdentity() {
505
+ try {
506
+ const k = globalThis.localStorage?.getItem(IDENTITY_HINT_KEY) ?? "";
507
+ return /^0[23][0-9a-f]{64}$/i.test(k) ? k : void 0;
508
+ } catch {
509
+ return void 0;
510
+ }
511
+ }
512
+ function rememberIdentity(key) {
513
+ try {
514
+ if (key) globalThis.localStorage?.setItem(IDENTITY_HINT_KEY, key);
515
+ else globalThis.localStorage?.removeItem(IDENTITY_HINT_KEY);
516
+ } catch {
517
+ }
518
+ }
519
+
429
520
  // src/discover.ts
430
521
  var import_sdk3 = require("@bsv/sdk");
431
522
  var RDNS_EXTENSION = "com.bwalletx.extension";
432
523
  var RDNS_MOBILE = "space.bwallet.mobile";
433
524
  var BWALLETX_RDNS = [RDNS_EXTENSION, RDNS_MOBILE];
434
- function discoverBwalletX(ms = 1200) {
525
+ var DEFAULT_DISCOVER_MS = 300;
526
+ function discoverBwalletX(ms = DEFAULT_DISCOVER_MS) {
435
527
  if (typeof window === "undefined") return Promise.resolve(null);
436
528
  return new Promise((resolve) => {
437
529
  let done = false;
530
+ let timer;
438
531
  const finish = (r) => {
439
532
  if (done) return;
440
533
  done = true;
534
+ if (timer !== void 0) clearTimeout(timer);
441
535
  window.removeEventListener("brc100:announceWallet", onAnnounce);
442
536
  resolve(r);
443
537
  };
@@ -450,11 +544,14 @@ function discoverBwalletX(ms = 1200) {
450
544
  };
451
545
  window.addEventListener("brc100:announceWallet", onAnnounce);
452
546
  window.dispatchEvent(new Event("brc100:requestWallet"));
453
- setTimeout(() => {
454
- const cwi = window.CWI;
455
- if (cwi) return finish({ wallet: cwi, method: "in-app", rdns: null });
456
- if (!isFramed()) return finish(null);
457
- void probeXdm(ms).then((w) => finish(w ? { wallet: w, method: "in-app", rdns: null } : null));
547
+ if (done) return;
548
+ if (isFramed()) {
549
+ void probeXdm(Math.max(ms, 1200)).then((w) => finish(w ? { wallet: w, method: "in-app", rdns: null } : null));
550
+ return;
551
+ }
552
+ timer = setTimeout(() => {
553
+ timer = void 0;
554
+ finish(null);
458
555
  }, ms);
459
556
  });
460
557
  }
@@ -510,39 +607,116 @@ async function probeXdm(ms = 1200, allowed = BWALLETX_PARENT_ORIGINS) {
510
607
  return null;
511
608
  }
512
609
  }
610
+ var warmedAt = 0;
611
+ function prewarmBwalletX() {
612
+ if (typeof window === "undefined" || Date.now() - warmedAt < 2e4) return;
613
+ warmedAt = Date.now();
614
+ void discoverBwalletX(0).then((f) => {
615
+ if (f && f.method === "extension") void f.wallet.isAuthenticated({}).catch(() => {
616
+ });
617
+ });
618
+ }
619
+ function bindPrewarm(el2) {
620
+ const on = () => prewarmBwalletX();
621
+ const evs = ["pointerenter", "focus", "touchstart"];
622
+ for (const e of evs) el2.addEventListener(e, on, { passive: true });
623
+ return () => evs.forEach((e) => el2.removeEventListener(e, on));
624
+ }
513
625
 
514
626
  // src/signin.ts
515
627
  var LOGIN_PROTOCOL = [2, "bwallet sign in"];
516
628
  async function connectBwalletX(opts = {}) {
517
- if (opts.wallet) return { wallet: opts.wallet, method: "wallet" };
518
- const found = await discoverBwalletX(opts.discoverMs);
519
- if (found) return { wallet: found.wallet, method: found.method };
629
+ const status = (s) => {
630
+ try {
631
+ opts.onStatus?.(s);
632
+ } catch {
633
+ }
634
+ };
635
+ if (opts.wallet) {
636
+ status("opening");
637
+ return { wallet: opts.wallet, method: "wallet" };
638
+ }
639
+ status("finding");
640
+ const found = await discoverBwalletX(opts.discoverMs ?? DEFAULT_DISCOVER_MS);
641
+ if (found) {
642
+ status("opening");
643
+ return { wallet: found.wallet, method: found.method };
644
+ }
520
645
  if (opts.reusePairing !== false) {
521
646
  const restored = await restorePairing(opts.pairing ?? {});
522
- if (restored) return { wallet: restored, method: "pairing" };
647
+ if (restored) {
648
+ status("opening");
649
+ return { wallet: restored, method: "pairing" };
650
+ }
523
651
  }
524
652
  if (!opts.pairing) {
525
653
  throw Object.assign(new Error("bWalletX was not found in this browser"), { code: "NOT_FOUND" });
526
654
  }
655
+ status("pairing");
527
656
  return { wallet: await pairBwalletX(opts.pairing), method: "pairing" };
528
657
  }
658
+ async function waitIfLocked(wallet) {
659
+ let ok = false;
660
+ try {
661
+ ok = !!(await wallet.isAuthenticated({}))?.authenticated;
662
+ } catch {
663
+ }
664
+ if (!ok) await Promise.resolve().then(() => wallet.waitForAuthentication({})).catch(() => {
665
+ });
666
+ }
667
+ function challengeWantsIdentityKey(e) {
668
+ const msg = e instanceof Error ? e.message : String(e ?? "");
669
+ return /identityKey/i.test(msg);
670
+ }
529
671
  async function signInWithBwalletX(opts) {
530
672
  const { wallet, method } = await connectBwalletX(opts);
531
- if (method !== "pairing") await wallet.waitForAuthentication({}).catch(() => {
673
+ if (method !== "pairing" && !opts.skipWaitForAuthentication) await waitIfLocked(wallet);
674
+ const status = () => {
675
+ try {
676
+ opts.onStatus?.("signing");
677
+ } catch {
678
+ }
679
+ };
680
+ const sign = (c2) => wallet.createSignature({
681
+ protocolID: c2.protocolID ?? LOGIN_PROTOCOL,
682
+ keyID: c2.keyID,
683
+ counterparty: "anyone",
684
+ data: Array.from(new TextEncoder().encode(c2.message))
532
685
  });
533
- const { publicKey: identityKey } = await atStage("identity", () => wallet.getPublicKey({ identityKey: true }));
534
- const c = await atStage("challenge", () => opts.challenge(identityKey));
535
- const { signature } = await atStage(
536
- "signature",
537
- () => wallet.createSignature({
538
- protocolID: c.protocolID ?? LOGIN_PROTOCOL,
539
- keyID: c.keyID,
540
- counterparty: "anyone",
541
- data: Array.from(new TextEncoder().encode(c.message))
542
- })
543
- );
686
+ const keyFirst = async () => {
687
+ const { publicKey: identityKey2 } = await atStage("identity", () => wallet.getPublicKey({ identityKey: true }));
688
+ status();
689
+ const c2 = await atStage("challenge", () => opts.challenge(identityKey2));
690
+ const { signature: signature2 } = await atStage("signature", () => sign(c2));
691
+ return { identityKey: identityKey2, signature: Array.from(signature2), nonce: c2.nonce, method, wallet };
692
+ };
693
+ if (opts.challengeNeedsIdentityKey && !opts.identityKey) return keyFirst();
694
+ let c;
695
+ try {
696
+ c = await atStage("challenge", () => opts.challenge(opts.identityKey));
697
+ } catch (e) {
698
+ if (!opts.identityKey && challengeWantsIdentityKey(e)) return keyFirst();
699
+ throw e;
700
+ }
701
+ status();
702
+ const [{ publicKey: identityKey }, { signature }] = await Promise.all([
703
+ opts.identityKey ? Promise.resolve({ publicKey: opts.identityKey }) : atStage("identity", () => wallet.getPublicKey({ identityKey: true })),
704
+ atStage("signature", () => sign(c))
705
+ ]);
544
706
  return { identityKey, signature: Array.from(signature), nonce: c.nonce, method, wallet };
545
707
  }
708
+ async function signInAndVerify(opts) {
709
+ const hinted = !opts.identityKey && !!opts.identityKeyHint;
710
+ const r = await signInWithBwalletX({ ...opts, identityKey: opts.identityKey ?? opts.identityKeyHint });
711
+ try {
712
+ return { result: r, verified: await atStage("verify", () => opts.verify(r)) };
713
+ } catch (e) {
714
+ const status = e?.status;
715
+ if (!hinted || status !== 401) throw e;
716
+ const again = await signInWithBwalletX({ ...opts, wallet: r.wallet, identityKey: void 0, skipWaitForAuthentication: true });
717
+ return { result: again, verified: await atStage("verify", () => opts.verify(again)) };
718
+ }
719
+ }
546
720
  async function atStage(stage, run) {
547
721
  try {
548
722
  return await run();
@@ -553,11 +727,272 @@ async function atStage(stage, run) {
553
727
  }
554
728
  }
555
729
 
730
+ // src/wallets.ts
731
+ var import_sdk4 = require("@bsv/sdk");
732
+ async function brc100Connected(wallet, o, c = {}) {
733
+ if (c.identity === false) return { wallet, payer: wallet, identityKey: null, optionId: o.id, kind: o.kind };
734
+ await waitIfLocked(wallet);
735
+ const { publicKey } = await wallet.getPublicKey({ identityKey: true });
736
+ return { wallet, payer: wallet, identityKey: publicKey, optionId: o.id, kind: o.kind };
737
+ }
738
+ function brc100Option(wallet, info) {
739
+ const opt = {
740
+ id: info.id,
741
+ name: info.name,
742
+ icon: info.icon,
743
+ rdns: info.rdns,
744
+ kind: "brc100",
745
+ isBwalletX: info.isBwalletX ?? false,
746
+ substrate: info.substrate,
747
+ wallet,
748
+ connect: (c) => brc100Connected(wallet, opt, c)
749
+ };
750
+ return opt;
751
+ }
752
+ function pairingOption(pairing) {
753
+ const opt = {
754
+ id: "bwalletx-pairing",
755
+ name: "bWalletX on your phone",
756
+ kind: "pairing",
757
+ isBwalletX: true,
758
+ substrate: "pairing",
759
+ async connect(c) {
760
+ const p = typeof pairing === "function" ? pairing() : pairing;
761
+ const w = await restorePairing(p) ?? await pairBwalletX(p);
762
+ return brc100Connected(w, opt, c);
763
+ }
764
+ };
765
+ return opt;
766
+ }
767
+ function handcashAdapter(o) {
768
+ const opt = {
769
+ id: "handcash",
770
+ name: "HandCash",
771
+ icon: o.icon,
772
+ kind: "handcash",
773
+ isBwalletX: false,
774
+ substrate: "adapter",
775
+ async connect() {
776
+ const p = await o.login();
777
+ return {
778
+ wallet: null,
779
+ payer: o.pay ? { pay: o.pay } : null,
780
+ identityKey: p.identityKey ?? null,
781
+ handle: p.handle,
782
+ paymail: p.paymail ?? `${p.handle.replace(/^\$/, "")}@handcash.io`,
783
+ optionId: opt.id,
784
+ kind: "handcash",
785
+ transitional: true
786
+ };
787
+ }
788
+ };
789
+ return opt;
790
+ }
791
+ var NAMES = { [RDNS_EXTENSION]: "bWalletX", "space.bwallet.mobile": "bWalletX" };
792
+ async function probe(substrate, ms) {
793
+ try {
794
+ const w = new import_sdk4.WalletClient(substrate);
795
+ const ok = await Promise.race([w.isAuthenticated({}).then(() => true), new Promise((r) => setTimeout(() => r(false), ms))]);
796
+ return ok ? w : null;
797
+ } catch {
798
+ return null;
799
+ }
800
+ }
801
+ function cwiIsAnnounced(cwi, announced) {
802
+ return announced.some((o) => o.wallet === cwi || o.rdns === RDNS_MOBILE);
803
+ }
804
+ async function discoverWallets(o = {}) {
805
+ const out = [];
806
+ const ms = o.ms ?? 300;
807
+ if (typeof window !== "undefined") {
808
+ const seen = /* @__PURE__ */ new Map();
809
+ const onAnnounce = (e) => {
810
+ const d = e.detail;
811
+ if (!d?.wallet) return;
812
+ const rdns = d.info?.rdns ?? d.info?.uuid ?? `wallet-${seen.size}`;
813
+ if (seen.has(rdns)) return;
814
+ const isB = BWALLETX_RDNS.includes(rdns);
815
+ const substrate = rdns === RDNS_EXTENSION ? "extension" : rdns === RDNS_MOBILE ? "in-app" : "injected";
816
+ seen.set(rdns, brc100Option(d.wallet, { id: rdns, rdns, name: NAMES[rdns] ?? d.info?.name ?? rdns, icon: d.info?.icon, isBwalletX: isB, substrate }));
817
+ };
818
+ window.addEventListener("brc100:announceWallet", onAnnounce);
819
+ window.dispatchEvent(new Event("brc100:requestWallet"));
820
+ const local = o.probeLocal ? Promise.all([probe("json-api", ms), probe("Cicada", ms)]) : Promise.resolve([null, null]);
821
+ await new Promise((r) => setTimeout(r, seen.size > 0 || window.CWI ? Math.min(ms, 50) : ms));
822
+ window.removeEventListener("brc100:announceWallet", onAnnounce);
823
+ out.push(...seen.values());
824
+ if (![...seen.values()].some((w) => w.isBwalletX) && isFramed()) {
825
+ const x = await probeXdm(Math.max(ms, 1200));
826
+ if (x) out.push(brc100Option(x, { id: "bwalletx-xdm", name: "bWalletX", isBwalletX: true, substrate: "xdm" }));
827
+ }
828
+ const cwi = window.CWI;
829
+ if (cwi && !cwiIsAnnounced(cwi, [...seen.values()])) {
830
+ out.push(brc100Option(cwi, { id: "window.CWI", name: seen.size ? "Another browser wallet" : "Browser wallet", isBwalletX: false, substrate: "window.CWI" }));
831
+ }
832
+ const [json, cicada] = await local;
833
+ if (json) out.push(brc100Option(json, { id: "localhost:3321", name: "Desktop wallet (localhost:3321)", substrate: "json-api" }));
834
+ if (cicada) out.push(brc100Option(cicada, { id: "localhost:3301", name: "Desktop wallet (localhost:3301)", substrate: "cicada" }));
835
+ }
836
+ if (o.pairing) out.push(pairingOption(o.pairing));
837
+ out.push(...o.extra ?? []);
838
+ const rank = (w) => w.isBwalletX ? 0 : w.kind === "handcash" ? 2 : 1;
839
+ return out.map((w, i) => ({ w, i })).sort((a, b) => rank(a.w) - rank(b.w) || a.i - b.i).map((x) => x.w);
840
+ }
841
+ var DESKTOP_OPTION_ID = "desktop";
842
+ function desktopOption(ms = 1500) {
843
+ const opt = {
844
+ id: DESKTOP_OPTION_ID,
845
+ name: "Desktop wallet",
846
+ kind: "brc100",
847
+ isBwalletX: false,
848
+ substrate: "json-api",
849
+ async connect(c) {
850
+ const [json, cicada] = await Promise.all([probe("json-api", ms), probe("Cicada", ms)]);
851
+ const w = json ?? cicada;
852
+ if (!w) throw Object.assign(new Error("No desktop wallet answered on this computer"), { code: "NOT_FOUND" });
853
+ opt.wallet = w;
854
+ opt.substrate = json ? "json-api" : "cicada";
855
+ return brc100Connected(w, opt, c);
856
+ }
857
+ };
858
+ return opt;
859
+ }
860
+ function autoChoice(list, force = false) {
861
+ const detected = list.filter((w) => w.kind === "brc100");
862
+ const others = list.filter((w) => w.kind === "handcash" || w.kind === "custom");
863
+ if (force) return detected[0] ?? list.find((w) => w.kind !== "pairing") ?? list[0] ?? null;
864
+ if (!list.length) return null;
865
+ if (list.length === 1) return list[0];
866
+ if (detected.length === 1 && others.length === 0) return detected[0];
867
+ if (detected.length === 0 && others.length === 0) return list[0];
868
+ return void 0;
869
+ }
870
+
871
+ // src/choice.ts
872
+ var BWALLETX_TYPE = "bwalletx";
873
+ function substrateOf(o) {
874
+ if (o.substrate) return o.substrate;
875
+ if (o.kind === "pairing") return "pairing";
876
+ if (o.isBwalletX && o.kind === "brc100") return o.rdns === RDNS_MOBILE ? "in-app" : o.id === "bwalletx-xdm" ? "xdm" : "extension";
877
+ if (o.kind === "handcash" || o.kind === "custom") return "adapter";
878
+ if (o.rdns === RDNS_EXTENSION) return "extension";
879
+ if (o.rdns === RDNS_MOBILE) return "in-app";
880
+ if (o.id === "window.CWI") return "window.CWI";
881
+ if (o.id === "localhost:3321" || o.id === DESKTOP_OPTION_ID) return "json-api";
882
+ if (o.id === "localhost:3301") return "cicada";
883
+ return "injected";
884
+ }
885
+ function walletTypeOf(o) {
886
+ return o.isBwalletX ? BWALLETX_TYPE : o.rdns ?? o.id;
887
+ }
888
+ var OWN_MARKERS = ["extension", "in-app", "xdm"];
889
+ function isPresentBwalletX(o) {
890
+ return o.isBwalletX && o.kind === "brc100" && !!o.wallet && OWN_MARKERS.includes(substrateOf(o));
891
+ }
892
+ function hasBwalletX(list) {
893
+ return list.some(isPresentBwalletX);
894
+ }
895
+ function otherDetected(list) {
896
+ return list.filter((o) => !o.isBwalletX && o.kind === "brc100" && !!o.wallet);
897
+ }
898
+ function chooserOrder(list) {
899
+ const rank = (o) => isPresentBwalletX(o) ? 0 : o.isBwalletX ? 1 : o.kind === "brc100" ? 2 : o.kind === "custom" ? 3 : 4;
900
+ return list.map((o, i) => ({ o, i })).sort((a, b) => rank(a.o) - rank(b.o) || a.i - b.i).map((x) => x.o);
901
+ }
902
+ function decideWallet(list, o = {}) {
903
+ if (o.walletId) {
904
+ const w = list.find((x) => x.id === o.walletId);
905
+ if (w) return { option: w, reason: "requested" };
906
+ }
907
+ if (o.chooser) return { option: null, reason: list.length || o.picker !== false ? "chooser" : "none" };
908
+ if (o.fastPath === false) {
909
+ const a = autoChoice([...list], o.picker === false);
910
+ return a === void 0 ? { option: null, reason: "chooser" } : a === null ? { option: null, reason: "none" } : { option: a, reason: "legacy" };
911
+ }
912
+ const b = list.find(isPresentBwalletX);
913
+ if (b) return { option: b, reason: "bwalletx" };
914
+ if (o.picker === false) {
915
+ const phone = list.find((x) => x.isBwalletX);
916
+ return phone ? { option: phone, reason: "bwalletx-phone" } : { option: null, reason: "none" };
917
+ }
918
+ return list.length ? { option: null, reason: "chooser" } : { option: null, reason: "none" };
919
+ }
920
+ function outcomeOf(e) {
921
+ const err = e;
922
+ if (err?.code === "NOT_FOUND") return "not-found";
923
+ if (err?.name === "AbortError" || err?.message === "cancelled") return "cancelled";
924
+ if (isUserRejection(e)) return "rejected";
925
+ return "error";
926
+ }
927
+ function attemptFor(option, outcome, startedAt, now = Date.now()) {
928
+ return {
929
+ walletType: option ? walletTypeOf(option) : null,
930
+ substrate: option ? substrateOf(option) : null,
931
+ outcome,
932
+ ms: Math.max(0, Math.round(now - startedAt))
933
+ };
934
+ }
935
+ function reportAttempt(hook, a) {
936
+ if (!hook) return;
937
+ try {
938
+ hook(Object.freeze({ walletType: a.walletType, substrate: a.substrate, outcome: a.outcome, ms: a.ms }));
939
+ } catch {
940
+ }
941
+ }
942
+
943
+ // src/connect-flow.ts
944
+ async function runConnect(o) {
945
+ const started = Date.now();
946
+ const { list } = o;
947
+ let choice = null;
948
+ let reason = "none";
949
+ try {
950
+ const d = decideWallet(list, { walletId: o.walletId, chooser: o.chooser, picker: o.picker, fastPath: o.fastPath });
951
+ choice = d.option;
952
+ reason = d.reason;
953
+ if (!choice && d.reason === "chooser") choice = await o.pick(list);
954
+ if (!choice) {
955
+ if (d.reason === "none" || !list.length) throw Object.assign(new Error("No wallet found. Get bWalletX at bwalletx.com."), { code: "NOT_FOUND" });
956
+ reportAttempt(o.onAttempt, attemptFor(null, "cancelled", started));
957
+ return null;
958
+ }
959
+ const signIn = !!(o.challengeUrl && o.verifyUrl) && choice.kind !== "handcash";
960
+ const cw = await choice.connect(signIn ? { identity: false } : void 0);
961
+ let handle = cw.handle ?? null;
962
+ let paymail = cw.paymail ?? null;
963
+ let identityKey = cw.identityKey;
964
+ let signedIn = false;
965
+ if (cw.wallet && signIn) {
966
+ const { result, verified } = await signInAndVerify({
967
+ wallet: cw.wallet,
968
+ identityKey: cw.identityKey ?? void 0,
969
+ // The key that signed in here last time: a repeat sign-in is then the signature alone.
970
+ identityKeyHint: rememberedIdentity(),
971
+ challengeNeedsIdentityKey: o.challengeNeedsIdentityKey,
972
+ challenge: (key) => o.post(o.challengeUrl, key ? { identityKey: key } : {}),
973
+ verify: (r) => o.post(o.verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature })
974
+ });
975
+ rememberIdentity(result.identityKey);
976
+ identityKey = result.identityKey;
977
+ handle = verified.handle ?? handle;
978
+ paymail = verified.paymail ?? paymail;
979
+ signedIn = true;
980
+ } else if (cw.wallet && !identityKey) {
981
+ identityKey = (await cw.wallet.getPublicKey({ identityKey: true })).publicKey;
982
+ }
983
+ reportAttempt(o.onAttempt, attemptFor(choice, signedIn ? "signed-in" : "connected", started));
984
+ return { connected: cw, identityKey, handle, paymail, signedIn, walletType: walletTypeOf(choice), substrate: substrateOf(choice), reason };
985
+ } catch (e) {
986
+ reportAttempt(o.onAttempt, attemptFor(choice, outcomeOf(e), started));
987
+ throw e;
988
+ }
989
+ }
990
+
556
991
  // src/modal.ts
557
992
  var import_qrcode_generator = __toESM(require("qrcode-generator"), 1);
558
993
 
559
994
  // src/access-client.ts
560
- var import_sdk4 = require("@bsv/sdk");
995
+ var import_sdk5 = require("@bsv/sdk");
561
996
 
562
997
  // src/access.ts
563
998
  var ACCESS_PROTOCOL = [2, "bwalletx access"];
@@ -695,7 +1130,7 @@ async function proveHoldings(wallet, challenge, tokenId, basket = "bsv21") {
695
1130
  const counterparty = ci.counterparty ?? "self";
696
1131
  for (const protocolID of ci.protocolID ? [ci.protocolID] : [ONESAT, LEGACY_ONESAT]) {
697
1132
  const { publicKey } = await wallet.getPublicKey({ protocolID, keyID: ci.keyID, counterparty });
698
- const h = import_sdk4.Utils.toHex(import_sdk4.Hash.hash160(import_sdk4.PublicKey.fromString(publicKey).encode(true)));
1133
+ const h = import_sdk5.Utils.toHex(import_sdk5.Hash.hash160(import_sdk5.PublicKey.fromString(publicKey).encode(true)));
699
1134
  if (want && h !== want) continue;
700
1135
  const existing = byKey.get(publicKey);
701
1136
  if (existing) {
@@ -745,7 +1180,7 @@ async function requestAccess(o) {
745
1180
  var SIGNIN_TAG = "bwalletx-signin";
746
1181
  var GET_URL = "https://bwalletx.com/get";
747
1182
  var WEB_WALLET_URL = "https://web.bwalletx.com";
748
- var VERBS = { signin: "Sign in with", continue: "Continue with", connect: "Connect" };
1183
+ var VERBS = { signin: "Sign in with", continue: "Continue with", connect: "Connect", get: "Get", play: "Play with" };
749
1184
  function signinText(label = "signin", brand = "bWalletX") {
750
1185
  return `${VERBS[label] ?? VERBS.signin} ${brand}`;
751
1186
  }
@@ -800,7 +1235,50 @@ button[aria-busy=true]{cursor:wait}
800
1235
  .nf a:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:2px}
801
1236
  :host([theme=dark]) .nf,:host-context(.dark) .nf{color:#a1a1aa}
802
1237
  .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
1238
+ /* pending: one plain line under the button */
1239
+ .note{margin:8px 0 0;width:0;min-width:100%;font-size:12px;line-height:1.4;color:#71717a;text-align:center;overflow-wrap:break-word}
1240
+ :host([theme=dark]) .note,:host-context(.dark) .note{color:#a1a1aa}
1241
+ /* "Other wallets" (0.5.6): one small link under the button */
1242
+ .ow{margin:8px 0 0;width:0;min-width:100%;font-size:12px;line-height:1.4;color:#71717a;text-align:center}
1243
+ :host([theme=dark]) .ow,:host-context(.dark) .ow{color:#a1a1aa}
1244
+ .lnk{all:unset;cursor:pointer;color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px}
1245
+ .lnk:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:2px}
1246
+ /* error: one line plus Retry */
1247
+ .err{margin:0 0 8px;width:0;min-width:100%;font-size:13px;font-weight:600;line-height:1.4;color:#b91c1c;text-align:center;overflow-wrap:anywhere}
1248
+ :host([theme=dark]) .err,:host-context(.dark) .err{color:#fca5a5}
1249
+ /* signed in: the badge */
1250
+ button.chip{display:inline-flex;align-items:center;justify-content:flex-start;gap:10px;width:auto;max-width:100%;padding:5px 14px 5px 5px;border-radius:999px;
1251
+ background:#0b0b0c;color:#ffd24d;border:1px solid rgba(255,210,77,.55);box-shadow:none;font-size:15px}
1252
+ button.chip:hover{background:#151517;border-color:#ffd24d}
1253
+ button.chip:active{background:#000;transform:none}
1254
+ button.chip:focus-visible{outline:2px solid #ffd24d;outline-offset:3px}
1255
+ :host([theme=light]) button.chip{background:#fff;color:#09090b;border-color:#f59e0b}
1256
+ :host([theme=light]) button.chip:hover{background:#fffbeb}
1257
+ :host([full-width]) button.chip{width:100%}
1258
+ .av{position:relative;flex:none;display:inline-grid;place-items:center;width:30px;height:30px;border-radius:50%;overflow:hidden;
1259
+ background:radial-gradient(circle at 30% 30%,#ffe27a,#f5b800 55%,#b88600);color:#000;font-weight:800;font-size:14px}
1260
+ .av img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
1261
+ .name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:min(52vw,260px)}
1262
+ .dot{flex:none;width:8px;height:8px;border-radius:50%;background:#ffd24d;box-shadow:0 0 0 3px rgba(255,210,77,.22)}
1263
+ :host([size=sm]) button.chip{font-size:13px;padding:3px 12px 3px 3px;gap:8px}
1264
+ :host([size=sm]) .av{width:26px;height:26px}
1265
+ :host([size=icon]) button.chip{width:48px;height:48px;padding:0;justify-content:center}
1266
+ :host([size=icon]) .name,:host([size=icon]) .dot{display:none}
1267
+ :host([size=icon]) .av{width:40px;height:40px}
1268
+ `;
1269
+ var MENU_CSS = `
1270
+ :host{all:initial}
1271
+ .menu{position:fixed;z-index:2147483600;box-sizing:border-box;min-width:200px;padding:6px;border-radius:14px;background:#0b0b0c;color:#ffd24d;
1272
+ border:1px solid rgba(255,210,77,.55);box-shadow:0 12px 32px rgba(0,0,0,.45);font:600 14px/1.3 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
1273
+ .who{padding:8px 10px 10px;font-size:12px;font-weight:500;color:#e7d9a8;overflow-wrap:anywhere;border-bottom:1px solid rgba(255,210,77,.3);margin-bottom:4px}
1274
+ .item{all:unset;box-sizing:border-box;display:block;width:100%;padding:11px 10px;border-radius:10px;cursor:pointer;color:#ffd24d}
1275
+ .item:hover,.item:focus-visible{background:#ffd24d;color:#000;outline:none}
1276
+ .tip{position:fixed;z-index:2147483600;box-sizing:border-box;padding:8px 12px;border-radius:10px;background:#0b0b0c;color:#e7d9a8;
1277
+ border:1px solid rgba(255,210,77,.4);box-shadow:0 8px 24px rgba(0,0,0,.35);font:500 12px/1.4 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
803
1278
  `;
1279
+ var PENDING_NOTE = "Sign in: shares only your public identity key, moves no money";
1280
+ var PENDING_LABEL = "Approve in bWalletX\u2026";
1281
+ var PENDING_LABEL_OTHER = "Approve in your wallet\u2026";
804
1282
  function b64url2(bytes) {
805
1283
  let s = "";
806
1284
  for (const b of bytes) s += String.fromCharCode(b);
@@ -833,14 +1311,15 @@ async function buildAuthorizeUrl(p) {
833
1311
  async function postJson(url, body) {
834
1312
  const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
835
1313
  const data = await res.json().catch(() => ({}));
836
- if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
1314
+ if (!res.ok) throw Object.assign(new Error(data.error ?? `HTTP ${res.status}`), { status: res.status });
837
1315
  return data;
838
1316
  }
839
1317
  async function postAccess(url, body) {
840
1318
  const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
841
1319
  return res.json().catch(() => ({ error: "bad_request", message: `HTTP ${res.status}` }));
842
1320
  }
843
- var OBSERVED = ["size", "theme", "label", "brand", "subtitle", "loading", "disabled", "full-width", "aria-label", "get-url", "web-url"];
1321
+ var OBSERVED = ["size", "theme", "label", "brand", "subtitle", "loading", "disabled", "full-width", "aria-label", "get-url", "web-url", "state", "identity-key", "paymail", "handle", "avatar-url", "error", "other-wallets", "challenge-url"];
1322
+ var STATES = ["signed-out", "restoring", "pending", "signed-in", "error"];
844
1323
  function createClass() {
845
1324
  return class BwalletxSigninElement extends HTMLElement {
846
1325
  static get observedAttributes() {
@@ -849,12 +1328,23 @@ function createClass() {
849
1328
  #root;
850
1329
  #notFound = false;
851
1330
  #busy = false;
1331
+ #phase = "signed-out";
1332
+ #account = null;
1333
+ #error = "";
1334
+ #restored = false;
1335
+ #pendingLabel = PENDING_LABEL;
1336
+ #menu = null;
852
1337
  constructor() {
853
1338
  super();
854
1339
  this.#root = this.attachShadow({ mode: "open" });
855
1340
  }
856
1341
  connectedCallback() {
857
1342
  this.#render();
1343
+ void this.#restore();
1344
+ }
1345
+ disconnectedCallback() {
1346
+ this.#menu?.close(false);
1347
+ this.#hideTip();
858
1348
  }
859
1349
  attributeChangedCallback() {
860
1350
  if (this.isConnected) this.#render();
@@ -866,92 +1356,402 @@ function createClass() {
866
1356
  get notFound() {
867
1357
  return this.#notFound;
868
1358
  }
1359
+ /** The state shown now (the `state` attribute when set, else the element's own). */
1360
+ get state() {
1361
+ const a = this.#attr("state");
1362
+ return a && STATES.includes(a) ? a : this.#phase;
1363
+ }
1364
+ /** The signed-in account, or null. */
1365
+ get account() {
1366
+ if (!this.#controlled) return this.#account;
1367
+ const identityKey = this.#attr("identity-key");
1368
+ return identityKey ? { identityKey, paymail: this.#attr("paymail"), handle: this.#attr("handle"), avatarUrl: this.#attr("avatar-url") } : null;
1369
+ }
1370
+ /** True while the menu is open (tests). */
1371
+ get menuOpen() {
1372
+ return !!this.#menu;
1373
+ }
869
1374
  focus(opts) {
870
1375
  this.button?.focus(opts);
871
1376
  }
1377
+ get #controlled() {
1378
+ return this.hasAttribute("state");
1379
+ }
1380
+ get #stateful() {
1381
+ return !this.hasAttribute("stateless");
1382
+ }
872
1383
  #attr(name) {
873
1384
  return this.getAttribute(name);
874
1385
  }
1386
+ /** Restore the badge from session-url once, with no wallet call. */
1387
+ async #restore() {
1388
+ const url = this.#attr("session-url");
1389
+ if (this.#restored || !url || this.#controlled || !this.#stateful) return;
1390
+ this.#restored = true;
1391
+ this.#set("restoring");
1392
+ try {
1393
+ const r = await fetch(url, { credentials: "include", headers: { accept: "application/json" } });
1394
+ const j = await r.json().catch(() => ({}));
1395
+ if (this.#phase !== "restoring") return;
1396
+ if (j.identityKey) this.#signedIn({ identityKey: j.identityKey, handle: j.handle ?? null, paymail: j.paymail ?? null });
1397
+ else this.#set("signed-out");
1398
+ } catch {
1399
+ if (this.#phase === "restoring") this.#set("signed-out");
1400
+ }
1401
+ }
1402
+ #set(p) {
1403
+ this.#phase = p;
1404
+ if (p !== "signed-in") this.#menu?.close(false);
1405
+ this.#render();
1406
+ }
1407
+ #signedIn(a) {
1408
+ this.#account = a;
1409
+ this.#error = "";
1410
+ this.#set("signed-in");
1411
+ const lookup = this.#attr("lookup-url");
1412
+ if (!a.paymail && lookup !== "off") {
1413
+ void lookupPaymail(a.identityKey, lookup ?? void 0).then((pm) => {
1414
+ if (pm && this.#account?.identityKey === a.identityKey && !this.#account.paymail) {
1415
+ this.#account = { ...this.#account, paymail: pm };
1416
+ this.#render();
1417
+ }
1418
+ });
1419
+ }
1420
+ }
875
1421
  #render() {
876
1422
  const size = this.#attr("size") ?? "lg";
877
1423
  const brand = this.#attr("brand") === "bWallet" ? "bWallet" : "bWalletX";
878
- const text = signinText(this.#attr("label") ?? "signin", brand);
1424
+ const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
1425
+ const state = this.state;
1426
+ const acct = this.account;
1427
+ if (state === "signed-in" && acct) {
1428
+ this.#hideTip();
1429
+ const name = accountLabel(acct);
1430
+ const avatar = acct.avatarUrl ?? avatarUrlFor(acct.identityKey);
1431
+ const initial = (name.replace(/^\$/, "").charAt(0) || "b").toUpperCase();
1432
+ this.#root.innerHTML = `<style>${SIGNIN_CSS}</style><button type="button" class="chip" part="button chip" aria-haspopup="menu" aria-expanded="${this.#menu ? "true" : "false"}" aria-label="Signed in with bWalletX as ${esc(name)}. Account menu" title="Signed in as ${esc(name)}"><span class="av" part="avatar" aria-hidden="true">${esc(initial)}${avatar ? `<img alt="" src="${esc(avatar)}" referrerpolicy="no-referrer">` : ""}</span><span class="name" part="label">${esc(name)}</span><span class="dot" part="dot" aria-hidden="true"></span></button>`;
1433
+ const img = this.#root.querySelector("img");
1434
+ if (img) img.addEventListener("error", () => img.remove(), { once: true });
1435
+ const chip = this.button;
1436
+ chip.addEventListener("click", () => this.#menu ? this.#menu.close(true) : this.#openMenu());
1437
+ chip.addEventListener("keydown", (e) => {
1438
+ if ((e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") && !this.#menu) {
1439
+ e.preventDefault();
1440
+ this.#openMenu();
1441
+ }
1442
+ });
1443
+ return;
1444
+ }
1445
+ const pending = state === "pending";
1446
+ const restoring = state === "restoring";
1447
+ const error = state === "error";
1448
+ const pendingLabel = this.#controlled ? PENDING_LABEL : this.#pendingLabel;
1449
+ const text = pending ? pendingLabel : error ? "Retry" : signinText(this.#attr("label") ?? "signin", brand);
879
1450
  const subAttr = this.#attr("subtitle");
880
1451
  const subDefault = brand === "bWalletX" && (size === "lg" || size === "md") ? "also works with bWallet" : null;
881
- const subtitle = subAttr === null || subAttr === "" || subAttr === "on" ? subAttr === "on" ? "also works with bWallet" : subDefault : subAttr === "off" ? null : subAttr;
882
- const loading = this.hasAttribute("loading") || this.#busy;
1452
+ const subtitle = pending || error ? null : subAttr === null || subAttr === "" || subAttr === "on" ? subAttr === "on" ? "also works with bWallet" : subDefault : subAttr === "off" ? null : subAttr;
1453
+ const loading = this.hasAttribute("loading") || this.#busy || pending || restoring;
883
1454
  const disabled = this.hasAttribute("disabled");
884
1455
  const markPx = size === "icon" ? 24 : size === "sm" ? 15 : size === "md" ? 17 : 20;
885
- const aria = this.#attr("aria-label") ?? (subtitle && size !== "icon" ? `${text}, ${subtitle}` : text);
1456
+ const aria = pending ? pendingLabel : this.#attr("aria-label") ?? (subtitle && size !== "icon" ? `${text}, ${subtitle}` : text);
886
1457
  const getUrl = this.#attr("get-url") ?? GET_URL;
887
1458
  const webUrl = this.#attr("web-url") ?? WEB_WALLET_URL;
888
- const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
1459
+ const errText = error ? (this.#controlled ? this.#attr("error") : this.#error) || "Sign-in failed" : "";
889
1460
  const label = size === "icon" ? "" : `<span class="txt" part="text"><b part="label">${esc(text)}</b>${subtitle ? `<small part="subtitle">${esc(subtitle)}</small>` : ""}</span>`;
890
1461
  const mark = loading ? '<span class="spin" aria-hidden="true"></span>' : markSvg(markPx);
891
- const nf = this.#notFound ? `<p class="nf" part="not-found" role="status">bWalletX wasn't found in this browser. <a href="${esc(getUrl)}" target="_blank" rel="noopener">Get ${brand}</a> or <a href="${esc(webUrl)}" target="_blank" rel="noopener">use the web wallet</a>.</p>` : "";
892
- this.#root.innerHTML = `<style>${SIGNIN_CSS}</style><button type="button" part="button" aria-label="${esc(aria)}" title="${esc(subtitle ? `${text} (${subtitle})` : text)}"${disabled ? " disabled" : ""}${loading ? ' aria-busy="true"' : ""}><span class="mark" part="mark">${mark}</span>${label}</button>${loading ? '<span class="sr" role="status">Connecting to bWalletX\u2026</span>' : ""}${nf}`;
1462
+ const otherOff = this.#attr("other-wallets") === "off";
1463
+ const otherLink = '<button type="button" class="lnk" data-act="other" part="other-wallets-link">Other wallets</button>';
1464
+ const nf = this.#notFound ? `<p class="nf" part="not-found" role="status">bWalletX wasn't found in this browser. <a href="${esc(getUrl)}" target="_blank" rel="noopener">Get ${brand}</a> or <a href="${esc(webUrl)}" target="_blank" rel="noopener">use the web wallet</a>.${otherOff ? "" : ` ${otherLink}`}</p>` : "";
1465
+ const ow = !this.#notFound && !pending && !restoring && this.#otherWalletsShown(size) ? `<p class="ow" part="other-wallets">${otherLink}</p>` : "";
1466
+ const err = error ? `<p class="err" part="error" role="alert">${esc(errText)}</p>` : "";
1467
+ const compact = size === "sm" || size === "icon";
1468
+ const note = pending && !compact ? `<p class="note" part="note">${esc(PENDING_NOTE)}</p>` : "";
1469
+ const status = pending ? `<span class="sr" role="status">${esc(pendingLabel)}</span>` : loading ? '<span class="sr" role="status">Connecting to bWalletX\u2026</span>' : "";
1470
+ this.#root.innerHTML = `<style>${SIGNIN_CSS}</style>${err}<button type="button" part="button" aria-label="${esc(aria)}" title="${esc(subtitle ? `${text} (${subtitle})` : text)}"${disabled ? " disabled" : ""}${loading ? ' aria-busy="true"' : ""}><span class="mark" part="mark">${mark}</span>${label}</button>${status}${note}${ow}${nf}`;
893
1471
  this.button.addEventListener("click", () => void this.#click());
1472
+ for (const l of Array.from(this.#root.querySelectorAll("[data-act=other]"))) l.addEventListener("click", () => void this.otherWallets());
1473
+ bindPrewarm(this.button);
1474
+ if (pending && compact) this.#showTip(PENDING_NOTE);
1475
+ else this.#hideTip();
1476
+ }
1477
+ #tip = null;
1478
+ #showTip(text) {
1479
+ const b = this.button;
1480
+ if (!b || typeof document === "undefined") return;
1481
+ if (!this.#tip) {
1482
+ const host = document.createElement("div");
1483
+ host.setAttribute("data-bwalletx-tip", "");
1484
+ const root = host.attachShadow({ mode: "open" });
1485
+ root.innerHTML = `<style>${MENU_CSS}</style><div class="tip" role="note"></div>`;
1486
+ document.body.appendChild(host);
1487
+ this.#tip = host;
1488
+ }
1489
+ const tip2 = this.#tip.shadowRoot.querySelector(".tip");
1490
+ tip2.textContent = text;
1491
+ const r = b.getBoundingClientRect();
1492
+ const vw = window.innerWidth || document.documentElement.clientWidth || 360;
1493
+ const width = Math.min(280, vw - 32);
1494
+ tip2.style.width = `${width}px`;
1495
+ tip2.style.left = `${Math.max(16, Math.min(r.right - width, vw - width - 16))}px`;
1496
+ tip2.style.top = `${r.bottom + 8}px`;
1497
+ }
1498
+ #hideTip() {
1499
+ this.#tip?.remove();
1500
+ this.#tip = null;
1501
+ }
1502
+ #openMenu() {
1503
+ const acct = this.account;
1504
+ const chip = this.button;
1505
+ if (!acct || !chip || typeof document === "undefined") return;
1506
+ const name = accountLabel(acct);
1507
+ const host = document.createElement("div");
1508
+ host.setAttribute("data-bwalletx-menu", "");
1509
+ const root = host.attachShadow({ mode: "open" });
1510
+ const style = document.createElement("style");
1511
+ style.textContent = MENU_CSS;
1512
+ const menu = document.createElement("div");
1513
+ menu.className = "menu";
1514
+ menu.setAttribute("role", "menu");
1515
+ menu.setAttribute("aria-label", "bWalletX account");
1516
+ const who = document.createElement("div");
1517
+ who.className = "who";
1518
+ who.textContent = `Signed in as ${acct.paymail || name}`;
1519
+ const item = (text, run) => {
1520
+ const b = document.createElement("button");
1521
+ b.type = "button";
1522
+ b.className = "item";
1523
+ b.setAttribute("role", "menuitem");
1524
+ b.tabIndex = -1;
1525
+ b.textContent = text;
1526
+ b.addEventListener("click", () => run(b));
1527
+ return b;
1528
+ };
1529
+ const copyWhat = acct.paymail ? "paymail" : "key";
1530
+ const copy = item(`Copy ${copyWhat}`, (b) => {
1531
+ void copyText(acct.paymail || acct.identityKey).then((ok) => {
1532
+ b.textContent = ok ? "Copied" : `Copy ${copyWhat}`;
1533
+ setTimeout(() => close(true), ok ? 600 : 0);
1534
+ });
1535
+ });
1536
+ const out = item("Disconnect", () => {
1537
+ close(false);
1538
+ void this.signOut();
1539
+ });
1540
+ menu.append(who, copy, out);
1541
+ root.append(style, menu);
1542
+ document.body.appendChild(host);
1543
+ const place = () => {
1544
+ const r = chip.getBoundingClientRect();
1545
+ const vw = window.innerWidth || document.documentElement.clientWidth || 360;
1546
+ const vh = window.innerHeight || document.documentElement.clientHeight || 640;
1547
+ const width = Math.min(260, vw - 32);
1548
+ menu.style.width = `${width}px`;
1549
+ const left = Math.max(16, Math.min(r.right - width, vw - width - 16));
1550
+ menu.style.left = `${left}px`;
1551
+ const h = menu.offsetHeight || 150;
1552
+ const below = r.bottom + 8;
1553
+ menu.style.top = `${below + h > vh && r.top - 8 - h > 0 ? r.top - 8 - h : below}px`;
1554
+ };
1555
+ place();
1556
+ const items = [copy, out];
1557
+ const focusAt = (i) => items[(i + items.length) % items.length].focus();
1558
+ const onKey = (e) => {
1559
+ const i = items.indexOf(root.activeElement);
1560
+ if (e.key === "Escape") {
1561
+ e.preventDefault();
1562
+ close(true);
1563
+ } else if (e.key === "ArrowDown") {
1564
+ e.preventDefault();
1565
+ focusAt(i + 1);
1566
+ } else if (e.key === "ArrowUp") {
1567
+ e.preventDefault();
1568
+ focusAt(i - 1);
1569
+ } else if (e.key === "Home") {
1570
+ e.preventDefault();
1571
+ focusAt(0);
1572
+ } else if (e.key === "End") {
1573
+ e.preventDefault();
1574
+ focusAt(items.length - 1);
1575
+ } else if (e.key === "Tab") close(false);
1576
+ };
1577
+ const onDown = (e) => {
1578
+ const path = e.composedPath?.() ?? [];
1579
+ if (!path.includes(host) && !path.includes(this)) close(false);
1580
+ };
1581
+ const onMove = () => close(false);
1582
+ menu.addEventListener("keydown", onKey);
1583
+ document.addEventListener("pointerdown", onDown, true);
1584
+ window.addEventListener("resize", onMove);
1585
+ window.addEventListener("scroll", place, true);
1586
+ const close = (focusChip = false) => {
1587
+ if (this.#menu?.host !== host) return;
1588
+ this.#menu = null;
1589
+ menu.removeEventListener("keydown", onKey);
1590
+ document.removeEventListener("pointerdown", onDown, true);
1591
+ window.removeEventListener("resize", onMove);
1592
+ window.removeEventListener("scroll", place, true);
1593
+ host.remove();
1594
+ this.button?.setAttribute("aria-expanded", "false");
1595
+ if (focusChip) this.button?.focus();
1596
+ };
1597
+ this.#menu = { host, close };
1598
+ chip.setAttribute("aria-expanded", "true");
1599
+ focusAt(0);
1600
+ }
1601
+ /**
1602
+ * Disconnect: emits bwalletx-disconnect (cancelable). Unless a listener cancels it (BwalletXProvider signs out
1603
+ * itself) or the element is driven by `state`, posts logout-url and shows signed out.
1604
+ */
1605
+ async signOut() {
1606
+ this.#menu?.close(false);
1607
+ if (!this.#emit("bwalletx-disconnect", { element: this, account: this.account }, true)) return;
1608
+ if (this.#controlled) return;
1609
+ const url = this.#attr("logout-url");
1610
+ if (url) await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: "{}" }).catch(() => void 0);
1611
+ this.#account = null;
1612
+ this.#set("signed-out");
1613
+ this.#emit("bwalletx-signed-out");
1614
+ this.button?.focus();
894
1615
  }
895
1616
  #emit(type, detail, cancelable = false) {
896
1617
  return this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable }));
897
1618
  }
898
- #setBusy(on) {
899
- this.#busy = on;
900
- this.#render();
1619
+ /** "Other wallets" is offered under the button: other-wallets on, or auto at lg/md when this element signs in. */
1620
+ #otherWalletsShown(size) {
1621
+ const a = this.#attr("other-wallets");
1622
+ if (a === "off") return false;
1623
+ if (a === "on") return true;
1624
+ return (size === "lg" || size === "md") && (this.#controlled || !!this.#attr("challenge-url"));
901
1625
  }
902
- async #click() {
903
- if (this.hasAttribute("disabled") || this.hasAttribute("loading") || this.#busy) return;
904
- if (!this.#emit("bwalletx-signin", { element: this }, true)) return;
905
- const href = this.#attr("href");
906
- const authorizeUrl = this.#attr("authorize-url");
907
- if (authorizeUrl) {
908
- location.assign(await buildAuthorizeUrl({ authorizeUrl, clientId: this.#attr("client-id"), redirectUri: this.#attr("redirect-uri"), scope: this.#attr("scope") }));
909
- return;
1626
+ /** Open the wallet chooser (the "Other wallets" link): phone QR, other browser wallets, desktop wallets. */
1627
+ otherWallets() {
1628
+ return this.#click("chooser");
1629
+ }
1630
+ #attempt(option, outcome, started) {
1631
+ this.#emit("bwalletx-attempt", attemptFor(option, outcome, started));
1632
+ }
1633
+ /**
1634
+ * Which wallet: bWalletX by its own marker goes straight to its sheet; otherwise the chooser ("Get bWalletX" first)
1635
+ * when another wallet is here or the user asked; otherwise 'not-found'. null: the chooser was closed.
1636
+ */
1637
+ async #choose(mode, ms, pairing) {
1638
+ if (mode !== "chooser") {
1639
+ const f = await discoverBwalletX(ms);
1640
+ if (f) {
1641
+ const substrate = f.rdns === RDNS_EXTENSION ? "extension" : f.rdns === RDNS_MOBILE ? "in-app" : "xdm";
1642
+ return brc100Option(f.wallet, { id: f.rdns ?? "bwalletx-xdm", rdns: f.rdns ?? void 0, name: "bWalletX", isBwalletX: true, substrate });
1643
+ }
910
1644
  }
911
- if (href) {
912
- location.assign(href);
913
- return;
1645
+ const list = await discoverWallets({ ms: 50, pairing: () => pairing });
1646
+ if (mode !== "chooser" && !hasBwalletX(list) && !otherDetected(list).length) return "not-found";
1647
+ return chooseWallet(list, { getUrl: this.#attr("get-url") ?? void 0 });
1648
+ }
1649
+ async #click(mode = "signin") {
1650
+ if (this.hasAttribute("disabled") || this.hasAttribute("loading") || this.#busy || this.state === "pending") return;
1651
+ if (!this.#emit("bwalletx-signin", { element: this, mode }, true)) return;
1652
+ if (mode === "signin") {
1653
+ const href = this.#attr("href");
1654
+ const authorizeUrl = this.#attr("authorize-url");
1655
+ if (authorizeUrl) {
1656
+ location.assign(await buildAuthorizeUrl({ authorizeUrl, clientId: this.#attr("client-id"), redirectUri: this.#attr("redirect-uri"), scope: this.#attr("scope") }));
1657
+ return;
1658
+ }
1659
+ if (href) {
1660
+ location.assign(href);
1661
+ return;
1662
+ }
914
1663
  }
915
1664
  const challengeUrl = this.#attr("challenge-url");
916
1665
  if (!challengeUrl) return;
1666
+ const own = this.#stateful && !this.#controlled;
1667
+ const prev = this.#phase === "signed-in" ? "signed-in" : "signed-out";
1668
+ const started = Date.now();
1669
+ const pairing = pairingModal();
1670
+ let option = null;
917
1671
  this.#notFound = false;
918
- this.#setBusy(true);
1672
+ this.#busy = true;
1673
+ this.#pendingLabel = PENDING_LABEL;
1674
+ if (own) this.#set("pending");
1675
+ else this.#render();
919
1676
  try {
920
- const ms = Number(this.#attr("discover-ms")) || 1200;
921
- const found = await discoverBwalletX(ms);
922
- if (!found) {
1677
+ const ms = Number(this.#attr("discover-ms")) || DEFAULT_DISCOVER_MS;
1678
+ const picked = await this.#choose(mode, ms, pairing);
1679
+ if (picked === "not-found") {
923
1680
  this.#notFound = true;
1681
+ if (own) this.#phase = "signed-out";
1682
+ this.#attempt(null, "not-found", started);
924
1683
  this.#emit("bwalletx-not-found");
925
1684
  if (this.#attr("require-token")) this.#emit("bwalletx-access-denied", { code: "not_installed", message: "No wallet found" });
926
1685
  return;
927
1686
  }
1687
+ if (!picked) {
1688
+ if (own) this.#phase = prev;
1689
+ this.#attempt(null, "cancelled", started);
1690
+ return;
1691
+ }
1692
+ option = picked;
1693
+ if (!option.isBwalletX) {
1694
+ this.#pendingLabel = PENDING_LABEL_OTHER;
1695
+ if (own) this.#render();
1696
+ }
1697
+ const cw = await option.connect({ identity: false });
1698
+ const wallet = cw.wallet;
1699
+ if (!wallet) throw Object.assign(new Error("This wallet cannot sign in here"), { code: "NOT_SUPPORTED" });
1700
+ const sub = substrateOf(option);
1701
+ const method = sub === "extension" ? "extension" : sub === "in-app" || sub === "xdm" ? "in-app" : sub === "pairing" ? "pairing" : "wallet";
928
1702
  const requireToken = this.#attr("require-token");
929
1703
  if (requireToken) {
1704
+ if (own) this.#phase = "signed-out";
930
1705
  const verifyUrl2 = this.#attr("verify-url");
931
1706
  const min = this.#attr("min");
932
1707
  const minBps = this.#attr("min-bps");
933
1708
  const hint = { token: { id: requireToken, ...minBps ? { minBps: Number(minBps) } : { min: min ?? "1" } } };
934
1709
  try {
935
1710
  const a = await requestAccess({
936
- wallet: found.wallet,
1711
+ wallet,
937
1712
  challenge: (identityKey) => postJson(challengeUrl, { identityKey, require: hint }),
938
1713
  verify: verifyUrl2 ? (response2) => postAccess(verifyUrl2, response2) : void 0
939
1714
  });
1715
+ this.#attempt(option, "signed-in", started);
940
1716
  this.#emit("bwalletx-access-granted", { identityKey: a.response.identityKey, response: a.verified });
941
1717
  } catch (e) {
942
1718
  const err = e instanceof AccessError ? e : new AccessError("bad_request", String(e));
1719
+ this.#attempt(option, outcomeOf(e), started);
943
1720
  this.#emit("bwalletx-access-denied", { code: err.code, message: err.message, have: err.have, need: err.need });
944
1721
  }
945
1722
  return;
946
1723
  }
947
- const r = await signInWithBwalletX({ wallet: found.wallet, challenge: (identityKey) => postJson(challengeUrl, { identityKey }) });
948
1724
  const verifyUrl = this.#attr("verify-url");
949
- const response = verifyUrl ? await postJson(verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature }) : null;
950
- this.#emit("bwalletx-signed-in", { identityKey: r.identityKey, method: found.method, nonce: r.nonce, signature: r.signature, response });
1725
+ const { result: r, verified } = await signInAndVerify({
1726
+ wallet,
1727
+ identityKeyHint: verifyUrl ? rememberedIdentity() : void 0,
1728
+ challenge: (identityKey) => postJson(challengeUrl, identityKey ? { identityKey } : {}),
1729
+ verify: async (res) => verifyUrl ? await postJson(verifyUrl, { identityKey: res.identityKey, nonce: res.nonce, signature: res.signature }) : {}
1730
+ });
1731
+ if (verifyUrl) rememberIdentity(r.identityKey);
1732
+ const response = verifyUrl ? verified : null;
1733
+ if (own) this.#signedIn({ identityKey: r.identityKey, handle: response?.handle ?? null, paymail: response?.paymail ?? null });
1734
+ this.#attempt(option, "signed-in", started);
1735
+ this.#emit("bwalletx-signed-in", { identityKey: r.identityKey, method, nonce: r.nonce, signature: r.signature, response });
951
1736
  } catch (error) {
952
- this.#emit("bwalletx-error", { error });
1737
+ const outcome = outcomeOf(error);
1738
+ this.#attempt(option, outcome, started);
1739
+ const closedQr = outcome === "cancelled" && option?.kind === "pairing";
1740
+ if (own) {
1741
+ if (closedQr) this.#phase = prev;
1742
+ else {
1743
+ this.#error = signInErrorText(error);
1744
+ this.#phase = "error";
1745
+ }
1746
+ }
1747
+ if (!closedQr) this.#emit("bwalletx-error", { error });
953
1748
  } finally {
954
- this.#setBusy(false);
1749
+ this.#busy = false;
1750
+ try {
1751
+ pairing.close();
1752
+ } catch {
1753
+ }
1754
+ this.#render();
955
1755
  }
956
1756
  }
957
1757
  };
@@ -983,7 +1783,21 @@ a.bwx-btn{display:block;text-align:center;text-decoration:none;box-sizing:border
983
1783
  .bwx-list{display:flex;flex-direction:column;gap:8px}
984
1784
  .bwx-opt{display:flex;align-items:center;gap:10px;text-align:left}
985
1785
  .bwx-opt img{width:24px;height:24px;border-radius:6px}
986
- .bwx-tag{margin-left:auto;font-size:11px;color:#8d866f}
1786
+ .bwx-tag{margin-left:auto;font-size:11px;color:#8d866f;white-space:nowrap}
1787
+ .bwx-opt{min-height:48px;box-sizing:border-box}
1788
+ a.bwx-btn.bwx-opt{display:flex;text-align:left}
1789
+ .bwx-btn:focus{outline:none}
1790
+ .bwx-btn:focus-visible{outline:2px solid #e8c766;outline-offset:2px}
1791
+ .bwx-mk{display:inline-flex;align-items:center;justify-content:center;flex:none;width:28px;height:28px;border-radius:50%;background:#000}
1792
+ .bwx-ico{display:inline-grid;place-items:center;flex:none;width:28px;height:28px;border-radius:8px;background:#2a2518;color:#e8c766;font-weight:700;font-size:13px}
1793
+ .bwx-opt img{width:28px;height:28px;border-radius:8px;flex:none}
1794
+ .bwx-txt{display:flex;flex-direction:column;min-width:0;gap:1px}
1795
+ .bwx-txt b{font-weight:600}
1796
+ .bwx-txt small{font-size:12px;color:#8d866f;font-weight:400}
1797
+ .bwx-primary .bwx-txt small{color:rgba(0,0,0,.68)}
1798
+ .bwx-primary .bwx-tag{color:rgba(0,0,0,.6)}
1799
+ .bwx-sep{font-size:12px;color:#8d866f;margin:6px 2px 0}
1800
+ .bwx-foot{font-size:12px;color:#8d866f;margin:12px 0 0}
987
1801
  `;
988
1802
  function mount() {
989
1803
  const root = document.createElement("div");
@@ -1112,105 +1926,94 @@ function pickWallet(options, o = {}) {
1112
1926
  ui.box.lastChild.append(cancel);
1113
1927
  });
1114
1928
  }
1115
-
1116
- // src/wallets.ts
1117
- var import_sdk5 = require("@bsv/sdk");
1118
- async function brc100Connected(wallet, o) {
1119
- await wallet.waitForAuthentication({}).catch(() => {
1120
- });
1121
- const { publicKey } = await wallet.getPublicKey({ identityKey: true });
1122
- return { wallet, payer: wallet, identityKey: publicKey, optionId: o.id, kind: o.kind };
1123
- }
1124
- function brc100Option(wallet, info) {
1125
- const opt = {
1126
- id: info.id,
1127
- name: info.name,
1128
- icon: info.icon,
1129
- rdns: info.rdns,
1130
- kind: "brc100",
1131
- isBwalletX: info.isBwalletX ?? false,
1132
- connect: () => brc100Connected(wallet, opt)
1133
- };
1134
- return opt;
1135
- }
1136
- function pairingOption(pairing) {
1137
- const opt = {
1138
- id: "bwalletx-pairing",
1139
- name: "bWalletX on your phone",
1140
- kind: "pairing",
1141
- isBwalletX: true,
1142
- async connect() {
1143
- const p = typeof pairing === "function" ? pairing() : pairing;
1144
- const w = await restorePairing(p) ?? await pairBwalletX(p);
1145
- return brc100Connected(w, opt);
1146
- }
1147
- };
1148
- return opt;
1149
- }
1150
- function handcashAdapter(o) {
1151
- const opt = {
1152
- id: "handcash",
1153
- name: "HandCash",
1154
- icon: o.icon,
1155
- kind: "handcash",
1156
- isBwalletX: false,
1157
- async connect() {
1158
- const p = await o.login();
1159
- return {
1160
- wallet: null,
1161
- payer: o.pay ? { pay: o.pay } : null,
1162
- identityKey: p.identityKey ?? null,
1163
- handle: p.handle,
1164
- paymail: p.paymail ?? `${p.handle.replace(/^\$/, "")}@handcash.io`,
1165
- optionId: opt.id,
1166
- kind: "handcash",
1167
- transitional: true
1168
- };
1169
- }
1170
- };
1171
- return opt;
1172
- }
1173
- var NAMES = { [RDNS_EXTENSION]: "bWalletX", "space.bwallet.mobile": "bWalletX" };
1174
- async function probe(substrate, ms) {
1175
- try {
1176
- const w = new import_sdk5.WalletClient(substrate);
1177
- const ok = await Promise.race([w.isAuthenticated({}).then(() => true), new Promise((r) => setTimeout(() => r(false), ms))]);
1178
- return ok ? w : null;
1179
- } catch {
1180
- return null;
1929
+ function optionRow(w, primary, sub, tag) {
1930
+ const b = el("button", `bwx-btn bwx-opt${primary ? " bwx-primary" : ""}`);
1931
+ b.type = "button";
1932
+ b.dataset.wallet = w.id;
1933
+ if (w.isBwalletX) {
1934
+ const mk = el("span", "bwx-mk");
1935
+ mk.innerHTML = markSvg(18);
1936
+ b.append(mk);
1937
+ } else if (w.icon) {
1938
+ const img = el("img");
1939
+ img.src = w.icon;
1940
+ img.alt = "";
1941
+ b.append(img);
1942
+ } else {
1943
+ b.append(el("span", "bwx-ico", (w.name.trim().charAt(0) || "W").toUpperCase()));
1181
1944
  }
1945
+ const t = el("span", "bwx-txt");
1946
+ t.append(el("b", "", w.name));
1947
+ if (sub) t.append(el("small", "", sub));
1948
+ b.append(t);
1949
+ if (tag) b.append(el("span", "bwx-tag", tag));
1950
+ return b;
1182
1951
  }
1183
- async function discoverWallets(o = {}) {
1184
- const out = [];
1185
- const ms = o.ms ?? 1200;
1186
- if (typeof window !== "undefined") {
1187
- const seen = /* @__PURE__ */ new Map();
1188
- const onAnnounce = (e) => {
1189
- const d = e.detail;
1190
- if (!d?.wallet) return;
1191
- const rdns = d.info?.rdns ?? d.info?.uuid ?? `wallet-${seen.size}`;
1192
- if (seen.has(rdns)) return;
1193
- const isB = BWALLETX_RDNS.includes(rdns);
1194
- seen.set(rdns, brc100Option(d.wallet, { id: rdns, rdns, name: NAMES[rdns] ?? d.info?.name ?? rdns, icon: d.info?.icon, isBwalletX: isB }));
1952
+ function chooseWallet(options, o = {}) {
1953
+ if (typeof document === "undefined") return Promise.resolve(null);
1954
+ return new Promise((resolve) => {
1955
+ const ui = mount();
1956
+ ui.root.dataset.bwxChooser = "";
1957
+ const done = (w) => {
1958
+ document.removeEventListener("keydown", onKey, true);
1959
+ ui.close();
1960
+ resolve(w);
1195
1961
  };
1196
- window.addEventListener("brc100:announceWallet", onAnnounce);
1197
- window.dispatchEvent(new Event("brc100:requestWallet"));
1198
- const local = o.probeLocal ? Promise.all([probe("json-api", ms), probe("Cicada", ms)]) : Promise.resolve([null, null]);
1199
- await new Promise((r) => setTimeout(r, ms));
1200
- window.removeEventListener("brc100:announceWallet", onAnnounce);
1201
- out.push(...seen.values());
1202
- const cwi = window.CWI;
1203
- if (cwi && ![...seen.values()].some((x) => x.rdns === "space.bwallet.mobile")) {
1204
- out.push(brc100Option(cwi, { id: "window.CWI", name: "This app\u2019s wallet", isBwalletX: false }));
1962
+ const onKey = (e) => {
1963
+ if (e.key === "Escape") {
1964
+ e.preventDefault();
1965
+ done(null);
1966
+ }
1967
+ };
1968
+ document.addEventListener("keydown", onKey, true);
1969
+ ui.box.append(el("h2", "", o.title ?? "Choose a wallet"));
1970
+ const list = el("div", "bwx-list");
1971
+ const here = hasBwalletX(options);
1972
+ if (!here) {
1973
+ const get = el("a", "bwx-btn bwx-opt bwx-primary");
1974
+ get.href = o.getUrl ?? GET_URL;
1975
+ get.target = "_blank";
1976
+ get.rel = "noopener";
1977
+ get.dataset.wallet = "get-bwalletx";
1978
+ const mk = el("span", "bwx-mk");
1979
+ mk.innerHTML = markSvg(18);
1980
+ const t = el("span", "bwx-txt");
1981
+ t.append(el("b", "", "Get bWalletX"), el("small", "", "Free. Every feature on this site"));
1982
+ get.append(mk, t);
1983
+ list.append(get);
1205
1984
  }
1206
- const [json, cicada] = await local;
1207
- if (json) out.push(brc100Option(json, { id: "localhost:3321", name: "Desktop wallet (localhost:3321)" }));
1208
- if (cicada) out.push(brc100Option(cicada, { id: "localhost:3301", name: "Desktop wallet (localhost:3301)" }));
1209
- }
1210
- if (o.pairing) out.push(pairingOption(o.pairing));
1211
- out.push(...o.extra ?? []);
1212
- const rank = (w) => w.isBwalletX ? 0 : w.kind === "handcash" ? 2 : 1;
1213
- return out.map((w, i) => ({ w, i })).sort((a, b) => rank(a.w) - rank(b.w) || a.i - b.i).map((x) => x.w);
1985
+ const ordered = chooserOrder(options);
1986
+ const touch = isTouchDevice();
1987
+ let sep = false;
1988
+ for (const w of ordered) {
1989
+ if (!here && !sep) {
1990
+ list.append(el("p", "bwx-sep", "Or use"));
1991
+ sep = true;
1992
+ }
1993
+ const sub = isPresentBwalletX(w) ? "Recommended" : w.kind === "pairing" ? touch ? "Open the bWalletX app" : "Scan a QR with your phone" : void 0;
1994
+ const tag = w.kind === "handcash" ? "until BRC-100" : !w.isBwalletX && w.kind === "brc100" ? "Fewer features" : void 0;
1995
+ const row2 = optionRow(w, isPresentBwalletX(w), sub, tag);
1996
+ row2.onclick = () => done(w);
1997
+ list.append(row2);
1998
+ }
1999
+ if (o.desktop !== false && !options.some((w) => w.substrate === "json-api" || w.substrate === "cicada")) {
2000
+ if (!here && !sep) list.append(el("p", "bwx-sep", "Or use"));
2001
+ const d = desktopOption();
2002
+ const row2 = optionRow(d, false, "Metanet or BSV Desktop on this computer", "Fewer features");
2003
+ row2.onclick = () => done(d);
2004
+ list.append(row2);
2005
+ }
2006
+ ui.box.append(list);
2007
+ if (otherDetected(options).length || !here) ui.box.append(el("p", "bwx-foot", "Some features need bWalletX."));
2008
+ const row = el("div", "bwx-row");
2009
+ const cancel = el("button", "bwx-btn", "Cancel");
2010
+ cancel.type = "button";
2011
+ cancel.onclick = () => done(null);
2012
+ row.append(cancel);
2013
+ ui.box.append(row);
2014
+ ui.root.onclick = (e) => e.target === ui.root && done(null);
2015
+ list.querySelector("a,button")?.focus();
2016
+ });
1214
2017
  }
1215
2018
 
1216
2019
  // src/pay.ts
@@ -1293,11 +2096,11 @@ async function resolvePaymailP2P(paymail, sats, f = fetch) {
1293
2096
  return {
1294
2097
  outputs: body.outputs.map((o) => ({ script: o.script, satoshis: o.satoshis })),
1295
2098
  reference: body.reference,
1296
- submit: receive ? async (hex) => {
2099
+ submit: receive ? async (hex2) => {
1297
2100
  await f(fill(receive), {
1298
2101
  method: "POST",
1299
2102
  headers: { "content-type": "application/json" },
1300
- body: JSON.stringify({ hex, reference: body.reference, metadata: {} })
2103
+ body: JSON.stringify({ hex: hex2, reference: body.reference, metadata: {} })
1301
2104
  });
1302
2105
  } : void 0
1303
2106
  };
@@ -1375,19 +2178,52 @@ async function buyToken(source, req) {
1375
2178
  }
1376
2179
 
1377
2180
  // src/react.ts
2181
+ function signinStateFor(v) {
2182
+ if (v.status === "restoring") return "restoring";
2183
+ if (v.status === "connecting") return "pending";
2184
+ if (v.status === "error") return "error";
2185
+ if (v.status === "connected" && v.identityKey) return "signed-in";
2186
+ return "signed-out";
2187
+ }
1378
2188
  function BwalletxSignin(props) {
1379
2189
  const ref = (0, import_react.useRef)(null);
1380
2190
  const cb = (0, import_react.useRef)(props);
1381
2191
  cb.current = props;
2192
+ const ctx = (0, import_react.useContext)(Ctx);
2193
+ const ctxRef = (0, import_react.useRef)(ctx);
2194
+ ctxRef.current = ctx;
2195
+ const driven = !!ctx && props.stateful !== false;
1382
2196
  (0, import_react.useEffect)(() => {
1383
2197
  defineBwalletxSignin();
1384
2198
  const el2 = ref.current;
1385
2199
  if (!el2) return;
1386
2200
  const on = [
1387
- ["bwalletx-signin", (e) => cb.current.onClick?.(e)],
2201
+ [
2202
+ "bwalletx-signin",
2203
+ (e) => {
2204
+ cb.current.onClick?.(e);
2205
+ const c = ctxRef.current;
2206
+ if (!e.defaultPrevented && c && cb.current.stateful !== false) {
2207
+ e.preventDefault();
2208
+ const mode = e.detail?.mode;
2209
+ c.connect(mode === "chooser" ? { chooser: true } : {}).catch(() => void 0);
2210
+ }
2211
+ }
2212
+ ],
1388
2213
  ["bwalletx-signed-in", (e) => cb.current.onSignedIn?.(e.detail)],
1389
2214
  ["bwalletx-not-found", () => cb.current.onNotFound?.()],
1390
- ["bwalletx-error", (e) => cb.current.onError?.(e.detail.error)]
2215
+ ["bwalletx-error", (e) => cb.current.onError?.(e.detail.error)],
2216
+ [
2217
+ "bwalletx-disconnect",
2218
+ (e) => {
2219
+ const c = ctxRef.current;
2220
+ if (c && cb.current.stateful !== false) {
2221
+ e.preventDefault();
2222
+ void c.disconnect().then(() => cb.current.onSignedOut?.());
2223
+ }
2224
+ }
2225
+ ],
2226
+ ["bwalletx-signed-out", () => cb.current.onSignedOut?.()]
1391
2227
  ];
1392
2228
  for (const [t, f] of on) el2.addEventListener(t, f);
1393
2229
  return () => {
@@ -1396,6 +2232,7 @@ function BwalletxSignin(props) {
1396
2232
  }, []);
1397
2233
  const p = props;
1398
2234
  const flag = (v) => v ? "" : void 0;
2235
+ const state = driven ? signinStateFor(ctx) : void 0;
1399
2236
  return (0, import_react.createElement)("bwalletx-signin", {
1400
2237
  ref,
1401
2238
  id: p.id,
@@ -1411,6 +2248,16 @@ function BwalletxSignin(props) {
1411
2248
  disabled: flag(p.disabled),
1412
2249
  "challenge-url": p.challengeUrl,
1413
2250
  "verify-url": p.verifyUrl,
2251
+ "session-url": driven ? void 0 : p.sessionUrl,
2252
+ "logout-url": driven ? void 0 : p.logoutUrl,
2253
+ "lookup-url": p.lookupUrl,
2254
+ stateless: p.stateful === false ? "" : void 0,
2255
+ state,
2256
+ "identity-key": driven ? ctx.identityKey ?? void 0 : void 0,
2257
+ paymail: driven ? ctx.paymail ?? void 0 : void 0,
2258
+ handle: driven ? ctx.handle ?? void 0 : void 0,
2259
+ "avatar-url": driven ? avatarUrlFor(ctx.identityKey) ?? void 0 : void 0,
2260
+ error: driven && ctx.status === "error" ? signInErrorText(ctx.error) : void 0,
1414
2261
  "get-url": p.getUrl,
1415
2262
  "web-url": p.webUrl,
1416
2263
  href: p.href,
@@ -1419,6 +2266,7 @@ function BwalletxSignin(props) {
1419
2266
  "redirect-uri": p.redirectUri,
1420
2267
  scope: p.scope,
1421
2268
  "discover-ms": p.discoverMs,
2269
+ "other-wallets": p.otherWallets,
1422
2270
  "aria-label": p["aria-label"]
1423
2271
  });
1424
2272
  }
@@ -1430,6 +2278,7 @@ var empty = {
1430
2278
  paymail: null,
1431
2279
  wallet: null,
1432
2280
  walletId: null,
2281
+ walletType: null,
1433
2282
  transitional: false,
1434
2283
  signedIn: false,
1435
2284
  error: null
@@ -1491,45 +2340,42 @@ function BwalletXProvider(p) {
1491
2340
  setState((s) => ({ ...s, status: "connecting", error: null }));
1492
2341
  try {
1493
2342
  const list = await listWallets();
1494
- let choice = opts.walletId ? list.find((w) => w.id === opts.walletId) : void 0;
1495
- if (!choice) {
1496
- const direct = list.filter((w) => w.kind !== "pairing");
1497
- choice = c.picker !== false && list.length > 1 ? await pickWallet(list) : direct[0] ?? list[0] ?? null;
1498
- }
1499
- if (!choice) {
1500
- if (!list.length) throw Object.assign(new Error("No wallet found. Get bWalletX at bwalletx.com."), { code: "NOT_FOUND" });
1501
- setState((s) => ({ ...s, status: s.identityKey ? "connected" : "idle" }));
2343
+ const fast = c.fastPath !== false;
2344
+ const r = await runConnect({
2345
+ list,
2346
+ walletId: opts.walletId,
2347
+ chooser: opts.chooser,
2348
+ picker: c.picker,
2349
+ fastPath: fast,
2350
+ pick: fast ? (l) => chooseWallet(l) : pickWallet,
2351
+ onAttempt: c.onAttempt,
2352
+ challengeUrl: c.challengeUrl,
2353
+ verifyUrl: c.verifyUrl,
2354
+ challengeNeedsIdentityKey: c.challengeNeedsIdentityKey,
2355
+ post: postJson2
2356
+ });
2357
+ if (!r) {
2358
+ setState((s) => ({ ...s, status: s.identityKey && s.signedIn ? "connected" : "idle" }));
1502
2359
  return null;
1503
2360
  }
1504
- const cw = await choice.connect();
1505
- let { handle = null, paymail = null } = cw;
1506
- let signedIn = false;
1507
- if (cw.wallet && c.challengeUrl && c.verifyUrl) {
1508
- const r = await signInWithBwalletX({
1509
- wallet: cw.wallet,
1510
- challenge: (identityKey) => postJson2(c.challengeUrl, { identityKey })
1511
- });
1512
- const v = await atStage("verify", () => postJson2(c.verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature }));
1513
- handle = v.handle ?? handle;
1514
- paymail = v.paymail ?? paymail;
1515
- signedIn = true;
1516
- }
2361
+ const cw = r.connected;
1517
2362
  payer.current = cw.payer;
1518
2363
  setState({
1519
2364
  status: "connected",
1520
- identityKey: cw.identityKey,
1521
- handle,
1522
- paymail,
2365
+ identityKey: r.identityKey,
2366
+ handle: r.handle,
2367
+ paymail: r.paymail,
1523
2368
  wallet: cw.wallet,
1524
2369
  walletId: cw.optionId,
2370
+ walletType: r.walletType,
1525
2371
  transitional: !!cw.transitional,
1526
- signedIn,
2372
+ signedIn: r.signedIn,
1527
2373
  error: null
1528
2374
  });
1529
- return cw;
2375
+ return { ...cw, identityKey: r.identityKey, handle: r.handle ?? void 0, paymail: r.paymail ?? void 0 };
1530
2376
  } catch (e) {
1531
2377
  if (e?.message === "cancelled") {
1532
- setState((s) => ({ ...s, status: s.identityKey ? "connected" : "idle" }));
2378
+ setState((s) => ({ ...s, status: s.identityKey && s.signedIn ? "connected" : "idle" }));
1533
2379
  return null;
1534
2380
  }
1535
2381
  throw fail(e);
@@ -1545,6 +2391,17 @@ function BwalletXProvider(p) {
1545
2391
  payer.current = null;
1546
2392
  setState(empty);
1547
2393
  }, [state.walletId]);
2394
+ (0, import_react.useEffect)(() => {
2395
+ const key = state.identityKey;
2396
+ if (!key || state.paymail || p.lookupUrl === "off") return;
2397
+ let live = true;
2398
+ void lookupPaymail(key, p.lookupUrl).then((pm) => {
2399
+ if (live && pm) setState((s) => s.identityKey === key && !s.paymail ? { ...s, paymail: pm } : s);
2400
+ });
2401
+ return () => {
2402
+ live = false;
2403
+ };
2404
+ }, [state.identityKey, state.paymail, p.lookupUrl]);
1548
2405
  const need = () => {
1549
2406
  if (!payer.current) throw new PaymentError("NOT_SUPPORTED", "Connect a wallet first (connect()).");
1550
2407
  return payer.current;
@@ -1569,6 +2426,24 @@ function useBwalletX() {
1569
2426
  if (!v) throw new Error("useBwalletX() must be used inside <BwalletXProvider>");
1570
2427
  return v;
1571
2428
  }
2429
+ function accountFor(v) {
2430
+ const status = signinStateFor(v);
2431
+ const signedIn = status === "signed-in";
2432
+ return {
2433
+ status,
2434
+ identityKey: signedIn ? v.identityKey : null,
2435
+ paymail: signedIn ? v.paymail : null,
2436
+ handle: signedIn ? v.handle : null,
2437
+ label: signedIn ? accountLabel(v) : null,
2438
+ avatarUrl: signedIn ? avatarUrlFor(v.identityKey) : null,
2439
+ error: status === "error" ? signInErrorText(v.error) : null,
2440
+ login: () => v.connect().then(() => void 0, () => void 0),
2441
+ logout: () => v.disconnect()
2442
+ };
2443
+ }
2444
+ function useBwalletXAccount() {
2445
+ return accountFor(useBwalletX());
2446
+ }
1572
2447
  var BwalletxProvider = BwalletXProvider;
1573
2448
  // Annotate the CommonJS export names for ESM import in node:
1574
2449
  0 && (module.exports = {
@@ -1576,10 +2451,14 @@ var BwalletxProvider = BwalletXProvider;
1576
2451
  BwalletxProvider,
1577
2452
  BwalletxSignin,
1578
2453
  PaymentError,
2454
+ accountFor,
2455
+ autoChoice,
1579
2456
  brc100Option,
1580
2457
  discoverWallets,
1581
2458
  handcashAdapter,
1582
2459
  pairingOption,
1583
- useBwalletX
2460
+ signinStateFor,
2461
+ useBwalletX,
2462
+ useBwalletXAccount
1584
2463
  });
1585
2464
  //# sourceMappingURL=react.cjs.map