@bwalletx/connect 0.5.0 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.cjs CHANGED
@@ -83,10 +83,22 @@ function pairUrl(link) {
83
83
  const q = new URLSearchParams({ v: link.v, r: link.r, c: link.c, k: link.k, o: link.o, e: String(link.e) });
84
84
  return `https://${PAIR_HOST}/pair?${q}`;
85
85
  }
86
- var relaySocketUrl = (relay, channel, role, expiry) => {
86
+ var newClaim = () => b64url(crypto.getRandomValues(new Uint8Array(16)));
87
+ var WALLET_CLAIM_INFO = "bwallet-pair-wallet-claim-v1";
88
+ var hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
89
+ async function walletClaimFor(channel, sitePub) {
90
+ const d = await crypto.subtle.digest("SHA-256", enc.encode(`${WALLET_CLAIM_INFO}|${channel}|${sitePub}`));
91
+ return b64url(new Uint8Array(d));
92
+ }
93
+ async function claimDigest(claim) {
94
+ return hex(new Uint8Array(await crypto.subtle.digest("SHA-256", enc.encode(claim))));
95
+ }
96
+ var relaySocketUrl = (relay, channel, role, expiry, claim, walletClaimHash) => {
87
97
  const local = /^(localhost|127\.0\.0\.1)(:\d+)?$/.test(relay);
88
98
  const q = new URLSearchParams({ role });
89
99
  if (expiry) q.set("e", String(expiry));
100
+ if (claim) q.set("claim", claim);
101
+ if (walletClaimHash && role === "site") q.set("wc", walletClaimHash);
90
102
  return `${local ? "ws" : "wss"}://${relay}/v1/c/${channel}?${q}`;
91
103
  };
92
104
  async function deriveSession(mine, theirs, channel) {
@@ -184,6 +196,7 @@ function parseStored(raw) {
184
196
  if (typeof p.s !== "string" || !/^[0-9a-f]{64}$/i.test(p.s)) return null;
185
197
  if (typeof p.k !== "string" || !/^0[23][0-9a-f]{64}$/i.test(p.k)) return null;
186
198
  if (!n(p.sent) || !n(p.lastSeen) || !n(p.pairedAt)) return null;
199
+ if (p.sc !== void 0 && (typeof p.sc !== "string" || !/^[A-Za-z0-9_-]{22,64}$/.test(p.sc))) return null;
187
200
  return p;
188
201
  } catch {
189
202
  return null;
@@ -241,7 +254,12 @@ var Link = class {
241
254
  await this.opening;
242
255
  }
243
256
  reopen() {
244
- const ws = this.socket(relaySocketUrl(this.p.r, this.p.c, "site", Math.floor(Date.now() / 1e3) + QR_LIFETIME_S - 10));
257
+ if (!this.p.sc) {
258
+ this.p = { ...this.p, sc: newClaim() };
259
+ this.persist();
260
+ }
261
+ const e = Math.floor(Date.now() / 1e3) + QR_LIFETIME_S - 10;
262
+ const ws = this.socket(relaySocketUrl(this.p.r, this.p.c, "site", e, this.p.sc));
245
263
  return new Promise((resolve, reject) => {
246
264
  ws.onopen = () => {
247
265
  this.ws = ws;
@@ -335,11 +353,13 @@ function pairBwalletX(opts) {
335
353
  const siteKey = import_sdk2.PrivateKey.fromRandom();
336
354
  const channel = newChannel();
337
355
  const expiry = Math.floor(Date.now() / 1e3) + QR_LIFETIME_S;
338
- return new Promise((resolve, reject) => {
356
+ const siteClaim = newClaim();
357
+ const sitePub = siteKey.toPublicKey().toString();
358
+ return walletClaimFor(channel, sitePub).then(claimDigest).then((wc) => new Promise((resolve, reject) => {
339
359
  if (opts.signal?.aborted) return reject(new Error("cancelled"));
340
360
  let ws;
341
361
  try {
342
- ws = socket(relaySocketUrl(relay, channel, "site", expiry));
362
+ ws = socket(relaySocketUrl(relay, channel, "site", expiry, siteClaim, wc));
343
363
  } catch (e) {
344
364
  return reject(e);
345
365
  }
@@ -378,7 +398,7 @@ function pairBwalletX(opts) {
378
398
  if (msg.t === "ready") {
379
399
  settled = true;
380
400
  clearTimeout(timer);
381
- const p = { v: 1, c: channel, r: relay, s: siteKey.toHex(), k: walletKey, ...sealer.counters, pairedAt: Date.now() };
401
+ const p = { v: 1, c: channel, r: relay, s: siteKey.toHex(), k: walletKey, ...sealer.counters, pairedAt: Date.now(), sc: siteClaim };
382
402
  await Promise.resolve(storage.setItem(key, JSON.stringify(p))).catch(() => {
383
403
  });
384
404
  const link = new Link(p, sealer, storage, key, socket, opts.callTimeoutMs ?? 12e4);
@@ -388,8 +408,8 @@ function pairBwalletX(opts) {
388
408
  fail(new Error("Pairing was declined in bWalletX"));
389
409
  }
390
410
  });
391
- opts.onLink(pairUrl({ v: PAIR_VERSION, r: relay, c: channel, k: siteKey.toPublicKey().toString(), o: origin, e: expiry }));
392
- });
411
+ opts.onLink(pairUrl({ v: PAIR_VERSION, r: relay, c: channel, k: sitePub, o: origin, e: expiry }));
412
+ }));
393
413
  }
394
414
  async function restorePairing(opts = {}) {
395
415
  const storage = opts.storage === void 0 ? defaultStorage() : opts.storage ?? memoryStorage();
@@ -407,17 +427,40 @@ async function clearPairing(opts = {}) {
407
427
  });
408
428
  }
409
429
 
430
+ // src/theme.ts
431
+ var WALLET_THEME_META = "bwalletx-theme";
432
+ var ID = /^[a-z0-9-]{1,32}$/;
433
+ function declareWalletTheme(theme) {
434
+ if (typeof document === "undefined") return;
435
+ const id = (theme ?? "").trim().toLowerCase();
436
+ let meta = document.head.querySelector(`meta[name="${WALLET_THEME_META}"]`);
437
+ if (!id || !ID.test(id)) {
438
+ meta?.remove();
439
+ return;
440
+ }
441
+ if (!meta) {
442
+ meta = document.createElement("meta");
443
+ meta.name = WALLET_THEME_META;
444
+ document.head.appendChild(meta);
445
+ }
446
+ if (meta.content !== id) meta.content = id;
447
+ }
448
+
410
449
  // src/discover.ts
450
+ var import_sdk3 = require("@bsv/sdk");
411
451
  var RDNS_EXTENSION = "com.bwalletx.extension";
412
452
  var RDNS_MOBILE = "space.bwallet.mobile";
413
453
  var BWALLETX_RDNS = [RDNS_EXTENSION, RDNS_MOBILE];
414
- function discoverBwalletX(ms = 1200) {
454
+ var DEFAULT_DISCOVER_MS = 300;
455
+ function discoverBwalletX(ms = DEFAULT_DISCOVER_MS) {
415
456
  if (typeof window === "undefined") return Promise.resolve(null);
416
457
  return new Promise((resolve) => {
417
458
  let done = false;
459
+ let timer;
418
460
  const finish = (r) => {
419
461
  if (done) return;
420
462
  done = true;
463
+ if (timer !== void 0) clearTimeout(timer);
421
464
  window.removeEventListener("brc100:announceWallet", onAnnounce);
422
465
  resolve(r);
423
466
  };
@@ -428,605 +471,340 @@ function discoverBwalletX(ms = 1200) {
428
471
  finish({ wallet: d.wallet, method: rdns === RDNS_EXTENSION ? "extension" : "in-app", rdns });
429
472
  }
430
473
  };
474
+ const cwiNow = () => {
475
+ const cwi = window.CWI;
476
+ if (cwi) finish({ wallet: cwi, method: "in-app", rdns: null });
477
+ return !!cwi;
478
+ };
431
479
  window.addEventListener("brc100:announceWallet", onAnnounce);
432
480
  window.dispatchEvent(new Event("brc100:requestWallet"));
433
- setTimeout(() => {
434
- const cwi = window.CWI;
435
- finish(cwi ? { wallet: cwi, method: "in-app", rdns: null } : null);
481
+ if (done || cwiNow()) return;
482
+ if (isFramed()) {
483
+ void probeXdm(Math.max(ms, 1200)).then((w) => finish(w ? { wallet: w, method: "in-app", rdns: null } : null));
484
+ return;
485
+ }
486
+ timer = setTimeout(() => {
487
+ timer = void 0;
488
+ if (!cwiNow()) finish(null);
436
489
  }, ms);
437
490
  });
438
491
  }
492
+ var isFramed = () => {
493
+ try {
494
+ return window.parent !== window;
495
+ } catch {
496
+ return true;
497
+ }
498
+ };
499
+ var BWALLETX_PARENT_ORIGINS = [
500
+ "https://web.bwalletx.com",
501
+ "https://desktop.bwalletx.com",
502
+ "https://bwalletx.com",
503
+ "https://www.bwallet.space",
504
+ "https://bwallet.space",
505
+ "capacitor://localhost",
506
+ // iOS app shell
507
+ "https://localhost",
508
+ // Android app shell
509
+ "tauri://localhost",
510
+ // bWalletX Desktop (macOS)
511
+ "https://tauri.localhost",
512
+ // bWalletX Desktop (Windows)
513
+ "chrome-extension://bombmmcjghjdlbmcclnhjodgcelnehgl"
514
+ // bWalletX for Chrome (fixed id)
515
+ ];
516
+ var parentOrigin = () => {
517
+ try {
518
+ const anc = window.location.ancestorOrigins;
519
+ if (anc && anc.length > 0) return anc[0] ?? null;
520
+ if (!document.referrer) return null;
521
+ const u = new URL(document.referrer);
522
+ return `${u.protocol}//${u.host}`;
523
+ } catch {
524
+ return null;
525
+ }
526
+ };
527
+ async function probeXdm(ms = 1200, allowed = BWALLETX_PARENT_ORIGINS) {
528
+ const origin = parentOrigin();
529
+ if (!origin || !allowed.includes(origin)) return null;
530
+ try {
531
+ const w = new import_sdk3.WalletClient(new import_sdk3.XDM(origin));
532
+ const ok = await Promise.race([
533
+ w.isAuthenticated({}).then(
534
+ () => true,
535
+ () => false
536
+ ),
537
+ new Promise((r) => setTimeout(() => r(false), ms))
538
+ ]);
539
+ return ok ? w : null;
540
+ } catch {
541
+ return null;
542
+ }
543
+ }
544
+ var warmedAt = 0;
545
+ function prewarmBwalletX() {
546
+ if (typeof window === "undefined" || Date.now() - warmedAt < 2e4) return;
547
+ warmedAt = Date.now();
548
+ void discoverBwalletX(0).then((f) => {
549
+ if (f && f.method === "extension") void f.wallet.isAuthenticated({}).catch(() => {
550
+ });
551
+ });
552
+ }
553
+ function bindPrewarm(el2) {
554
+ const on = () => prewarmBwalletX();
555
+ const evs = ["pointerenter", "focus", "touchstart"];
556
+ for (const e of evs) el2.addEventListener(e, on, { passive: true });
557
+ return () => evs.forEach((e) => el2.removeEventListener(e, on));
558
+ }
439
559
 
440
560
  // src/signin.ts
441
561
  var LOGIN_PROTOCOL = [2, "bwallet sign in"];
442
562
  async function connectBwalletX(opts = {}) {
443
- if (opts.wallet) return { wallet: opts.wallet, method: "wallet" };
444
- const found = await discoverBwalletX(opts.discoverMs);
445
- if (found) return { wallet: found.wallet, method: found.method };
563
+ const status = (s) => {
564
+ try {
565
+ opts.onStatus?.(s);
566
+ } catch {
567
+ }
568
+ };
569
+ if (opts.wallet) {
570
+ status("opening");
571
+ return { wallet: opts.wallet, method: "wallet" };
572
+ }
573
+ status("finding");
574
+ const found = await discoverBwalletX(opts.discoverMs ?? DEFAULT_DISCOVER_MS);
575
+ if (found) {
576
+ status("opening");
577
+ return { wallet: found.wallet, method: found.method };
578
+ }
446
579
  if (opts.reusePairing !== false) {
447
580
  const restored = await restorePairing(opts.pairing ?? {});
448
- if (restored) return { wallet: restored, method: "pairing" };
581
+ if (restored) {
582
+ status("opening");
583
+ return { wallet: restored, method: "pairing" };
584
+ }
449
585
  }
450
586
  if (!opts.pairing) {
451
587
  throw Object.assign(new Error("bWalletX was not found in this browser"), { code: "NOT_FOUND" });
452
588
  }
589
+ status("pairing");
453
590
  return { wallet: await pairBwalletX(opts.pairing), method: "pairing" };
454
591
  }
455
592
  async function signInWithBwalletX(opts) {
456
593
  const { wallet, method } = await connectBwalletX(opts);
457
594
  if (method !== "pairing") await wallet.waitForAuthentication({}).catch(() => {
458
595
  });
459
- const { publicKey: identityKey } = await wallet.getPublicKey({ identityKey: true });
460
- const c = await opts.challenge(identityKey);
461
- const { signature } = await wallet.createSignature({
462
- protocolID: c.protocolID ?? LOGIN_PROTOCOL,
463
- keyID: c.keyID,
464
- counterparty: "anyone",
465
- data: Array.from(new TextEncoder().encode(c.message))
466
- });
596
+ const { publicKey: identityKey } = await atStage("identity", () => wallet.getPublicKey({ identityKey: true }));
597
+ try {
598
+ opts.onStatus?.("signing");
599
+ } catch {
600
+ }
601
+ const c = await atStage("challenge", () => opts.challenge(identityKey));
602
+ const { signature } = await atStage(
603
+ "signature",
604
+ () => wallet.createSignature({
605
+ protocolID: c.protocolID ?? LOGIN_PROTOCOL,
606
+ keyID: c.keyID,
607
+ counterparty: "anyone",
608
+ data: Array.from(new TextEncoder().encode(c.message))
609
+ })
610
+ );
467
611
  return { identityKey, signature: Array.from(signature), nonce: c.nonce, method, wallet };
468
612
  }
613
+ async function atStage(stage, run) {
614
+ try {
615
+ return await run();
616
+ } catch (e) {
617
+ const err = e instanceof Error ? e : new Error(String(e));
618
+ if (!err.stage) Object.assign(err, { stage });
619
+ throw err;
620
+ }
621
+ }
469
622
 
470
623
  // src/modal.ts
471
624
  var import_qrcode_generator = __toESM(require("qrcode-generator"), 1);
472
- var CSS = `
473
- .bwx-ov{position:fixed;inset:0;z-index:2147483000;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;padding:16px;font:15px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}
474
- .bwx-box{background:#0b0b0b;color:#f4f1e8;border:1px solid #3a3220;border-radius:16px;width:min(360px,100%);padding:20px;box-shadow:0 20px 60px rgba(0,0,0,.5)}
475
- .bwx-box h2{margin:0 0 6px;font-size:18px;color:#e8c766}
476
- .bwx-box p{margin:0 0 14px;color:#bdb6a3;font-size:14px}
477
- .bwx-qr{background:#fff;border-radius:12px;padding:12px;display:flex;justify-content:center}
478
- .bwx-qr svg{width:100%;height:auto;max-width:260px}
479
- .bwx-code{margin:14px 0 0;text-align:center;font-size:28px;letter-spacing:.3em;color:#e8c766;font-variant-numeric:tabular-nums}
480
- .bwx-row{display:flex;gap:8px;margin-top:14px}
481
- .bwx-btn{flex:1;border:1px solid #3a3220;background:#15130e;color:#f4f1e8;border-radius:10px;padding:10px 12px;font:inherit;cursor:pointer}
482
- .bwx-btn:hover{border-color:#e8c766}
483
- .bwx-list{display:flex;flex-direction:column;gap:8px}
484
- .bwx-opt{display:flex;align-items:center;gap:10px;text-align:left}
485
- .bwx-opt img{width:24px;height:24px;border-radius:6px}
486
- .bwx-tag{margin-left:auto;font-size:11px;color:#8d866f}
487
- `;
488
- function mount() {
489
- const root = document.createElement("div");
490
- root.className = "bwx-ov";
491
- root.setAttribute("role", "dialog");
492
- root.setAttribute("aria-modal", "true");
493
- const style = document.createElement("style");
494
- style.textContent = CSS;
495
- const box = document.createElement("div");
496
- box.className = "bwx-box";
497
- root.append(style, box);
498
- document.body.appendChild(root);
499
- return { root, box, close: () => root.remove() };
500
- }
501
- function el(tag, cls, text) {
502
- const e = document.createElement(tag);
503
- if (cls) e.className = cls;
504
- if (text != null) e.textContent = text;
505
- return e;
625
+
626
+ // src/access-client.ts
627
+ var import_sdk4 = require("@bsv/sdk");
628
+
629
+ // src/access.ts
630
+ var ACCESS_PROTOCOL = [2, "bwalletx access"];
631
+ var MAX_HOLDING_PROOFS = 50;
632
+ var AccessError = class extends Error {
633
+ code;
634
+ have;
635
+ need;
636
+ constructor(code, message, extra = {}) {
637
+ super(message ?? code);
638
+ this.name = "AccessError";
639
+ this.code = code;
640
+ this.have = extra.have;
641
+ this.need = extra.need;
642
+ }
643
+ toJSON() {
644
+ const b = { error: this.code, message: this.message };
645
+ if (this.have !== void 0) b.have = this.have;
646
+ if (this.need !== void 0) b.need = this.need;
647
+ return b;
648
+ }
649
+ };
650
+ function canonicalJson(v) {
651
+ if (v === null) return "null";
652
+ if (typeof v === "string" || typeof v === "boolean") return JSON.stringify(v);
653
+ if (typeof v === "number") {
654
+ if (!Number.isFinite(v)) throw new Error("canonicalJson: non-finite number");
655
+ return JSON.stringify(v);
656
+ }
657
+ if (Array.isArray(v)) return `[${v.map((x) => canonicalJson(x)).join(",")}]`;
658
+ if (typeof v === "object") {
659
+ const o = v;
660
+ const keys = Object.keys(o).filter((k) => o[k] !== void 0).sort();
661
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
662
+ }
663
+ throw new Error(`canonicalJson: unsupported ${typeof v}`);
506
664
  }
507
- function qrSvg(text) {
508
- const q = (0, import_qrcode_generator.default)(0, "M");
509
- q.addData(text);
510
- q.make();
511
- return q.createSvgTag({ cellSize: 4, margin: 2, scalable: true });
665
+ function challengeBytes(challenge) {
666
+ return Array.from(new TextEncoder().encode(challenge));
512
667
  }
513
- function pairingModal(o = {}) {
514
- const ac = new AbortController();
515
- let ui = null;
516
- let codeEl = null;
517
- const close = () => {
518
- ui?.close();
519
- ui = null;
520
- };
521
- ac.signal.addEventListener("abort", close);
522
- return {
523
- signal: ac.signal,
524
- close,
525
- onLink(link) {
526
- if (typeof document === "undefined") return;
527
- close();
528
- ui = mount();
529
- const h = el("h2", "", o.title ?? "Connect bWalletX");
530
- const p = el("p", "", "Scan with bWalletX on your phone, or paste the link into web.bwalletx.com.");
531
- const qr = el("div", "bwx-qr");
532
- qr.innerHTML = qrSvg(link);
533
- codeEl = el("div", "bwx-code", "");
534
- codeEl.setAttribute("aria-live", "polite");
535
- const row = el("div", "bwx-row");
536
- const copy = el("button", "bwx-btn", "Copy link");
537
- copy.onclick = () => void navigator.clipboard?.writeText(link).then(() => copy.textContent = "Copied");
538
- const cancel = el("button", "bwx-btn", "Cancel");
539
- cancel.onclick = () => {
540
- o.onCancel?.();
541
- ac.abort();
542
- };
543
- row.append(copy, cancel);
544
- ui.box.append(h, p, qr, codeEl, row);
545
- },
546
- onCode(code) {
547
- if (codeEl) {
548
- codeEl.textContent = code;
549
- const p = ui?.box.querySelector("p");
550
- if (p) p.textContent = "Check bWalletX shows the same code, then tap Connect.";
668
+ var TOKEN_ID = /^bsv21:[0-9a-f]{64}_\d{1,6}$/;
669
+ var RAW = /^[1-9]\d{0,38}$/;
670
+ function normaliseRequirement(r) {
671
+ if (!r || typeof r !== "object") throw new AccessError("bad_request", "require must be an object");
672
+ const req = r;
673
+ const out = {};
674
+ if (req.token !== void 0) {
675
+ const t = req.token;
676
+ if (!t || typeof t !== "object" || typeof t.id !== "string") throw new AccessError("bad_request", "require.token.id is required");
677
+ const id = t.id.trim().toLowerCase();
678
+ if (!TOKEN_ID.test(id)) throw new AccessError("bad_request", "require.token.id must be 'bsv21:<txid>_<vout>'");
679
+ const hasMin = t.min !== void 0;
680
+ const hasBps = t.minBps !== void 0;
681
+ if (hasMin === hasBps) throw new AccessError("bad_request", "require.token needs exactly one of min or minBps");
682
+ out.token = { id };
683
+ if (hasMin) {
684
+ if (typeof t.min !== "string" || !RAW.test(t.min)) throw new AccessError("bad_request", "require.token.min must be a positive integer string (raw units)");
685
+ out.token.min = t.min;
686
+ } else {
687
+ if (!Number.isInteger(t.minBps) || t.minBps < 1 || t.minBps > 1e4) {
688
+ throw new AccessError("bad_request", "require.token.minBps must be an integer 1..10000");
551
689
  }
690
+ out.token.minBps = t.minBps;
552
691
  }
553
- };
554
- }
555
- function pickWallet(options, o = {}) {
556
- if (typeof document === "undefined") return Promise.resolve(null);
557
- return new Promise((resolve) => {
558
- const ui = mount();
559
- const done = (w) => {
560
- ui.close();
561
- resolve(w);
562
- };
563
- ui.box.append(el("h2", "", o.title ?? "Choose a wallet"));
564
- if (!options.length) ui.box.append(el("p", "", "No wallet found. Get bWalletX at bwalletx.com."));
565
- const list = el("div", "bwx-list");
566
- for (const w of options) {
567
- const b = el("button", "bwx-btn bwx-opt");
568
- if (w.icon) {
569
- const img = el("img");
570
- img.src = w.icon;
571
- img.alt = "";
572
- b.append(img);
573
- }
574
- b.append(el("span", "", w.name));
575
- if (w.kind === "handcash") b.append(el("span", "bwx-tag", "until BRC-100"));
576
- b.onclick = () => done(w);
577
- list.append(b);
692
+ }
693
+ if (req.identityLevel !== void 0) {
694
+ if (!Number.isInteger(req.identityLevel) || req.identityLevel < 1 || req.identityLevel > 10) {
695
+ throw new AccessError("bad_request", "require.identityLevel must be an integer 1..10");
578
696
  }
579
- const cancel = el("button", "bwx-btn", "Cancel");
580
- cancel.onclick = () => done(null);
581
- ui.root.onclick = (e) => e.target === ui.root && done(null);
582
- ui.box.append(list, el("div", "bwx-row"));
583
- ui.box.lastChild.append(cancel);
584
- });
697
+ out.identityLevel = req.identityLevel;
698
+ }
699
+ if (req.kyc !== void 0) {
700
+ if (typeof req.kyc !== "boolean") throw new AccessError("bad_request", "require.kyc must be a boolean");
701
+ if (req.kyc) out.kyc = true;
702
+ }
703
+ if (!out.token && !out.identityLevel && !out.kyc) throw new AccessError("bad_request", "require is empty");
704
+ return out;
705
+ }
706
+ function parseAccessChallenge(s) {
707
+ if (typeof s !== "string" || s.length > 4096) throw new AccessError("bad_request", "challenge must be a string");
708
+ let c;
709
+ try {
710
+ c = JSON.parse(s);
711
+ } catch {
712
+ throw new AccessError("bad_request", "challenge is not JSON");
713
+ }
714
+ if (!c || typeof c !== "object" || c.v !== 1) throw new AccessError("bad_request", "unsupported challenge version");
715
+ if (c.kind !== "401" && c.kind !== "402" && c.kind !== "403") throw new AccessError("bad_request", "bad kind");
716
+ if (typeof c.origin !== "string" || typeof c.nonce !== "string" || !/^[A-Za-z0-9_-]{16,64}$/.test(c.nonce)) {
717
+ throw new AccessError("bad_request", "bad origin or nonce");
718
+ }
719
+ if (!Number.isInteger(c.iat) || !Number.isInteger(c.exp) || c.exp <= c.iat) throw new AccessError("bad_request", "bad iat/exp");
720
+ if (c.sub !== void 0 && (typeof c.sub !== "string" || !/^0[23][0-9a-f]{64}$/.test(c.sub))) throw new AccessError("bad_request", "bad sub");
721
+ c.require = normaliseRequirement(c.require);
722
+ if (canonicalJson(c) !== s) throw new AccessError("bad_request", "challenge is not canonical");
723
+ return c;
585
724
  }
586
725
 
587
- // src/wallets.ts
588
- var import_sdk3 = require("@bsv/sdk");
589
- async function brc100Connected(wallet, o) {
590
- await wallet.waitForAuthentication({}).catch(() => {
591
- });
592
- const { publicKey } = await wallet.getPublicKey({ identityKey: true });
593
- return { wallet, payer: wallet, identityKey: publicKey, optionId: o.id, kind: o.kind };
726
+ // src/access-client.ts
727
+ var ONESAT = [0, "onesat"];
728
+ var LEGACY_ONESAT = [0, "p 1sat"];
729
+ function toAccessError(e) {
730
+ if (e instanceof AccessError) return e;
731
+ const err = e;
732
+ const msg = String(err?.message ?? e ?? "");
733
+ if (err?.code === "NOT_FOUND") return new AccessError("not_installed", msg);
734
+ if (/reject|denied|declin|cancel|user/i.test(msg) || err?.code === "ERR_USER_REJECTED" || err?.status === 4001) {
735
+ return new AccessError("user_rejected", msg);
736
+ }
737
+ return new AccessError("bad_request", msg || "access failed");
594
738
  }
595
- function brc100Option(wallet, info) {
596
- const opt = {
597
- id: info.id,
598
- name: info.name,
599
- icon: info.icon,
600
- rdns: info.rdns,
601
- kind: "brc100",
602
- isBwalletX: info.isBwalletX ?? false,
603
- connect: () => brc100Connected(wallet, opt)
604
- };
605
- return opt;
739
+ function throwIfAccessError(body) {
740
+ const b = body;
741
+ if (b && typeof b.error === "string") throw new AccessError(b.error, b.message, { have: b.have, need: b.need });
606
742
  }
607
- function pairingOption(pairing) {
608
- const opt = {
609
- id: "bwalletx-pairing",
610
- name: "bWalletX on your phone",
611
- kind: "pairing",
612
- isBwalletX: true,
613
- async connect() {
614
- const p = typeof pairing === "function" ? pairing() : pairing;
615
- const w = await restorePairing(p) ?? await pairBwalletX(p);
616
- return brc100Connected(w, opt);
617
- }
618
- };
619
- return opt;
743
+ function hash160FromP2pkh(script) {
744
+ const m = /^76a914([0-9a-f]{40})88ac$/i.exec(script.slice(-50));
745
+ return m ? m[1].toLowerCase() : null;
620
746
  }
621
- function handcashAdapter(o) {
622
- const opt = {
623
- id: "handcash",
624
- name: "HandCash",
625
- icon: o.icon,
626
- kind: "handcash",
627
- isBwalletX: false,
628
- async connect() {
629
- const p = await o.login();
630
- return {
631
- wallet: null,
632
- payer: o.pay ? { pay: o.pay } : null,
633
- identityKey: p.identityKey ?? null,
634
- handle: p.handle,
635
- paymail: p.paymail ?? `${p.handle.replace(/^\$/, "")}@handcash.io`,
636
- optionId: opt.id,
637
- kind: "handcash",
638
- transitional: true
639
- };
747
+ async function proveHoldings(wallet, challenge, tokenId, basket = "bsv21") {
748
+ const bare = tokenId.replace(/^bsv21:/, "");
749
+ const list = await wallet.listOutputs({ basket, include: "locking scripts", includeCustomInstructions: true, limit: 1e4 });
750
+ const byKey = /* @__PURE__ */ new Map();
751
+ const data = challengeBytes(challenge);
752
+ for (const out of list.outputs) {
753
+ if (byKey.size >= MAX_HOLDING_PROOFS) break;
754
+ let ci = {};
755
+ try {
756
+ ci = JSON.parse(out.customInstructions ?? "{}");
757
+ } catch {
758
+ continue;
640
759
  }
641
- };
642
- return opt;
643
- }
644
- var NAMES = { [RDNS_EXTENSION]: "bWalletX", "space.bwallet.mobile": "bWalletX" };
645
- async function probe(substrate, ms) {
646
- try {
647
- const w = new import_sdk3.WalletClient(substrate);
648
- const ok = await Promise.race([w.isAuthenticated({}).then(() => true), new Promise((r) => setTimeout(() => r(false), ms))]);
649
- return ok ? w : null;
650
- } catch {
651
- return null;
652
- }
653
- }
654
- async function discoverWallets(o = {}) {
655
- const out = [];
656
- const ms = o.ms ?? 1200;
657
- if (typeof window !== "undefined") {
658
- const seen = /* @__PURE__ */ new Map();
659
- const onAnnounce = (e) => {
660
- const d = e.detail;
661
- if (!d?.wallet) return;
662
- const rdns = d.info?.rdns ?? d.info?.uuid ?? `wallet-${seen.size}`;
663
- if (seen.has(rdns)) return;
664
- const isB = BWALLETX_RDNS.includes(rdns);
665
- seen.set(rdns, brc100Option(d.wallet, { id: rdns, rdns, name: NAMES[rdns] ?? d.info?.name ?? rdns, icon: d.info?.icon, isBwalletX: isB }));
666
- };
667
- window.addEventListener("brc100:announceWallet", onAnnounce);
668
- window.dispatchEvent(new Event("brc100:requestWallet"));
669
- const local = o.probeLocal ? Promise.all([probe("json-api", ms), probe("Cicada", ms)]) : Promise.resolve([null, null]);
670
- await new Promise((r) => setTimeout(r, ms));
671
- window.removeEventListener("brc100:announceWallet", onAnnounce);
672
- out.push(...seen.values());
673
- const cwi = window.CWI;
674
- if (cwi && ![...seen.values()].some((x) => x.rdns === "space.bwallet.mobile")) {
675
- out.push(brc100Option(cwi, { id: "window.CWI", name: "This app\u2019s wallet", isBwalletX: false }));
760
+ if (ci.id !== bare || !ci.keyID) continue;
761
+ const want = out.lockingScript ? hash160FromP2pkh(out.lockingScript) : null;
762
+ const counterparty = ci.counterparty ?? "self";
763
+ for (const protocolID of ci.protocolID ? [ci.protocolID] : [ONESAT, LEGACY_ONESAT]) {
764
+ const { publicKey } = await wallet.getPublicKey({ protocolID, keyID: ci.keyID, counterparty });
765
+ const h = import_sdk4.Utils.toHex(import_sdk4.Hash.hash160(import_sdk4.PublicKey.fromString(publicKey).encode(true)));
766
+ if (want && h !== want) continue;
767
+ const existing = byKey.get(publicKey);
768
+ if (existing) {
769
+ existing.outpoints.push(out.outpoint.replace(".", "_"));
770
+ break;
771
+ }
772
+ const { signature } = await wallet.createSignature({ protocolID, keyID: ci.keyID, counterparty, data });
773
+ byKey.set(publicKey, { pubkey: publicKey, signature: Array.from(signature), outpoints: [out.outpoint.replace(".", "_")] });
774
+ break;
676
775
  }
677
- const [json, cicada] = await local;
678
- if (json) out.push(brc100Option(json, { id: "localhost:3321", name: "Desktop wallet (localhost:3321)" }));
679
- if (cicada) out.push(brc100Option(cicada, { id: "localhost:3301", name: "Desktop wallet (localhost:3301)" }));
680
- }
681
- if (o.pairing) out.push(pairingOption(o.pairing));
682
- out.push(...o.extra ?? []);
683
- const rank = (w) => w.isBwalletX ? 0 : w.kind === "handcash" ? 2 : 1;
684
- return out.map((w, i) => ({ w, i })).sort((a, b) => rank(a.w) - rank(b.w) || a.i - b.i).map((x) => x.w);
685
- }
686
-
687
- // src/pay.ts
688
- var import_sdk4 = require("@bsv/sdk");
689
- var BRC29_PROTOCOL = [2, "3241645161d8"];
690
- var PaymentError = class extends Error {
691
- constructor(code, message, maybeSent = false, cause) {
692
- super(message);
693
- this.code = code;
694
- this.maybeSent = maybeSent;
695
- this.cause = cause;
696
- this.name = "PaymentError";
697
- }
698
- code;
699
- maybeSent;
700
- cause;
701
- };
702
- var isLegacy = (s) => typeof s.pay === "function" && typeof s.createAction !== "function";
703
- var isIdentityKeyHex = (s) => /^0[23][0-9a-fA-F]{64}$/.test(s);
704
- var isPaymail = (s) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
705
- function randomB64(n) {
706
- const b = new Uint8Array(n);
707
- globalThis.crypto.getRandomValues(b);
708
- return import_sdk4.Utils.toBase64(Array.from(b));
709
- }
710
- function opReturn(text) {
711
- const d = import_sdk4.Utils.toArray(text, "utf8");
712
- const len = d.length;
713
- const push = len < 76 ? [len] : len <= 255 ? [76, len] : [77, len & 255, len >> 8];
714
- return import_sdk4.Utils.toHex([0, 106, ...push, ...d]);
715
- }
716
- var clip = (s, n) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
717
- var describe = (s) => clip(s.length < 5 ? `${s} payment`.trim() : s, 50);
718
- async function usdRate(o) {
719
- if (typeof o.usdPerBsv === "number") return o.usdPerBsv;
720
- try {
721
- if (typeof o.usdPerBsv === "function") return await o.usdPerBsv();
722
- const r = await (o.fetch ?? fetch)("https://api.whatsonchain.com/v1/bsv/main/exchangerate");
723
- const j = await r.json();
724
- if (!r.ok || !(Number(j.rate) > 0)) throw new Error(`status ${r.status}`);
725
- return Number(j.rate);
726
- } catch (e) {
727
- throw new PaymentError("RATE_FAILED", `Could not get the BSV/USD rate: ${e.message}`, false, e);
728
- }
729
- }
730
- async function amountInSats(req, o = {}) {
731
- if (req.sats != null) {
732
- if (!Number.isSafeInteger(req.sats) || req.sats <= 0) throw new PaymentError("BAD_REQUEST", "sats must be a positive integer");
733
- return req.sats;
734
- }
735
- if (req.usd != null) {
736
- if (!(req.usd > 0)) throw new PaymentError("BAD_REQUEST", "usd must be positive");
737
- return Math.max(1, Math.ceil(req.usd / await usdRate(o) * 1e8));
738
776
  }
739
- throw new PaymentError("BAD_REQUEST", "Give sats or usd");
777
+ return [...byKey.values()];
740
778
  }
741
- async function resolvePaymailP2P(paymail, sats, f = fetch) {
742
- const [alias, domain] = paymail.split("@");
743
- const fail = (m, e) => new PaymentError("RESOLVE_FAILED", `Could not resolve ${paymail}: ${m}`, false, e);
744
- let caps;
745
- try {
746
- const r = await f(`https://${domain}/.well-known/bsvalias`);
747
- caps = (await r.json()).capabilities;
748
- } catch (e) {
749
- throw fail("no bsvalias document (or CORS blocked it; resolve on your server and pass resolvePaymail)", e);
750
- }
751
- const fill = (u) => u.replace("{alias}", encodeURIComponent(alias)).replace("{domain.tld}", domain);
752
- const dest = caps?.["2a40af698840"];
753
- if (!dest) throw fail("it does not offer P2P payment destinations");
754
- let body;
779
+ async function requestAccess(o) {
780
+ let wallet;
755
781
  try {
756
- const r = await f(fill(dest), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ satoshis: sats }) });
757
- if (!r.ok) throw new Error(`status ${r.status}`);
758
- body = await r.json();
782
+ ({ wallet } = await connectBwalletX({ wallet: o.wallet, pairing: o.pairing, discoverMs: o.discoverMs }));
759
783
  } catch (e) {
760
- throw fail(e.message, e);
784
+ throw e?.code === "NOT_FOUND" ? new AccessError("not_installed") : toAccessError(e);
761
785
  }
762
- if (!body.outputs?.length) throw fail("no outputs returned");
763
- const receive = caps["5f1323cddf31"];
764
- return {
765
- outputs: body.outputs.map((o) => ({ script: o.script, satoshis: o.satoshis })),
766
- reference: body.reference,
767
- submit: receive ? async (hex) => {
768
- await f(fill(receive), {
769
- method: "POST",
770
- headers: { "content-type": "application/json" },
771
- body: JSON.stringify({ hex, reference: body.reference, metadata: {} })
772
- });
773
- } : void 0
774
- };
775
- }
776
- async function runAction(wallet, description, outputs, labels) {
777
786
  try {
778
- return await wallet.createAction({
779
- description: describe(description),
780
- outputs: outputs.map((o) => ({ lockingScript: o.script, satoshis: o.satoshis, outputDescription: describe(o.description ?? "payment") })),
781
- labels: labels?.length ? labels : void 0,
782
- options: { randomizeOutputs: false }
787
+ const { publicKey: identityKey } = await wallet.getPublicKey({ identityKey: true });
788
+ const { challenge } = await o.challenge(identityKey);
789
+ const c = parseAccessChallenge(challenge);
790
+ if (c.sub && c.sub !== identityKey.toLowerCase()) throw new AccessError("wrong_account");
791
+ if (Date.now() / 1e3 >= c.exp) throw new AccessError("expired");
792
+ const { signature } = await wallet.createSignature({
793
+ protocolID: ACCESS_PROTOCOL,
794
+ keyID: c.nonce,
795
+ counterparty: "anyone",
796
+ data: challengeBytes(challenge)
783
797
  });
798
+ const response = { v: 1, challenge, identityKey, signature: Array.from(signature) };
799
+ if (c.require.token) response.holdings = await proveHoldings(wallet, challenge, c.require.token.id, o.tokenBasket);
800
+ let verified;
801
+ if (o.verify) {
802
+ verified = await o.verify(response);
803
+ throwIfAccessError(verified);
804
+ }
805
+ return { response, verified, wallet };
784
806
  } catch (e) {
785
- const msg = e?.message ?? String(e);
786
- const rejected = /denied|reject|cancel|abort|user/i.test(msg) || e?.code === 4001;
787
- throw new PaymentError(rejected ? "WALLET_REJECTED" : "WALLET_ERROR", rejected ? "The payment was declined in the wallet." : `The wallet could not make the payment: ${msg}`, !rejected, e);
788
- }
789
- }
790
- async function pay(source, req, o = {}) {
791
- const to = (req.to ?? "").trim();
792
- if (!to) throw new PaymentError("BAD_REQUEST", "Give a recipient (to)");
793
- if (req.memo && req.memo.length > 1e3) throw new PaymentError("BAD_REQUEST", "memo is too long");
794
- const sats = await amountInSats(req, o);
795
- if (isLegacy(source)) return source.pay({ ...req, to, sats });
796
- let outputs;
797
- let remittance;
798
- let submit;
799
- if (isIdentityKeyHex(to)) {
800
- const derivationPrefix = randomB64(8);
801
- const derivationSuffix = randomB64(8);
802
- let pk;
803
- let me;
804
- try {
805
- ({ publicKey: pk } = await source.getPublicKey({ protocolID: BRC29_PROTOCOL, keyID: `${derivationPrefix} ${derivationSuffix}`, counterparty: to.toLowerCase() }));
806
- ({ publicKey: me } = await source.getPublicKey({ identityKey: true }));
807
- } catch (e) {
808
- throw new PaymentError("WALLET_ERROR", `The wallet could not derive the payment key: ${e.message}`, false, e);
809
- }
810
- outputs = [{ script: new import_sdk4.P2PKH().lock(import_sdk4.PublicKey.fromString(pk).toHash()).toHex(), satoshis: sats, description: "payment" }];
811
- remittance = { senderIdentityKey: me, derivationPrefix, derivationSuffix, outputIndex: 0 };
812
- } else if (isPaymail(to)) {
813
- const r2 = await (o.resolvePaymail ?? ((p, s) => resolvePaymailP2P(p, s, o.fetch ?? fetch)))(to, sats);
814
- outputs = r2.outputs;
815
- submit = r2.submit;
816
- } else {
817
- try {
818
- outputs = [{ script: new import_sdk4.P2PKH().lock(to).toHex(), satoshis: sats, description: "payment" }];
819
- } catch {
820
- throw new PaymentError("BAD_REQUEST", `Not a paymail, address or identity key: ${to}`);
821
- }
822
- }
823
- if (req.memo && req.memoOnChain) outputs = [...outputs, { script: opReturn(req.memo), satoshis: 0, description: "memo" }];
824
- const r = await runAction(source, req.memo || `Pay ${clip(to, 30)}`, outputs, req.labels);
825
- if (!r.txid) throw new PaymentError("WALLET_ERROR", "The wallet did not return a txid", true);
826
- const tx = r.tx ? Array.from(r.tx) : void 0;
827
- if (submit && tx) {
828
- await submit(import_sdk4.Transaction.fromAtomicBEEF(tx).toHex()).catch(() => {
829
- });
830
- }
831
- return { txid: r.txid, tx, sats, outputs, remittance };
832
- }
833
- function tip(source, req, o = {}) {
834
- return pay(source, { memo: "Tip", ...req, labels: [...req.labels ?? [], "tip"] }, o);
835
- }
836
- async function buyToken(source, req) {
837
- if (isLegacy(source)) throw new PaymentError("NOT_SUPPORTED", "This wallet cannot buy tokens. Use a BRC-100 wallet such as bWalletX.");
838
- const q = await req.quote({ token: req.token, amount: req.amount });
839
- if (!q?.outputs?.length) throw new PaymentError("BAD_REQUEST", "The quote returned no outputs");
840
- const sats = q.outputs.reduce((n, x) => n + x.satoshis, 0);
841
- const r = await runAction(source, q.description ?? `Buy ${clip(req.token, 30)}`, q.outputs, [...req.labels ?? [], "token-buy"]);
842
- if (!r.txid) throw new PaymentError("WALLET_ERROR", "The wallet did not return a txid", true);
843
- const res = { txid: r.txid, tx: r.tx ? Array.from(r.tx) : void 0, sats, outputs: q.outputs };
844
- await q.onPaid?.(res);
845
- return res;
846
- }
847
-
848
- // src/access-client.ts
849
- var import_sdk5 = require("@bsv/sdk");
850
-
851
- // src/access.ts
852
- var ACCESS_PROTOCOL = [2, "bwalletx access"];
853
- var MAX_HOLDING_PROOFS = 50;
854
- var AccessError = class extends Error {
855
- code;
856
- have;
857
- need;
858
- constructor(code, message, extra = {}) {
859
- super(message ?? code);
860
- this.name = "AccessError";
861
- this.code = code;
862
- this.have = extra.have;
863
- this.need = extra.need;
864
- }
865
- toJSON() {
866
- const b = { error: this.code, message: this.message };
867
- if (this.have !== void 0) b.have = this.have;
868
- if (this.need !== void 0) b.need = this.need;
869
- return b;
870
- }
871
- };
872
- function canonicalJson(v) {
873
- if (v === null) return "null";
874
- if (typeof v === "string" || typeof v === "boolean") return JSON.stringify(v);
875
- if (typeof v === "number") {
876
- if (!Number.isFinite(v)) throw new Error("canonicalJson: non-finite number");
877
- return JSON.stringify(v);
878
- }
879
- if (Array.isArray(v)) return `[${v.map((x) => canonicalJson(x)).join(",")}]`;
880
- if (typeof v === "object") {
881
- const o = v;
882
- const keys = Object.keys(o).filter((k) => o[k] !== void 0).sort();
883
- return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
884
- }
885
- throw new Error(`canonicalJson: unsupported ${typeof v}`);
886
- }
887
- function challengeBytes(challenge) {
888
- return Array.from(new TextEncoder().encode(challenge));
889
- }
890
- var TOKEN_ID = /^bsv21:[0-9a-f]{64}_\d{1,6}$/;
891
- var RAW = /^[1-9]\d{0,38}$/;
892
- function normaliseRequirement(r) {
893
- if (!r || typeof r !== "object") throw new AccessError("bad_request", "require must be an object");
894
- const req = r;
895
- const out = {};
896
- if (req.token !== void 0) {
897
- const t = req.token;
898
- if (!t || typeof t !== "object" || typeof t.id !== "string") throw new AccessError("bad_request", "require.token.id is required");
899
- const id = t.id.trim().toLowerCase();
900
- if (!TOKEN_ID.test(id)) throw new AccessError("bad_request", "require.token.id must be 'bsv21:<txid>_<vout>'");
901
- const hasMin = t.min !== void 0;
902
- const hasBps = t.minBps !== void 0;
903
- if (hasMin === hasBps) throw new AccessError("bad_request", "require.token needs exactly one of min or minBps");
904
- out.token = { id };
905
- if (hasMin) {
906
- if (typeof t.min !== "string" || !RAW.test(t.min)) throw new AccessError("bad_request", "require.token.min must be a positive integer string (raw units)");
907
- out.token.min = t.min;
908
- } else {
909
- if (!Number.isInteger(t.minBps) || t.minBps < 1 || t.minBps > 1e4) {
910
- throw new AccessError("bad_request", "require.token.minBps must be an integer 1..10000");
911
- }
912
- out.token.minBps = t.minBps;
913
- }
914
- }
915
- if (req.identityLevel !== void 0) {
916
- if (!Number.isInteger(req.identityLevel) || req.identityLevel < 1 || req.identityLevel > 10) {
917
- throw new AccessError("bad_request", "require.identityLevel must be an integer 1..10");
918
- }
919
- out.identityLevel = req.identityLevel;
920
- }
921
- if (req.kyc !== void 0) {
922
- if (typeof req.kyc !== "boolean") throw new AccessError("bad_request", "require.kyc must be a boolean");
923
- if (req.kyc) out.kyc = true;
924
- }
925
- if (!out.token && !out.identityLevel && !out.kyc) throw new AccessError("bad_request", "require is empty");
926
- return out;
927
- }
928
- function parseAccessChallenge(s) {
929
- if (typeof s !== "string" || s.length > 4096) throw new AccessError("bad_request", "challenge must be a string");
930
- let c;
931
- try {
932
- c = JSON.parse(s);
933
- } catch {
934
- throw new AccessError("bad_request", "challenge is not JSON");
935
- }
936
- if (!c || typeof c !== "object" || c.v !== 1) throw new AccessError("bad_request", "unsupported challenge version");
937
- if (c.kind !== "401" && c.kind !== "402" && c.kind !== "403") throw new AccessError("bad_request", "bad kind");
938
- if (typeof c.origin !== "string" || typeof c.nonce !== "string" || !/^[A-Za-z0-9_-]{16,64}$/.test(c.nonce)) {
939
- throw new AccessError("bad_request", "bad origin or nonce");
940
- }
941
- if (!Number.isInteger(c.iat) || !Number.isInteger(c.exp) || c.exp <= c.iat) throw new AccessError("bad_request", "bad iat/exp");
942
- if (c.sub !== void 0 && (typeof c.sub !== "string" || !/^0[23][0-9a-f]{64}$/.test(c.sub))) throw new AccessError("bad_request", "bad sub");
943
- c.require = normaliseRequirement(c.require);
944
- if (canonicalJson(c) !== s) throw new AccessError("bad_request", "challenge is not canonical");
945
- return c;
946
- }
947
-
948
- // src/access-client.ts
949
- var ONESAT = [0, "onesat"];
950
- var LEGACY_ONESAT = [0, "p 1sat"];
951
- function toAccessError(e) {
952
- if (e instanceof AccessError) return e;
953
- const err = e;
954
- const msg = String(err?.message ?? e ?? "");
955
- if (err?.code === "NOT_FOUND") return new AccessError("not_installed", msg);
956
- if (/reject|denied|declin|cancel|user/i.test(msg) || err?.code === "ERR_USER_REJECTED" || err?.status === 4001) {
957
- return new AccessError("user_rejected", msg);
958
- }
959
- return new AccessError("bad_request", msg || "access failed");
960
- }
961
- function throwIfAccessError(body) {
962
- const b = body;
963
- if (b && typeof b.error === "string") throw new AccessError(b.error, b.message, { have: b.have, need: b.need });
964
- }
965
- function hash160FromP2pkh(script) {
966
- const m = /^76a914([0-9a-f]{40})88ac$/i.exec(script.slice(-50));
967
- return m ? m[1].toLowerCase() : null;
968
- }
969
- async function proveHoldings(wallet, challenge, tokenId, basket = "bsv21") {
970
- const bare = tokenId.replace(/^bsv21:/, "");
971
- const list = await wallet.listOutputs({ basket, include: "locking scripts", includeCustomInstructions: true, limit: 1e4 });
972
- const byKey = /* @__PURE__ */ new Map();
973
- const data = challengeBytes(challenge);
974
- for (const out of list.outputs) {
975
- if (byKey.size >= MAX_HOLDING_PROOFS) break;
976
- let ci = {};
977
- try {
978
- ci = JSON.parse(out.customInstructions ?? "{}");
979
- } catch {
980
- continue;
981
- }
982
- if (ci.id !== bare || !ci.keyID) continue;
983
- const want = out.lockingScript ? hash160FromP2pkh(out.lockingScript) : null;
984
- const counterparty = ci.counterparty ?? "self";
985
- for (const protocolID of ci.protocolID ? [ci.protocolID] : [ONESAT, LEGACY_ONESAT]) {
986
- const { publicKey } = await wallet.getPublicKey({ protocolID, keyID: ci.keyID, counterparty });
987
- const h = import_sdk5.Utils.toHex(import_sdk5.Hash.hash160(import_sdk5.PublicKey.fromString(publicKey).encode(true)));
988
- if (want && h !== want) continue;
989
- const existing = byKey.get(publicKey);
990
- if (existing) {
991
- existing.outpoints.push(out.outpoint.replace(".", "_"));
992
- break;
993
- }
994
- const { signature } = await wallet.createSignature({ protocolID, keyID: ci.keyID, counterparty, data });
995
- byKey.set(publicKey, { pubkey: publicKey, signature: Array.from(signature), outpoints: [out.outpoint.replace(".", "_")] });
996
- break;
997
- }
998
- }
999
- return [...byKey.values()];
1000
- }
1001
- async function requestAccess(o) {
1002
- let wallet;
1003
- try {
1004
- ({ wallet } = await connectBwalletX({ wallet: o.wallet, pairing: o.pairing, discoverMs: o.discoverMs }));
1005
- } catch (e) {
1006
- throw e?.code === "NOT_FOUND" ? new AccessError("not_installed") : toAccessError(e);
1007
- }
1008
- try {
1009
- const { publicKey: identityKey } = await wallet.getPublicKey({ identityKey: true });
1010
- const { challenge } = await o.challenge(identityKey);
1011
- const c = parseAccessChallenge(challenge);
1012
- if (c.sub && c.sub !== identityKey.toLowerCase()) throw new AccessError("wrong_account");
1013
- if (Date.now() / 1e3 >= c.exp) throw new AccessError("expired");
1014
- const { signature } = await wallet.createSignature({
1015
- protocolID: ACCESS_PROTOCOL,
1016
- keyID: c.nonce,
1017
- counterparty: "anyone",
1018
- data: challengeBytes(challenge)
1019
- });
1020
- const response = { v: 1, challenge, identityKey, signature: Array.from(signature) };
1021
- if (c.require.token) response.holdings = await proveHoldings(wallet, challenge, c.require.token.id, o.tokenBasket);
1022
- let verified;
1023
- if (o.verify) {
1024
- verified = await o.verify(response);
1025
- throwIfAccessError(verified);
1026
- }
1027
- return { response, verified, wallet };
1028
- } catch (e) {
1029
- throw toAccessError(e);
807
+ throw toAccessError(e);
1030
808
  }
1031
809
  }
1032
810
 
@@ -1114,144 +892,554 @@ async function buildAuthorizeUrl(p) {
1114
892
  set("code_challenge", b64url2(digest));
1115
893
  set("code_challenge_method", "S256");
1116
894
  try {
1117
- sessionStorage.setItem("bwalletx:oauth", JSON.stringify({ verifier, state, nonce }));
1118
- } catch {
895
+ sessionStorage.setItem("bwalletx:oauth", JSON.stringify({ verifier, state, nonce }));
896
+ } catch {
897
+ }
898
+ return u.toString();
899
+ }
900
+ async function postJson(url, body) {
901
+ const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
902
+ const data = await res.json().catch(() => ({}));
903
+ if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
904
+ return data;
905
+ }
906
+ async function postAccess(url, body) {
907
+ const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
908
+ return res.json().catch(() => ({ error: "bad_request", message: `HTTP ${res.status}` }));
909
+ }
910
+ var OBSERVED = ["size", "theme", "label", "brand", "subtitle", "loading", "disabled", "full-width", "aria-label", "get-url", "web-url"];
911
+ function createClass() {
912
+ return class BwalletxSigninElement extends HTMLElement {
913
+ static get observedAttributes() {
914
+ return OBSERVED;
915
+ }
916
+ #root;
917
+ #notFound = false;
918
+ #busy = false;
919
+ constructor() {
920
+ super();
921
+ this.#root = this.attachShadow({ mode: "open" });
922
+ }
923
+ connectedCallback() {
924
+ this.#render();
925
+ }
926
+ attributeChangedCallback() {
927
+ if (this.isConnected) this.#render();
928
+ }
929
+ /** The inner <button>, for focus() and tests. */
930
+ get button() {
931
+ return this.#root.querySelector("button");
932
+ }
933
+ get notFound() {
934
+ return this.#notFound;
935
+ }
936
+ focus(opts) {
937
+ this.button?.focus(opts);
938
+ }
939
+ #attr(name) {
940
+ return this.getAttribute(name);
941
+ }
942
+ #render() {
943
+ const size = this.#attr("size") ?? "lg";
944
+ const brand = this.#attr("brand") === "bWallet" ? "bWallet" : "bWalletX";
945
+ const text = signinText(this.#attr("label") ?? "signin", brand);
946
+ const subAttr = this.#attr("subtitle");
947
+ const subDefault = brand === "bWalletX" && (size === "lg" || size === "md") ? "also works with bWallet" : null;
948
+ const subtitle = subAttr === null || subAttr === "" || subAttr === "on" ? subAttr === "on" ? "also works with bWallet" : subDefault : subAttr === "off" ? null : subAttr;
949
+ const loading = this.hasAttribute("loading") || this.#busy;
950
+ const disabled = this.hasAttribute("disabled");
951
+ const markPx = size === "icon" ? 24 : size === "sm" ? 15 : size === "md" ? 17 : 20;
952
+ const aria = this.#attr("aria-label") ?? (subtitle && size !== "icon" ? `${text}, ${subtitle}` : text);
953
+ const getUrl = this.#attr("get-url") ?? GET_URL;
954
+ const webUrl = this.#attr("web-url") ?? WEB_WALLET_URL;
955
+ const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
956
+ const label = size === "icon" ? "" : `<span class="txt" part="text"><b part="label">${esc(text)}</b>${subtitle ? `<small part="subtitle">${esc(subtitle)}</small>` : ""}</span>`;
957
+ const mark = loading ? '<span class="spin" aria-hidden="true"></span>' : markSvg(markPx);
958
+ 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>` : "";
959
+ 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}`;
960
+ this.button.addEventListener("click", () => void this.#click());
961
+ bindPrewarm(this.button);
962
+ }
963
+ #emit(type, detail, cancelable = false) {
964
+ return this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable }));
965
+ }
966
+ #setBusy(on) {
967
+ this.#busy = on;
968
+ this.#render();
969
+ }
970
+ async #click() {
971
+ if (this.hasAttribute("disabled") || this.hasAttribute("loading") || this.#busy) return;
972
+ if (!this.#emit("bwalletx-signin", { element: this }, true)) return;
973
+ const href = this.#attr("href");
974
+ const authorizeUrl = this.#attr("authorize-url");
975
+ if (authorizeUrl) {
976
+ location.assign(await buildAuthorizeUrl({ authorizeUrl, clientId: this.#attr("client-id"), redirectUri: this.#attr("redirect-uri"), scope: this.#attr("scope") }));
977
+ return;
978
+ }
979
+ if (href) {
980
+ location.assign(href);
981
+ return;
982
+ }
983
+ const challengeUrl = this.#attr("challenge-url");
984
+ if (!challengeUrl) return;
985
+ this.#notFound = false;
986
+ this.#setBusy(true);
987
+ try {
988
+ const ms = Number(this.#attr("discover-ms")) || DEFAULT_DISCOVER_MS;
989
+ const found = await discoverBwalletX(ms);
990
+ if (!found) {
991
+ this.#notFound = true;
992
+ this.#emit("bwalletx-not-found");
993
+ if (this.#attr("require-token")) this.#emit("bwalletx-access-denied", { code: "not_installed", message: "No wallet found" });
994
+ return;
995
+ }
996
+ const requireToken = this.#attr("require-token");
997
+ if (requireToken) {
998
+ const verifyUrl2 = this.#attr("verify-url");
999
+ const min = this.#attr("min");
1000
+ const minBps = this.#attr("min-bps");
1001
+ const hint = { token: { id: requireToken, ...minBps ? { minBps: Number(minBps) } : { min: min ?? "1" } } };
1002
+ try {
1003
+ const a = await requestAccess({
1004
+ wallet: found.wallet,
1005
+ challenge: (identityKey) => postJson(challengeUrl, { identityKey, require: hint }),
1006
+ verify: verifyUrl2 ? (response2) => postAccess(verifyUrl2, response2) : void 0
1007
+ });
1008
+ this.#emit("bwalletx-access-granted", { identityKey: a.response.identityKey, response: a.verified });
1009
+ } catch (e) {
1010
+ const err = e instanceof AccessError ? e : new AccessError("bad_request", String(e));
1011
+ this.#emit("bwalletx-access-denied", { code: err.code, message: err.message, have: err.have, need: err.need });
1012
+ }
1013
+ return;
1014
+ }
1015
+ const r = await signInWithBwalletX({ wallet: found.wallet, challenge: (identityKey) => postJson(challengeUrl, { identityKey }) });
1016
+ const verifyUrl = this.#attr("verify-url");
1017
+ const response = verifyUrl ? await postJson(verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature }) : null;
1018
+ this.#emit("bwalletx-signed-in", { identityKey: r.identityKey, method: found.method, nonce: r.nonce, signature: r.signature, response });
1019
+ } catch (error) {
1020
+ this.#emit("bwalletx-error", { error });
1021
+ } finally {
1022
+ this.#setBusy(false);
1023
+ }
1024
+ }
1025
+ };
1026
+ }
1027
+ function defineBwalletxSignin(tag = SIGNIN_TAG) {
1028
+ if (typeof window === "undefined" || typeof customElements === "undefined") return void 0;
1029
+ const existing = customElements.get(tag);
1030
+ if (existing) return existing;
1031
+ const C = createClass();
1032
+ customElements.define(tag, C);
1033
+ return C;
1034
+ }
1035
+
1036
+ // src/modal.ts
1037
+ var CSS = `
1038
+ .bwx-ov{position:fixed;inset:0;z-index:2147483000;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;padding:16px;font:15px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}
1039
+ .bwx-box{background:#0b0b0b;color:#f4f1e8;border:1px solid #3a3220;border-radius:16px;width:min(360px,100%);padding:20px;box-shadow:0 20px 60px rgba(0,0,0,.5)}
1040
+ .bwx-box h2{margin:0 0 6px;font-size:18px;color:#e8c766}
1041
+ .bwx-box p{margin:0 0 14px;color:#bdb6a3;font-size:14px}
1042
+ .bwx-qr{background:#fff;border-radius:12px;padding:12px;display:flex;justify-content:center}
1043
+ .bwx-qr svg{width:100%;height:auto;max-width:260px}
1044
+ .bwx-code{margin:14px 0 0;text-align:center;font-size:28px;letter-spacing:.3em;color:#e8c766;font-variant-numeric:tabular-nums}
1045
+ .bwx-row{display:flex;gap:8px;margin-top:14px}
1046
+ .bwx-btn{flex:1;border:1px solid #3a3220;background:#15130e;color:#f4f1e8;border-radius:10px;padding:10px 12px;font:inherit;cursor:pointer}
1047
+ .bwx-btn:hover{border-color:#e8c766}
1048
+ a.bwx-btn{display:block;text-align:center;text-decoration:none;box-sizing:border-box}
1049
+ .bwx-primary{background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;border:0;font-weight:700;min-height:48px;padding:13px 12px}
1050
+ .bwx-stack{display:flex;flex-direction:column;gap:8px}
1051
+ .bwx-list{display:flex;flex-direction:column;gap:8px}
1052
+ .bwx-opt{display:flex;align-items:center;gap:10px;text-align:left}
1053
+ .bwx-opt img{width:24px;height:24px;border-radius:6px}
1054
+ .bwx-tag{margin-left:auto;font-size:11px;color:#8d866f}
1055
+ `;
1056
+ function mount() {
1057
+ const root = document.createElement("div");
1058
+ root.className = "bwx-ov";
1059
+ root.setAttribute("role", "dialog");
1060
+ root.setAttribute("aria-modal", "true");
1061
+ const style = document.createElement("style");
1062
+ style.textContent = CSS;
1063
+ const box = document.createElement("div");
1064
+ box.className = "bwx-box";
1065
+ root.append(style, box);
1066
+ document.body.appendChild(root);
1067
+ return { root, box, close: () => root.remove() };
1068
+ }
1069
+ function el(tag, cls, text) {
1070
+ const e = document.createElement(tag);
1071
+ if (cls) e.className = cls;
1072
+ if (text != null) e.textContent = text;
1073
+ return e;
1074
+ }
1075
+ function qrSvg(text) {
1076
+ const q = (0, import_qrcode_generator.default)(0, "M");
1077
+ q.addData(text);
1078
+ q.make();
1079
+ return q.createSvgTag({ cellSize: 4, margin: 2, scalable: true });
1080
+ }
1081
+ function isTouchDevice(env = globalThis) {
1082
+ const nav = env.navigator;
1083
+ if (!nav) return false;
1084
+ const ua = nav.userAgent ?? "";
1085
+ if (/Android|iPhone|iPad|iPod|Mobile|Silk|Kindle/i.test(ua)) return true;
1086
+ if (/Macintosh/.test(ua) && (nav.maxTouchPoints ?? 0) > 1) return true;
1087
+ try {
1088
+ return !!env.matchMedia?.("(pointer: coarse)").matches && !env.matchMedia?.("(hover: hover)").matches;
1089
+ } catch {
1090
+ return false;
1091
+ }
1092
+ }
1093
+ function pairingModal(o = {}) {
1094
+ const ac = new AbortController();
1095
+ let ui = null;
1096
+ let codeEl = null;
1097
+ const close = () => {
1098
+ ui?.close();
1099
+ ui = null;
1100
+ };
1101
+ ac.signal.addEventListener("abort", close);
1102
+ return {
1103
+ signal: ac.signal,
1104
+ close,
1105
+ onLink(link) {
1106
+ if (typeof document === "undefined") return;
1107
+ close();
1108
+ ui = mount();
1109
+ const app = o.mode === "app" || (o.mode ?? "auto") === "auto" && isTouchDevice();
1110
+ const h = el("h2", "", o.title ?? "Connect bWalletX");
1111
+ const cancel = el("button", "bwx-btn", "Cancel");
1112
+ cancel.onclick = () => {
1113
+ o.onCancel?.();
1114
+ ac.abort();
1115
+ };
1116
+ codeEl = el("div", "bwx-code", "");
1117
+ codeEl.setAttribute("aria-live", "polite");
1118
+ if (app) {
1119
+ const p2 = el("p", "", "Open bWalletX, approve, then come back to this page.");
1120
+ const stack = el("div", "bwx-stack");
1121
+ const open = el("a", "bwx-btn bwx-primary", "Open bWalletX");
1122
+ open.href = link;
1123
+ open.rel = "noopener";
1124
+ const web = el("button", "bwx-btn", "Use web.bwalletx.com instead");
1125
+ web.onclick = () => {
1126
+ void navigator.clipboard?.writeText(link).catch(() => void 0);
1127
+ window.open(WEB_WALLET_URL, "_blank", "noopener");
1128
+ p2.textContent = "Link copied. In web.bwalletx.com tap Connect and paste it, then come back here.";
1129
+ };
1130
+ stack.append(open, web, cancel);
1131
+ ui.box.append(h, p2, codeEl, stack);
1132
+ return;
1133
+ }
1134
+ const p = el("p", "", "Scan with bWalletX on your phone, or paste the link into web.bwalletx.com.");
1135
+ const qr = el("div", "bwx-qr");
1136
+ qr.innerHTML = qrSvg(link);
1137
+ const row = el("div", "bwx-row");
1138
+ const copy = el("button", "bwx-btn", "Copy link");
1139
+ copy.onclick = () => void navigator.clipboard?.writeText(link).then(() => copy.textContent = "Copied");
1140
+ row.append(copy, cancel);
1141
+ ui.box.append(h, p, qr, codeEl, row);
1142
+ },
1143
+ onCode(code) {
1144
+ if (codeEl) {
1145
+ codeEl.textContent = code;
1146
+ const p = ui?.box.querySelector("p");
1147
+ if (p) p.textContent = "Check bWalletX shows the same code, then tap Connect.";
1148
+ }
1149
+ }
1150
+ };
1151
+ }
1152
+ function pickWallet(options, o = {}) {
1153
+ if (typeof document === "undefined") return Promise.resolve(null);
1154
+ return new Promise((resolve) => {
1155
+ const ui = mount();
1156
+ const done = (w) => {
1157
+ ui.close();
1158
+ resolve(w);
1159
+ };
1160
+ ui.box.append(el("h2", "", o.title ?? "Choose a wallet"));
1161
+ if (!options.length) ui.box.append(el("p", "", "No wallet found. Get bWalletX at bwalletx.com."));
1162
+ const list = el("div", "bwx-list");
1163
+ for (const w of options) {
1164
+ const b = el("button", "bwx-btn bwx-opt");
1165
+ if (w.icon) {
1166
+ const img = el("img");
1167
+ img.src = w.icon;
1168
+ img.alt = "";
1169
+ b.append(img);
1170
+ }
1171
+ b.append(el("span", "", w.name));
1172
+ if (w.kind === "handcash") b.append(el("span", "bwx-tag", "until BRC-100"));
1173
+ b.onclick = () => done(w);
1174
+ list.append(b);
1175
+ }
1176
+ const cancel = el("button", "bwx-btn", "Cancel");
1177
+ cancel.onclick = () => done(null);
1178
+ ui.root.onclick = (e) => e.target === ui.root && done(null);
1179
+ ui.box.append(list, el("div", "bwx-row"));
1180
+ ui.box.lastChild.append(cancel);
1181
+ });
1182
+ }
1183
+
1184
+ // src/wallets.ts
1185
+ var import_sdk5 = require("@bsv/sdk");
1186
+ async function brc100Connected(wallet, o) {
1187
+ await wallet.waitForAuthentication({}).catch(() => {
1188
+ });
1189
+ const { publicKey } = await wallet.getPublicKey({ identityKey: true });
1190
+ return { wallet, payer: wallet, identityKey: publicKey, optionId: o.id, kind: o.kind };
1191
+ }
1192
+ function brc100Option(wallet, info) {
1193
+ const opt = {
1194
+ id: info.id,
1195
+ name: info.name,
1196
+ icon: info.icon,
1197
+ rdns: info.rdns,
1198
+ kind: "brc100",
1199
+ isBwalletX: info.isBwalletX ?? false,
1200
+ connect: () => brc100Connected(wallet, opt)
1201
+ };
1202
+ return opt;
1203
+ }
1204
+ function pairingOption(pairing) {
1205
+ const opt = {
1206
+ id: "bwalletx-pairing",
1207
+ name: "bWalletX on your phone",
1208
+ kind: "pairing",
1209
+ isBwalletX: true,
1210
+ async connect() {
1211
+ const p = typeof pairing === "function" ? pairing() : pairing;
1212
+ const w = await restorePairing(p) ?? await pairBwalletX(p);
1213
+ return brc100Connected(w, opt);
1214
+ }
1215
+ };
1216
+ return opt;
1217
+ }
1218
+ function handcashAdapter(o) {
1219
+ const opt = {
1220
+ id: "handcash",
1221
+ name: "HandCash",
1222
+ icon: o.icon,
1223
+ kind: "handcash",
1224
+ isBwalletX: false,
1225
+ async connect() {
1226
+ const p = await o.login();
1227
+ return {
1228
+ wallet: null,
1229
+ payer: o.pay ? { pay: o.pay } : null,
1230
+ identityKey: p.identityKey ?? null,
1231
+ handle: p.handle,
1232
+ paymail: p.paymail ?? `${p.handle.replace(/^\$/, "")}@handcash.io`,
1233
+ optionId: opt.id,
1234
+ kind: "handcash",
1235
+ transitional: true
1236
+ };
1237
+ }
1238
+ };
1239
+ return opt;
1240
+ }
1241
+ var NAMES = { [RDNS_EXTENSION]: "bWalletX", "space.bwallet.mobile": "bWalletX" };
1242
+ async function probe(substrate, ms) {
1243
+ try {
1244
+ const w = new import_sdk5.WalletClient(substrate);
1245
+ const ok = await Promise.race([w.isAuthenticated({}).then(() => true), new Promise((r) => setTimeout(() => r(false), ms))]);
1246
+ return ok ? w : null;
1247
+ } catch {
1248
+ return null;
1249
+ }
1250
+ }
1251
+ async function discoverWallets(o = {}) {
1252
+ const out = [];
1253
+ const ms = o.ms ?? 300;
1254
+ if (typeof window !== "undefined") {
1255
+ const seen = /* @__PURE__ */ new Map();
1256
+ const onAnnounce = (e) => {
1257
+ const d = e.detail;
1258
+ if (!d?.wallet) return;
1259
+ const rdns = d.info?.rdns ?? d.info?.uuid ?? `wallet-${seen.size}`;
1260
+ if (seen.has(rdns)) return;
1261
+ const isB = BWALLETX_RDNS.includes(rdns);
1262
+ seen.set(rdns, brc100Option(d.wallet, { id: rdns, rdns, name: NAMES[rdns] ?? d.info?.name ?? rdns, icon: d.info?.icon, isBwalletX: isB }));
1263
+ };
1264
+ window.addEventListener("brc100:announceWallet", onAnnounce);
1265
+ window.dispatchEvent(new Event("brc100:requestWallet"));
1266
+ const local = o.probeLocal ? Promise.all([probe("json-api", ms), probe("Cicada", ms)]) : Promise.resolve([null, null]);
1267
+ await new Promise((r) => setTimeout(r, seen.size > 0 || window.CWI ? Math.min(ms, 50) : ms));
1268
+ window.removeEventListener("brc100:announceWallet", onAnnounce);
1269
+ out.push(...seen.values());
1270
+ const cwi = window.CWI;
1271
+ if (cwi && ![...seen.values()].some((x) => x.rdns === "space.bwallet.mobile")) {
1272
+ out.push(brc100Option(cwi, { id: "window.CWI", name: "This app\u2019s wallet", isBwalletX: false }));
1273
+ }
1274
+ const [json, cicada] = await local;
1275
+ if (json) out.push(brc100Option(json, { id: "localhost:3321", name: "Desktop wallet (localhost:3321)" }));
1276
+ if (cicada) out.push(brc100Option(cicada, { id: "localhost:3301", name: "Desktop wallet (localhost:3301)" }));
1277
+ }
1278
+ if (o.pairing) out.push(pairingOption(o.pairing));
1279
+ out.push(...o.extra ?? []);
1280
+ const rank = (w) => w.isBwalletX ? 0 : w.kind === "handcash" ? 2 : 1;
1281
+ return out.map((w, i) => ({ w, i })).sort((a, b) => rank(a.w) - rank(b.w) || a.i - b.i).map((x) => x.w);
1282
+ }
1283
+
1284
+ // src/pay.ts
1285
+ var import_sdk6 = require("@bsv/sdk");
1286
+ var BRC29_PROTOCOL = [2, "3241645161d8"];
1287
+ var PaymentError = class extends Error {
1288
+ constructor(code, message, maybeSent = false, cause) {
1289
+ super(message);
1290
+ this.code = code;
1291
+ this.maybeSent = maybeSent;
1292
+ this.cause = cause;
1293
+ this.name = "PaymentError";
1294
+ }
1295
+ code;
1296
+ maybeSent;
1297
+ cause;
1298
+ };
1299
+ var isLegacy = (s) => typeof s.pay === "function" && typeof s.createAction !== "function";
1300
+ var isIdentityKeyHex = (s) => /^0[23][0-9a-fA-F]{64}$/.test(s);
1301
+ var isPaymail = (s) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
1302
+ function randomB64(n) {
1303
+ const b = new Uint8Array(n);
1304
+ globalThis.crypto.getRandomValues(b);
1305
+ return import_sdk6.Utils.toBase64(Array.from(b));
1306
+ }
1307
+ function opReturn(text) {
1308
+ const d = import_sdk6.Utils.toArray(text, "utf8");
1309
+ const len = d.length;
1310
+ const push = len < 76 ? [len] : len <= 255 ? [76, len] : [77, len & 255, len >> 8];
1311
+ return import_sdk6.Utils.toHex([0, 106, ...push, ...d]);
1312
+ }
1313
+ var clip = (s, n) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
1314
+ var describe = (s) => clip(s.length < 5 ? `${s} payment`.trim() : s, 50);
1315
+ async function usdRate(o) {
1316
+ if (typeof o.usdPerBsv === "number") return o.usdPerBsv;
1317
+ try {
1318
+ if (typeof o.usdPerBsv === "function") return await o.usdPerBsv();
1319
+ const r = await (o.fetch ?? fetch)("https://api.whatsonchain.com/v1/bsv/main/exchangerate");
1320
+ const j = await r.json();
1321
+ if (!r.ok || !(Number(j.rate) > 0)) throw new Error(`status ${r.status}`);
1322
+ return Number(j.rate);
1323
+ } catch (e) {
1324
+ throw new PaymentError("RATE_FAILED", `Could not get the BSV/USD rate: ${e.message}`, false, e);
1119
1325
  }
1120
- return u.toString();
1121
1326
  }
1122
- async function postJson(url, body) {
1123
- const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
1124
- const data = await res.json().catch(() => ({}));
1125
- if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
1126
- return data;
1327
+ async function amountInSats(req, o = {}) {
1328
+ if (req.sats != null) {
1329
+ if (!Number.isSafeInteger(req.sats) || req.sats <= 0) throw new PaymentError("BAD_REQUEST", "sats must be a positive integer");
1330
+ return req.sats;
1331
+ }
1332
+ if (req.usd != null) {
1333
+ if (!(req.usd > 0)) throw new PaymentError("BAD_REQUEST", "usd must be positive");
1334
+ return Math.max(1, Math.ceil(req.usd / await usdRate(o) * 1e8));
1335
+ }
1336
+ throw new PaymentError("BAD_REQUEST", "Give sats or usd");
1127
1337
  }
1128
- async function postAccess(url, body) {
1129
- const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
1130
- return res.json().catch(() => ({ error: "bad_request", message: `HTTP ${res.status}` }));
1338
+ async function resolvePaymailP2P(paymail, sats, f = fetch) {
1339
+ const [alias, domain] = paymail.split("@");
1340
+ const fail = (m, e) => new PaymentError("RESOLVE_FAILED", `Could not resolve ${paymail}: ${m}`, false, e);
1341
+ let caps;
1342
+ try {
1343
+ const r = await f(`https://${domain}/.well-known/bsvalias`);
1344
+ caps = (await r.json()).capabilities;
1345
+ } catch (e) {
1346
+ throw fail("no bsvalias document (or CORS blocked it; resolve on your server and pass resolvePaymail)", e);
1347
+ }
1348
+ const fill = (u) => u.replace("{alias}", encodeURIComponent(alias)).replace("{domain.tld}", domain);
1349
+ const dest = caps?.["2a40af698840"];
1350
+ if (!dest) throw fail("it does not offer P2P payment destinations");
1351
+ let body;
1352
+ try {
1353
+ const r = await f(fill(dest), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ satoshis: sats }) });
1354
+ if (!r.ok) throw new Error(`status ${r.status}`);
1355
+ body = await r.json();
1356
+ } catch (e) {
1357
+ throw fail(e.message, e);
1358
+ }
1359
+ if (!body.outputs?.length) throw fail("no outputs returned");
1360
+ const receive = caps["5f1323cddf31"];
1361
+ return {
1362
+ outputs: body.outputs.map((o) => ({ script: o.script, satoshis: o.satoshis })),
1363
+ reference: body.reference,
1364
+ submit: receive ? async (hex2) => {
1365
+ await f(fill(receive), {
1366
+ method: "POST",
1367
+ headers: { "content-type": "application/json" },
1368
+ body: JSON.stringify({ hex: hex2, reference: body.reference, metadata: {} })
1369
+ });
1370
+ } : void 0
1371
+ };
1131
1372
  }
1132
- var OBSERVED = ["size", "theme", "label", "brand", "subtitle", "loading", "disabled", "full-width", "aria-label", "get-url", "web-url"];
1133
- function createClass() {
1134
- return class BwalletxSigninElement extends HTMLElement {
1135
- static get observedAttributes() {
1136
- return OBSERVED;
1137
- }
1138
- #root;
1139
- #notFound = false;
1140
- #busy = false;
1141
- constructor() {
1142
- super();
1143
- this.#root = this.attachShadow({ mode: "open" });
1144
- }
1145
- connectedCallback() {
1146
- this.#render();
1147
- }
1148
- attributeChangedCallback() {
1149
- if (this.isConnected) this.#render();
1150
- }
1151
- /** The inner <button>, for focus() and tests. */
1152
- get button() {
1153
- return this.#root.querySelector("button");
1154
- }
1155
- get notFound() {
1156
- return this.#notFound;
1157
- }
1158
- focus(opts) {
1159
- this.button?.focus(opts);
1160
- }
1161
- #attr(name) {
1162
- return this.getAttribute(name);
1163
- }
1164
- #render() {
1165
- const size = this.#attr("size") ?? "lg";
1166
- const brand = this.#attr("brand") === "bWallet" ? "bWallet" : "bWalletX";
1167
- const text = signinText(this.#attr("label") ?? "signin", brand);
1168
- const subAttr = this.#attr("subtitle");
1169
- const subDefault = brand === "bWalletX" && (size === "lg" || size === "md") ? "also works with bWallet" : null;
1170
- const subtitle = subAttr === null || subAttr === "" || subAttr === "on" ? subAttr === "on" ? "also works with bWallet" : subDefault : subAttr === "off" ? null : subAttr;
1171
- const loading = this.hasAttribute("loading") || this.#busy;
1172
- const disabled = this.hasAttribute("disabled");
1173
- const markPx = size === "icon" ? 24 : size === "sm" ? 15 : size === "md" ? 17 : 20;
1174
- const aria = this.#attr("aria-label") ?? (subtitle && size !== "icon" ? `${text}, ${subtitle}` : text);
1175
- const getUrl = this.#attr("get-url") ?? GET_URL;
1176
- const webUrl = this.#attr("web-url") ?? WEB_WALLET_URL;
1177
- const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
1178
- const label = size === "icon" ? "" : `<span class="txt" part="text"><b part="label">${esc(text)}</b>${subtitle ? `<small part="subtitle">${esc(subtitle)}</small>` : ""}</span>`;
1179
- const mark = loading ? '<span class="spin" aria-hidden="true"></span>' : markSvg(markPx);
1180
- 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>` : "";
1181
- 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}`;
1182
- this.button.addEventListener("click", () => void this.#click());
1183
- }
1184
- #emit(type, detail, cancelable = false) {
1185
- return this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable }));
1186
- }
1187
- #setBusy(on) {
1188
- this.#busy = on;
1189
- this.#render();
1373
+ async function runAction(wallet, description, outputs, labels) {
1374
+ try {
1375
+ return await wallet.createAction({
1376
+ description: describe(description),
1377
+ outputs: outputs.map((o) => ({ lockingScript: o.script, satoshis: o.satoshis, outputDescription: describe(o.description ?? "payment") })),
1378
+ labels: labels?.length ? labels : void 0,
1379
+ options: { randomizeOutputs: false }
1380
+ });
1381
+ } catch (e) {
1382
+ const msg = e?.message ?? String(e);
1383
+ const rejected = /denied|reject|cancel|abort|user/i.test(msg) || e?.code === 4001;
1384
+ throw new PaymentError(rejected ? "WALLET_REJECTED" : "WALLET_ERROR", rejected ? "The payment was declined in the wallet." : `The wallet could not make the payment: ${msg}`, !rejected, e);
1385
+ }
1386
+ }
1387
+ async function pay(source, req, o = {}) {
1388
+ const to = (req.to ?? "").trim();
1389
+ if (!to) throw new PaymentError("BAD_REQUEST", "Give a recipient (to)");
1390
+ if (req.memo && req.memo.length > 1e3) throw new PaymentError("BAD_REQUEST", "memo is too long");
1391
+ const sats = await amountInSats(req, o);
1392
+ if (isLegacy(source)) return source.pay({ ...req, to, sats });
1393
+ let outputs;
1394
+ let remittance;
1395
+ let submit;
1396
+ if (isIdentityKeyHex(to)) {
1397
+ const derivationPrefix = randomB64(8);
1398
+ const derivationSuffix = randomB64(8);
1399
+ let pk;
1400
+ let me;
1401
+ try {
1402
+ ({ publicKey: pk } = await source.getPublicKey({ protocolID: BRC29_PROTOCOL, keyID: `${derivationPrefix} ${derivationSuffix}`, counterparty: to.toLowerCase() }));
1403
+ ({ publicKey: me } = await source.getPublicKey({ identityKey: true }));
1404
+ } catch (e) {
1405
+ throw new PaymentError("WALLET_ERROR", `The wallet could not derive the payment key: ${e.message}`, false, e);
1190
1406
  }
1191
- async #click() {
1192
- if (this.hasAttribute("disabled") || this.hasAttribute("loading") || this.#busy) return;
1193
- if (!this.#emit("bwalletx-signin", { element: this }, true)) return;
1194
- const href = this.#attr("href");
1195
- const authorizeUrl = this.#attr("authorize-url");
1196
- if (authorizeUrl) {
1197
- location.assign(await buildAuthorizeUrl({ authorizeUrl, clientId: this.#attr("client-id"), redirectUri: this.#attr("redirect-uri"), scope: this.#attr("scope") }));
1198
- return;
1199
- }
1200
- if (href) {
1201
- location.assign(href);
1202
- return;
1203
- }
1204
- const challengeUrl = this.#attr("challenge-url");
1205
- if (!challengeUrl) return;
1206
- this.#notFound = false;
1207
- this.#setBusy(true);
1208
- try {
1209
- const ms = Number(this.#attr("discover-ms")) || 1200;
1210
- const found = await discoverBwalletX(ms);
1211
- if (!found) {
1212
- this.#notFound = true;
1213
- this.#emit("bwalletx-not-found");
1214
- if (this.#attr("require-token")) this.#emit("bwalletx-access-denied", { code: "not_installed", message: "No wallet found" });
1215
- return;
1216
- }
1217
- const requireToken = this.#attr("require-token");
1218
- if (requireToken) {
1219
- const verifyUrl2 = this.#attr("verify-url");
1220
- const min = this.#attr("min");
1221
- const minBps = this.#attr("min-bps");
1222
- const hint = { token: { id: requireToken, ...minBps ? { minBps: Number(minBps) } : { min: min ?? "1" } } };
1223
- try {
1224
- const a = await requestAccess({
1225
- wallet: found.wallet,
1226
- challenge: (identityKey) => postJson(challengeUrl, { identityKey, require: hint }),
1227
- verify: verifyUrl2 ? (response2) => postAccess(verifyUrl2, response2) : void 0
1228
- });
1229
- this.#emit("bwalletx-access-granted", { identityKey: a.response.identityKey, response: a.verified });
1230
- } catch (e) {
1231
- const err = e instanceof AccessError ? e : new AccessError("bad_request", String(e));
1232
- this.#emit("bwalletx-access-denied", { code: err.code, message: err.message, have: err.have, need: err.need });
1233
- }
1234
- return;
1235
- }
1236
- const r = await signInWithBwalletX({ wallet: found.wallet, challenge: (identityKey) => postJson(challengeUrl, { identityKey }) });
1237
- const verifyUrl = this.#attr("verify-url");
1238
- const response = verifyUrl ? await postJson(verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature }) : null;
1239
- this.#emit("bwalletx-signed-in", { identityKey: r.identityKey, method: found.method, nonce: r.nonce, signature: r.signature, response });
1240
- } catch (error) {
1241
- this.#emit("bwalletx-error", { error });
1242
- } finally {
1243
- this.#setBusy(false);
1244
- }
1407
+ outputs = [{ script: new import_sdk6.P2PKH().lock(import_sdk6.PublicKey.fromString(pk).toHash()).toHex(), satoshis: sats, description: "payment" }];
1408
+ remittance = { senderIdentityKey: me, derivationPrefix, derivationSuffix, outputIndex: 0 };
1409
+ } else if (isPaymail(to)) {
1410
+ const r2 = await (o.resolvePaymail ?? ((p, s) => resolvePaymailP2P(p, s, o.fetch ?? fetch)))(to, sats);
1411
+ outputs = r2.outputs;
1412
+ submit = r2.submit;
1413
+ } else {
1414
+ try {
1415
+ outputs = [{ script: new import_sdk6.P2PKH().lock(to).toHex(), satoshis: sats, description: "payment" }];
1416
+ } catch {
1417
+ throw new PaymentError("BAD_REQUEST", `Not a paymail, address or identity key: ${to}`);
1245
1418
  }
1246
- };
1419
+ }
1420
+ if (req.memo && req.memoOnChain) outputs = [...outputs, { script: opReturn(req.memo), satoshis: 0, description: "memo" }];
1421
+ const r = await runAction(source, req.memo || `Pay ${clip(to, 30)}`, outputs, req.labels);
1422
+ if (!r.txid) throw new PaymentError("WALLET_ERROR", "The wallet did not return a txid", true);
1423
+ const tx = r.tx ? Array.from(r.tx) : void 0;
1424
+ if (submit && tx) {
1425
+ await submit(import_sdk6.Transaction.fromAtomicBEEF(tx).toHex()).catch(() => {
1426
+ });
1427
+ }
1428
+ return { txid: r.txid, tx, sats, outputs, remittance };
1247
1429
  }
1248
- function defineBwalletxSignin(tag = SIGNIN_TAG) {
1249
- if (typeof window === "undefined" || typeof customElements === "undefined") return void 0;
1250
- const existing = customElements.get(tag);
1251
- if (existing) return existing;
1252
- const C = createClass();
1253
- customElements.define(tag, C);
1254
- return C;
1430
+ function tip(source, req, o = {}) {
1431
+ return pay(source, { memo: "Tip", ...req, labels: [...req.labels ?? [], "tip"] }, o);
1432
+ }
1433
+ async function buyToken(source, req) {
1434
+ if (isLegacy(source)) throw new PaymentError("NOT_SUPPORTED", "This wallet cannot buy tokens. Use a BRC-100 wallet such as bWalletX.");
1435
+ const q = await req.quote({ token: req.token, amount: req.amount });
1436
+ if (!q?.outputs?.length) throw new PaymentError("BAD_REQUEST", "The quote returned no outputs");
1437
+ const sats = q.outputs.reduce((n, x) => n + x.satoshis, 0);
1438
+ const r = await runAction(source, q.description ?? `Buy ${clip(req.token, 30)}`, q.outputs, [...req.labels ?? [], "token-buy"]);
1439
+ if (!r.txid) throw new PaymentError("WALLET_ERROR", "The wallet did not return a txid", true);
1440
+ const res = { txid: r.txid, tx: r.tx ? Array.from(r.tx) : void 0, sats, outputs: q.outputs };
1441
+ await q.onPaid?.(res);
1442
+ return res;
1255
1443
  }
1256
1444
 
1257
1445
  // src/react.ts
@@ -1333,6 +1521,12 @@ function BwalletXProvider(p) {
1333
1521
  props.current.onError?.(err);
1334
1522
  return err;
1335
1523
  }, []);
1524
+ const themeOn = !!p.theme && (state.status === "connecting" || state.status === "connected");
1525
+ (0, import_react.useEffect)(() => {
1526
+ if (!themeOn) return;
1527
+ declareWalletTheme(p.theme);
1528
+ return () => declareWalletTheme(null);
1529
+ }, [themeOn, p.theme]);
1336
1530
  (0, import_react.useEffect)(() => {
1337
1531
  const url = p.sessionUrl;
1338
1532
  if (!url) return;
@@ -1383,7 +1577,7 @@ function BwalletXProvider(p) {
1383
1577
  wallet: cw.wallet,
1384
1578
  challenge: (identityKey) => postJson2(c.challengeUrl, { identityKey })
1385
1579
  });
1386
- const v = await postJson2(c.verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature });
1580
+ const v = await atStage("verify", () => postJson2(c.verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature }));
1387
1581
  handle = v.handle ?? handle;
1388
1582
  paymail = v.paymail ?? paymail;
1389
1583
  signedIn = true;