@bwalletx/connect 0.5.0 → 0.5.3

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
@@ -407,7 +407,27 @@ async function clearPairing(opts = {}) {
407
407
  });
408
408
  }
409
409
 
410
+ // src/theme.ts
411
+ var WALLET_THEME_META = "bwalletx-theme";
412
+ var ID = /^[a-z0-9-]{1,32}$/;
413
+ function declareWalletTheme(theme) {
414
+ if (typeof document === "undefined") return;
415
+ const id = (theme ?? "").trim().toLowerCase();
416
+ let meta = document.head.querySelector(`meta[name="${WALLET_THEME_META}"]`);
417
+ if (!id || !ID.test(id)) {
418
+ meta?.remove();
419
+ return;
420
+ }
421
+ if (!meta) {
422
+ meta = document.createElement("meta");
423
+ meta.name = WALLET_THEME_META;
424
+ document.head.appendChild(meta);
425
+ }
426
+ if (meta.content !== id) meta.content = id;
427
+ }
428
+
410
429
  // src/discover.ts
430
+ var import_sdk3 = require("@bsv/sdk");
411
431
  var RDNS_EXTENSION = "com.bwalletx.extension";
412
432
  var RDNS_MOBILE = "space.bwallet.mobile";
413
433
  var BWALLETX_RDNS = [RDNS_EXTENSION, RDNS_MOBILE];
@@ -432,10 +452,64 @@ function discoverBwalletX(ms = 1200) {
432
452
  window.dispatchEvent(new Event("brc100:requestWallet"));
433
453
  setTimeout(() => {
434
454
  const cwi = window.CWI;
435
- finish(cwi ? { wallet: cwi, method: "in-app", rdns: null } : null);
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));
436
458
  }, ms);
437
459
  });
438
460
  }
461
+ var isFramed = () => {
462
+ try {
463
+ return window.parent !== window;
464
+ } catch {
465
+ return true;
466
+ }
467
+ };
468
+ var BWALLETX_PARENT_ORIGINS = [
469
+ "https://web.bwalletx.com",
470
+ "https://desktop.bwalletx.com",
471
+ "https://bwalletx.com",
472
+ "https://www.bwallet.space",
473
+ "https://bwallet.space",
474
+ "capacitor://localhost",
475
+ // iOS app shell
476
+ "https://localhost",
477
+ // Android app shell
478
+ "tauri://localhost",
479
+ // bWalletX Desktop (macOS)
480
+ "https://tauri.localhost",
481
+ // bWalletX Desktop (Windows)
482
+ "chrome-extension://bombmmcjghjdlbmcclnhjodgcelnehgl"
483
+ // bWalletX for Chrome (fixed id)
484
+ ];
485
+ var parentOrigin = () => {
486
+ try {
487
+ const anc = window.location.ancestorOrigins;
488
+ if (anc && anc.length > 0) return anc[0] ?? null;
489
+ if (!document.referrer) return null;
490
+ const u = new URL(document.referrer);
491
+ return `${u.protocol}//${u.host}`;
492
+ } catch {
493
+ return null;
494
+ }
495
+ };
496
+ async function probeXdm(ms = 1200, allowed = BWALLETX_PARENT_ORIGINS) {
497
+ const origin = parentOrigin();
498
+ if (!origin || !allowed.includes(origin)) return null;
499
+ try {
500
+ const w = new import_sdk3.WalletClient(new import_sdk3.XDM(origin));
501
+ const ok = await Promise.race([
502
+ w.isAuthenticated({}).then(
503
+ () => true,
504
+ () => false
505
+ ),
506
+ new Promise((r) => setTimeout(() => r(false), ms))
507
+ ]);
508
+ return ok ? w : null;
509
+ } catch {
510
+ return null;
511
+ }
512
+ }
439
513
 
440
514
  // src/signin.ts
441
515
  var LOGIN_PROTOCOL = [2, "bwallet sign in"];
@@ -456,802 +530,848 @@ async function signInWithBwalletX(opts) {
456
530
  const { wallet, method } = await connectBwalletX(opts);
457
531
  if (method !== "pairing") await wallet.waitForAuthentication({}).catch(() => {
458
532
  });
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
- });
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
+ );
467
544
  return { identityKey, signature: Array.from(signature), nonce: c.nonce, method, wallet };
468
545
  }
546
+ async function atStage(stage, run) {
547
+ try {
548
+ return await run();
549
+ } catch (e) {
550
+ const err = e instanceof Error ? e : new Error(String(e));
551
+ if (!err.stage) Object.assign(err, { stage });
552
+ throw err;
553
+ }
554
+ }
469
555
 
470
556
  // src/modal.ts
471
557
  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;
558
+
559
+ // src/access-client.ts
560
+ var import_sdk4 = require("@bsv/sdk");
561
+
562
+ // src/access.ts
563
+ var ACCESS_PROTOCOL = [2, "bwalletx access"];
564
+ var MAX_HOLDING_PROOFS = 50;
565
+ var AccessError = class extends Error {
566
+ code;
567
+ have;
568
+ need;
569
+ constructor(code, message, extra = {}) {
570
+ super(message ?? code);
571
+ this.name = "AccessError";
572
+ this.code = code;
573
+ this.have = extra.have;
574
+ this.need = extra.need;
575
+ }
576
+ toJSON() {
577
+ const b = { error: this.code, message: this.message };
578
+ if (this.have !== void 0) b.have = this.have;
579
+ if (this.need !== void 0) b.need = this.need;
580
+ return b;
581
+ }
582
+ };
583
+ function canonicalJson(v) {
584
+ if (v === null) return "null";
585
+ if (typeof v === "string" || typeof v === "boolean") return JSON.stringify(v);
586
+ if (typeof v === "number") {
587
+ if (!Number.isFinite(v)) throw new Error("canonicalJson: non-finite number");
588
+ return JSON.stringify(v);
589
+ }
590
+ if (Array.isArray(v)) return `[${v.map((x) => canonicalJson(x)).join(",")}]`;
591
+ if (typeof v === "object") {
592
+ const o = v;
593
+ const keys = Object.keys(o).filter((k) => o[k] !== void 0).sort();
594
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
595
+ }
596
+ throw new Error(`canonicalJson: unsupported ${typeof v}`);
506
597
  }
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 });
598
+ function challengeBytes(challenge) {
599
+ return Array.from(new TextEncoder().encode(challenge));
512
600
  }
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.";
601
+ var TOKEN_ID = /^bsv21:[0-9a-f]{64}_\d{1,6}$/;
602
+ var RAW = /^[1-9]\d{0,38}$/;
603
+ function normaliseRequirement(r) {
604
+ if (!r || typeof r !== "object") throw new AccessError("bad_request", "require must be an object");
605
+ const req = r;
606
+ const out = {};
607
+ if (req.token !== void 0) {
608
+ const t = req.token;
609
+ if (!t || typeof t !== "object" || typeof t.id !== "string") throw new AccessError("bad_request", "require.token.id is required");
610
+ const id = t.id.trim().toLowerCase();
611
+ if (!TOKEN_ID.test(id)) throw new AccessError("bad_request", "require.token.id must be 'bsv21:<txid>_<vout>'");
612
+ const hasMin = t.min !== void 0;
613
+ const hasBps = t.minBps !== void 0;
614
+ if (hasMin === hasBps) throw new AccessError("bad_request", "require.token needs exactly one of min or minBps");
615
+ out.token = { id };
616
+ if (hasMin) {
617
+ 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)");
618
+ out.token.min = t.min;
619
+ } else {
620
+ if (!Number.isInteger(t.minBps) || t.minBps < 1 || t.minBps > 1e4) {
621
+ throw new AccessError("bad_request", "require.token.minBps must be an integer 1..10000");
551
622
  }
623
+ out.token.minBps = t.minBps;
552
624
  }
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);
625
+ }
626
+ if (req.identityLevel !== void 0) {
627
+ if (!Number.isInteger(req.identityLevel) || req.identityLevel < 1 || req.identityLevel > 10) {
628
+ throw new AccessError("bad_request", "require.identityLevel must be an integer 1..10");
578
629
  }
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
- });
630
+ out.identityLevel = req.identityLevel;
631
+ }
632
+ if (req.kyc !== void 0) {
633
+ if (typeof req.kyc !== "boolean") throw new AccessError("bad_request", "require.kyc must be a boolean");
634
+ if (req.kyc) out.kyc = true;
635
+ }
636
+ if (!out.token && !out.identityLevel && !out.kyc) throw new AccessError("bad_request", "require is empty");
637
+ return out;
638
+ }
639
+ function parseAccessChallenge(s) {
640
+ if (typeof s !== "string" || s.length > 4096) throw new AccessError("bad_request", "challenge must be a string");
641
+ let c;
642
+ try {
643
+ c = JSON.parse(s);
644
+ } catch {
645
+ throw new AccessError("bad_request", "challenge is not JSON");
646
+ }
647
+ if (!c || typeof c !== "object" || c.v !== 1) throw new AccessError("bad_request", "unsupported challenge version");
648
+ if (c.kind !== "401" && c.kind !== "402" && c.kind !== "403") throw new AccessError("bad_request", "bad kind");
649
+ if (typeof c.origin !== "string" || typeof c.nonce !== "string" || !/^[A-Za-z0-9_-]{16,64}$/.test(c.nonce)) {
650
+ throw new AccessError("bad_request", "bad origin or nonce");
651
+ }
652
+ if (!Number.isInteger(c.iat) || !Number.isInteger(c.exp) || c.exp <= c.iat) throw new AccessError("bad_request", "bad iat/exp");
653
+ 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");
654
+ c.require = normaliseRequirement(c.require);
655
+ if (canonicalJson(c) !== s) throw new AccessError("bad_request", "challenge is not canonical");
656
+ return c;
585
657
  }
586
658
 
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 };
659
+ // src/access-client.ts
660
+ var ONESAT = [0, "onesat"];
661
+ var LEGACY_ONESAT = [0, "p 1sat"];
662
+ function toAccessError(e) {
663
+ if (e instanceof AccessError) return e;
664
+ const err = e;
665
+ const msg = String(err?.message ?? e ?? "");
666
+ if (err?.code === "NOT_FOUND") return new AccessError("not_installed", msg);
667
+ if (/reject|denied|declin|cancel|user/i.test(msg) || err?.code === "ERR_USER_REJECTED" || err?.status === 4001) {
668
+ return new AccessError("user_rejected", msg);
669
+ }
670
+ return new AccessError("bad_request", msg || "access failed");
594
671
  }
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;
672
+ function throwIfAccessError(body) {
673
+ const b = body;
674
+ if (b && typeof b.error === "string") throw new AccessError(b.error, b.message, { have: b.have, need: b.need });
606
675
  }
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;
676
+ function hash160FromP2pkh(script) {
677
+ const m = /^76a914([0-9a-f]{40})88ac$/i.exec(script.slice(-50));
678
+ return m ? m[1].toLowerCase() : null;
620
679
  }
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
- };
680
+ async function proveHoldings(wallet, challenge, tokenId, basket = "bsv21") {
681
+ const bare = tokenId.replace(/^bsv21:/, "");
682
+ const list = await wallet.listOutputs({ basket, include: "locking scripts", includeCustomInstructions: true, limit: 1e4 });
683
+ const byKey = /* @__PURE__ */ new Map();
684
+ const data = challengeBytes(challenge);
685
+ for (const out of list.outputs) {
686
+ if (byKey.size >= MAX_HOLDING_PROOFS) break;
687
+ let ci = {};
688
+ try {
689
+ ci = JSON.parse(out.customInstructions ?? "{}");
690
+ } catch {
691
+ continue;
640
692
  }
641
- };
642
- return opt;
693
+ if (ci.id !== bare || !ci.keyID) continue;
694
+ const want = out.lockingScript ? hash160FromP2pkh(out.lockingScript) : null;
695
+ const counterparty = ci.counterparty ?? "self";
696
+ for (const protocolID of ci.protocolID ? [ci.protocolID] : [ONESAT, LEGACY_ONESAT]) {
697
+ 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)));
699
+ if (want && h !== want) continue;
700
+ const existing = byKey.get(publicKey);
701
+ if (existing) {
702
+ existing.outpoints.push(out.outpoint.replace(".", "_"));
703
+ break;
704
+ }
705
+ const { signature } = await wallet.createSignature({ protocolID, keyID: ci.keyID, counterparty, data });
706
+ byKey.set(publicKey, { pubkey: publicKey, signature: Array.from(signature), outpoints: [out.outpoint.replace(".", "_")] });
707
+ break;
708
+ }
709
+ }
710
+ return [...byKey.values()];
643
711
  }
644
- var NAMES = { [RDNS_EXTENSION]: "bWalletX", "space.bwallet.mobile": "bWalletX" };
645
- async function probe(substrate, ms) {
712
+ async function requestAccess(o) {
713
+ let wallet;
646
714
  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;
715
+ ({ wallet } = await connectBwalletX({ wallet: o.wallet, pairing: o.pairing, discoverMs: o.discoverMs }));
716
+ } catch (e) {
717
+ throw e?.code === "NOT_FOUND" ? new AccessError("not_installed") : toAccessError(e);
652
718
  }
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 }));
719
+ try {
720
+ const { publicKey: identityKey } = await wallet.getPublicKey({ identityKey: true });
721
+ const { challenge } = await o.challenge(identityKey);
722
+ const c = parseAccessChallenge(challenge);
723
+ if (c.sub && c.sub !== identityKey.toLowerCase()) throw new AccessError("wrong_account");
724
+ if (Date.now() / 1e3 >= c.exp) throw new AccessError("expired");
725
+ const { signature } = await wallet.createSignature({
726
+ protocolID: ACCESS_PROTOCOL,
727
+ keyID: c.nonce,
728
+ counterparty: "anyone",
729
+ data: challengeBytes(challenge)
730
+ });
731
+ const response = { v: 1, challenge, identityKey, signature: Array.from(signature) };
732
+ if (c.require.token) response.holdings = await proveHoldings(wallet, challenge, c.require.token.id, o.tokenBasket);
733
+ let verified;
734
+ if (o.verify) {
735
+ verified = await o.verify(response);
736
+ throwIfAccessError(verified);
676
737
  }
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)" }));
738
+ return { response, verified, wallet };
739
+ } catch (e) {
740
+ throw toAccessError(e);
680
741
  }
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
742
  }
686
743
 
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));
744
+ // src/element.ts
745
+ var SIGNIN_TAG = "bwalletx-signin";
746
+ var GET_URL = "https://bwalletx.com/get";
747
+ var WEB_WALLET_URL = "https://web.bwalletx.com";
748
+ var VERBS = { signin: "Sign in with", continue: "Continue with", connect: "Connect" };
749
+ function signinText(label = "signin", brand = "bWalletX") {
750
+ return `${VERBS[label] ?? VERBS.signin} ${brand}`;
709
751
  }
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]);
752
+ var uid = 0;
753
+ function markSvg(px = 20) {
754
+ const n = ++uid;
755
+ return `<svg viewBox="23 8 74 100" width="${Math.round(px * 0.74)}" height="${px}" aria-hidden="true" focusable="false"><defs><linearGradient id="bwxg${n}" gradientUnits="userSpaceOnUse" x1="0" y1="12" x2="0" y2="105"><stop offset="0" stop-color="#FFE58A"/><stop offset=".55" stop-color="#FFD24D"/><stop offset="1" stop-color="#C98F00"/></linearGradient><mask id="bwxh${n}"><rect width="140" height="140" fill="#fff"/><circle cx="60" cy="72" r="15" fill="#000"/></mask></defs><g fill="url(#bwxg${n})" mask="url(#bwxh${n})"><polygon points="45,12 45,76 27,76 27,30"/><circle cx="60" cy="72" r="33"/></g></svg>`;
715
756
  }
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;
757
+ var SIGNIN_CSS = `
758
+ :host{display:inline-block;vertical-align:middle;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
759
+ :host([full-width]){display:block;width:100%}
760
+ :host([hidden]){display:none}
761
+ button{all:unset;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:12px;width:100%;cursor:pointer;
762
+ padding:16px 20px;border-radius:12px;font-size:16px;font-weight:700;line-height:1.2;text-align:left;
763
+ background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;
764
+ box-shadow:0 10px 15px -3px rgba(245,158,11,.2),0 4px 6px -4px rgba(245,158,11,.2);
765
+ transition:background .15s,transform .1s,box-shadow .15s,border-color .15s;-webkit-tap-highlight-color:transparent}
766
+ button:hover{background:linear-gradient(180deg,#fde68a,#fbbf24)}
767
+ button:active{transform:translateY(1px);background:linear-gradient(180deg,#fbbf24,#d97706)}
768
+ button:focus-visible{outline:2px solid #b45309;outline-offset:3px}
769
+ button:disabled{opacity:.6;cursor:not-allowed;transform:none}
770
+ button[aria-busy=true]{cursor:wait}
771
+ .mark{display:inline-flex;align-items:center;justify-content:center;flex:none;width:28px;height:28px;border-radius:50%;background:#000}
772
+ .txt{display:flex;flex-direction:column;align-items:flex-start;min-width:0}
773
+ .txt b{font-weight:700;white-space:nowrap}
774
+ .txt small{font-size:11px;font-weight:500;color:rgba(0,0,0,.65);white-space:nowrap}
775
+ .spin{width:16px;height:16px;border-radius:50%;border:2px solid rgba(255,210,77,.35);border-top-color:#ffd24d;animation:r .8s linear infinite}
776
+ @keyframes r{to{transform:rotate(360deg)}}
777
+ @media (prefers-reduced-motion:reduce){button{transition:none}button:active{transform:none}.spin{animation:none;border-color:#ffd24d;border-right-color:transparent}}
778
+ /* sizes */
779
+ :host([size=md]) button{padding:10px 16px;border-radius:10px;font-size:15px;gap:10px}
780
+ :host([size=md]) .mark{width:24px;height:24px}
781
+ :host([size=md]) .txt small{font-size:10.5px}
782
+ :host([size=sm]) button{padding:6px 12px 6px 6px;border-radius:8px;font-size:13px;gap:8px;min-height:36px}
783
+ :host([size=sm]) .mark{width:22px;height:22px}
784
+ :host([size=icon]) button{width:48px;height:48px;padding:0;border-radius:50%;gap:0}
785
+ :host([size=icon]) .mark{width:34px;height:34px}
786
+ /* themes */
787
+ :host([theme=dark]) button{background:#0a0a0a;color:#fafafa;border:1px solid #3f3f46;box-shadow:none}
788
+ :host([theme=dark]) button:hover{background:#18181b;border-color:rgba(251,191,36,.6)}
789
+ :host([theme=dark]) button:active{background:#000}
790
+ :host([theme=dark]) button:focus-visible{outline-color:#fcd34d}
791
+ :host([theme=dark]) .mark{box-shadow:inset 0 0 0 1px #3f3f46}
792
+ :host([theme=dark]) .txt small{color:rgba(255,255,255,.65)}
793
+ :host([theme=light]) button{background:#fff;color:#09090b;border:1px solid #d4d4d8;box-shadow:0 1px 2px rgba(0,0,0,.06)}
794
+ :host([theme=light]) button:hover{background:#fafafa;border-color:#f59e0b}
795
+ :host([theme=light]) button:active{background:#f4f4f5}
796
+ :host([theme=light]) .txt small{color:#52525b}
797
+ /* not found */
798
+ .nf{margin-top:8px;font-size:12px;line-height:1.4;color:#71717a;text-align:center}
799
+ .nf a{color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px}
800
+ .nf a:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:2px}
801
+ :host([theme=dark]) .nf,:host-context(.dark) .nf{color:#a1a1aa}
802
+ .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
803
+ `;
804
+ function b64url2(bytes) {
805
+ let s = "";
806
+ for (const b of bytes) s += String.fromCharCode(b);
807
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
808
+ }
809
+ async function buildAuthorizeUrl(p) {
810
+ const rnd = (n) => b64url2(crypto.getRandomValues(new Uint8Array(n)));
811
+ const verifier = rnd(32);
812
+ const state = rnd(16);
813
+ const nonce = rnd(16);
814
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)));
815
+ const u = new URL(p.authorizeUrl, typeof location !== "undefined" ? location.href : void 0);
816
+ const set = (k, v) => {
817
+ if (v) u.searchParams.set(k, v);
818
+ };
819
+ set("response_type", "code");
820
+ set("client_id", p.clientId);
821
+ set("redirect_uri", p.redirectUri);
822
+ set("scope", p.scope || "openid profile");
823
+ set("state", state);
824
+ set("nonce", nonce);
825
+ set("code_challenge", b64url2(digest));
826
+ set("code_challenge_method", "S256");
720
827
  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);
828
+ sessionStorage.setItem("bwalletx:oauth", JSON.stringify({ verifier, state, nonce }));
829
+ } catch {
728
830
  }
831
+ return u.toString();
729
832
  }
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
- }
739
- throw new PaymentError("BAD_REQUEST", "Give sats or usd");
740
- }
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;
755
- 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();
759
- } catch (e) {
760
- throw fail(e.message, e);
761
- }
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
- };
833
+ async function postJson(url, body) {
834
+ const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
835
+ const data = await res.json().catch(() => ({}));
836
+ if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
837
+ return data;
775
838
  }
776
- async function runAction(wallet, description, outputs, labels) {
777
- 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 }
783
- });
784
- } 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
- }
839
+ async function postAccess(url, body) {
840
+ const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
841
+ return res.json().catch(() => ({ error: "bad_request", message: `HTTP ${res.status}` }));
789
842
  }
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);
843
+ var OBSERVED = ["size", "theme", "label", "brand", "subtitle", "loading", "disabled", "full-width", "aria-label", "get-url", "web-url"];
844
+ function createClass() {
845
+ return class BwalletxSigninElement extends HTMLElement {
846
+ static get observedAttributes() {
847
+ return OBSERVED;
809
848
  }
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}`);
849
+ #root;
850
+ #notFound = false;
851
+ #busy = false;
852
+ constructor() {
853
+ super();
854
+ this.#root = this.attachShadow({ mode: "open" });
821
855
  }
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);
856
+ connectedCallback() {
857
+ this.#render();
858
+ }
859
+ attributeChangedCallback() {
860
+ if (this.isConnected) this.#render();
861
+ }
862
+ /** The inner <button>, for focus() and tests. */
863
+ get button() {
864
+ return this.#root.querySelector("button");
865
+ }
866
+ get notFound() {
867
+ return this.#notFound;
868
+ }
869
+ focus(opts) {
870
+ this.button?.focus(opts);
871
+ }
872
+ #attr(name) {
873
+ return this.getAttribute(name);
874
+ }
875
+ #render() {
876
+ const size = this.#attr("size") ?? "lg";
877
+ const brand = this.#attr("brand") === "bWallet" ? "bWallet" : "bWalletX";
878
+ const text = signinText(this.#attr("label") ?? "signin", brand);
879
+ const subAttr = this.#attr("subtitle");
880
+ 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;
883
+ const disabled = this.hasAttribute("disabled");
884
+ 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);
886
+ const getUrl = this.#attr("get-url") ?? GET_URL;
887
+ const webUrl = this.#attr("web-url") ?? WEB_WALLET_URL;
888
+ const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
889
+ const label = size === "icon" ? "" : `<span class="txt" part="text"><b part="label">${esc(text)}</b>${subtitle ? `<small part="subtitle">${esc(subtitle)}</small>` : ""}</span>`;
890
+ 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}`;
893
+ this.button.addEventListener("click", () => void this.#click());
894
+ }
895
+ #emit(type, detail, cancelable = false) {
896
+ return this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable }));
897
+ }
898
+ #setBusy(on) {
899
+ this.#busy = on;
900
+ this.#render();
901
+ }
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;
910
+ }
911
+ if (href) {
912
+ location.assign(href);
913
+ return;
914
+ }
915
+ const challengeUrl = this.#attr("challenge-url");
916
+ if (!challengeUrl) return;
917
+ this.#notFound = false;
918
+ this.#setBusy(true);
919
+ try {
920
+ const ms = Number(this.#attr("discover-ms")) || 1200;
921
+ const found = await discoverBwalletX(ms);
922
+ if (!found) {
923
+ this.#notFound = true;
924
+ this.#emit("bwalletx-not-found");
925
+ if (this.#attr("require-token")) this.#emit("bwalletx-access-denied", { code: "not_installed", message: "No wallet found" });
926
+ return;
927
+ }
928
+ const requireToken = this.#attr("require-token");
929
+ if (requireToken) {
930
+ const verifyUrl2 = this.#attr("verify-url");
931
+ const min = this.#attr("min");
932
+ const minBps = this.#attr("min-bps");
933
+ const hint = { token: { id: requireToken, ...minBps ? { minBps: Number(minBps) } : { min: min ?? "1" } } };
934
+ try {
935
+ const a = await requestAccess({
936
+ wallet: found.wallet,
937
+ challenge: (identityKey) => postJson(challengeUrl, { identityKey, require: hint }),
938
+ verify: verifyUrl2 ? (response2) => postAccess(verifyUrl2, response2) : void 0
939
+ });
940
+ this.#emit("bwalletx-access-granted", { identityKey: a.response.identityKey, response: a.verified });
941
+ } catch (e) {
942
+ const err = e instanceof AccessError ? e : new AccessError("bad_request", String(e));
943
+ this.#emit("bwalletx-access-denied", { code: err.code, message: err.message, have: err.have, need: err.need });
944
+ }
945
+ return;
946
+ }
947
+ const r = await signInWithBwalletX({ wallet: found.wallet, challenge: (identityKey) => postJson(challengeUrl, { identityKey }) });
948
+ 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 });
951
+ } catch (error) {
952
+ this.#emit("bwalletx-error", { error });
953
+ } finally {
954
+ this.#setBusy(false);
955
+ }
956
+ }
957
+ };
835
958
  }
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;
959
+ function defineBwalletxSignin(tag = SIGNIN_TAG) {
960
+ if (typeof window === "undefined" || typeof customElements === "undefined") return void 0;
961
+ const existing = customElements.get(tag);
962
+ if (existing) return existing;
963
+ const C = createClass();
964
+ customElements.define(tag, C);
965
+ return C;
846
966
  }
847
967
 
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}`);
968
+ // src/modal.ts
969
+ var CSS = `
970
+ .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}
971
+ .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)}
972
+ .bwx-box h2{margin:0 0 6px;font-size:18px;color:#e8c766}
973
+ .bwx-box p{margin:0 0 14px;color:#bdb6a3;font-size:14px}
974
+ .bwx-qr{background:#fff;border-radius:12px;padding:12px;display:flex;justify-content:center}
975
+ .bwx-qr svg{width:100%;height:auto;max-width:260px}
976
+ .bwx-code{margin:14px 0 0;text-align:center;font-size:28px;letter-spacing:.3em;color:#e8c766;font-variant-numeric:tabular-nums}
977
+ .bwx-row{display:flex;gap:8px;margin-top:14px}
978
+ .bwx-btn{flex:1;border:1px solid #3a3220;background:#15130e;color:#f4f1e8;border-radius:10px;padding:10px 12px;font:inherit;cursor:pointer}
979
+ .bwx-btn:hover{border-color:#e8c766}
980
+ a.bwx-btn{display:block;text-align:center;text-decoration:none;box-sizing:border-box}
981
+ .bwx-primary{background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;border:0;font-weight:700;min-height:48px;padding:13px 12px}
982
+ .bwx-stack{display:flex;flex-direction:column;gap:8px}
983
+ .bwx-list{display:flex;flex-direction:column;gap:8px}
984
+ .bwx-opt{display:flex;align-items:center;gap:10px;text-align:left}
985
+ .bwx-opt img{width:24px;height:24px;border-radius:6px}
986
+ .bwx-tag{margin-left:auto;font-size:11px;color:#8d866f}
987
+ `;
988
+ function mount() {
989
+ const root = document.createElement("div");
990
+ root.className = "bwx-ov";
991
+ root.setAttribute("role", "dialog");
992
+ root.setAttribute("aria-modal", "true");
993
+ const style = document.createElement("style");
994
+ style.textContent = CSS;
995
+ const box = document.createElement("div");
996
+ box.className = "bwx-box";
997
+ root.append(style, box);
998
+ document.body.appendChild(root);
999
+ return { root, box, close: () => root.remove() };
886
1000
  }
887
- function challengeBytes(challenge) {
888
- return Array.from(new TextEncoder().encode(challenge));
1001
+ function el(tag, cls, text) {
1002
+ const e = document.createElement(tag);
1003
+ if (cls) e.className = cls;
1004
+ if (text != null) e.textContent = text;
1005
+ return e;
889
1006
  }
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;
1007
+ function qrSvg(text) {
1008
+ const q = (0, import_qrcode_generator.default)(0, "M");
1009
+ q.addData(text);
1010
+ q.make();
1011
+ return q.createSvgTag({ cellSize: 4, margin: 2, scalable: true });
927
1012
  }
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;
1013
+ function isTouchDevice(env = globalThis) {
1014
+ const nav = env.navigator;
1015
+ if (!nav) return false;
1016
+ const ua = nav.userAgent ?? "";
1017
+ if (/Android|iPhone|iPad|iPod|Mobile|Silk|Kindle/i.test(ua)) return true;
1018
+ if (/Macintosh/.test(ua) && (nav.maxTouchPoints ?? 0) > 1) return true;
931
1019
  try {
932
- c = JSON.parse(s);
1020
+ return !!env.matchMedia?.("(pointer: coarse)").matches && !env.matchMedia?.("(hover: hover)").matches;
933
1021
  } 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");
1022
+ return false;
940
1023
  }
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
1024
  }
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");
1025
+ function pairingModal(o = {}) {
1026
+ const ac = new AbortController();
1027
+ let ui = null;
1028
+ let codeEl = null;
1029
+ const close = () => {
1030
+ ui?.close();
1031
+ ui = null;
1032
+ };
1033
+ ac.signal.addEventListener("abort", close);
1034
+ return {
1035
+ signal: ac.signal,
1036
+ close,
1037
+ onLink(link) {
1038
+ if (typeof document === "undefined") return;
1039
+ close();
1040
+ ui = mount();
1041
+ const app = o.mode === "app" || (o.mode ?? "auto") === "auto" && isTouchDevice();
1042
+ const h = el("h2", "", o.title ?? "Connect bWalletX");
1043
+ const cancel = el("button", "bwx-btn", "Cancel");
1044
+ cancel.onclick = () => {
1045
+ o.onCancel?.();
1046
+ ac.abort();
1047
+ };
1048
+ codeEl = el("div", "bwx-code", "");
1049
+ codeEl.setAttribute("aria-live", "polite");
1050
+ if (app) {
1051
+ const p2 = el("p", "", "Open bWalletX, approve, then come back to this page.");
1052
+ const stack = el("div", "bwx-stack");
1053
+ const open = el("a", "bwx-btn bwx-primary", "Open bWalletX");
1054
+ open.href = link;
1055
+ open.rel = "noopener";
1056
+ const web = el("button", "bwx-btn", "Use web.bwalletx.com instead");
1057
+ web.onclick = () => {
1058
+ void navigator.clipboard?.writeText(link).catch(() => void 0);
1059
+ window.open(WEB_WALLET_URL, "_blank", "noopener");
1060
+ p2.textContent = "Link copied. In web.bwalletx.com tap Connect and paste it, then come back here.";
1061
+ };
1062
+ stack.append(open, web, cancel);
1063
+ ui.box.append(h, p2, codeEl, stack);
1064
+ return;
1065
+ }
1066
+ const p = el("p", "", "Scan with bWalletX on your phone, or paste the link into web.bwalletx.com.");
1067
+ const qr = el("div", "bwx-qr");
1068
+ qr.innerHTML = qrSvg(link);
1069
+ const row = el("div", "bwx-row");
1070
+ const copy = el("button", "bwx-btn", "Copy link");
1071
+ copy.onclick = () => void navigator.clipboard?.writeText(link).then(() => copy.textContent = "Copied");
1072
+ row.append(copy, cancel);
1073
+ ui.box.append(h, p, qr, codeEl, row);
1074
+ },
1075
+ onCode(code) {
1076
+ if (codeEl) {
1077
+ codeEl.textContent = code;
1078
+ const p = ui?.box.querySelector("p");
1079
+ if (p) p.textContent = "Check bWalletX shows the same code, then tap Connect.";
1080
+ }
1081
+ }
1082
+ };
1083
+ }
1084
+ function pickWallet(options, o = {}) {
1085
+ if (typeof document === "undefined") return Promise.resolve(null);
1086
+ return new Promise((resolve) => {
1087
+ const ui = mount();
1088
+ const done = (w) => {
1089
+ ui.close();
1090
+ resolve(w);
1091
+ };
1092
+ ui.box.append(el("h2", "", o.title ?? "Choose a wallet"));
1093
+ if (!options.length) ui.box.append(el("p", "", "No wallet found. Get bWalletX at bwalletx.com."));
1094
+ const list = el("div", "bwx-list");
1095
+ for (const w of options) {
1096
+ const b = el("button", "bwx-btn bwx-opt");
1097
+ if (w.icon) {
1098
+ const img = el("img");
1099
+ img.src = w.icon;
1100
+ img.alt = "";
1101
+ b.append(img);
1102
+ }
1103
+ b.append(el("span", "", w.name));
1104
+ if (w.kind === "handcash") b.append(el("span", "bwx-tag", "until BRC-100"));
1105
+ b.onclick = () => done(w);
1106
+ list.append(b);
1107
+ }
1108
+ const cancel = el("button", "bwx-btn", "Cancel");
1109
+ cancel.onclick = () => done(null);
1110
+ ui.root.onclick = (e) => e.target === ui.root && done(null);
1111
+ ui.box.append(list, el("div", "bwx-row"));
1112
+ ui.box.lastChild.append(cancel);
1113
+ });
960
1114
  }
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 });
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 };
964
1123
  }
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;
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;
968
1135
  }
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;
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);
981
1146
  }
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;
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
+ };
997
1169
  }
998
- }
999
- return [...byKey.values()];
1170
+ };
1171
+ return opt;
1000
1172
  }
1001
- async function requestAccess(o) {
1002
- let wallet;
1173
+ var NAMES = { [RDNS_EXTENSION]: "bWalletX", "space.bwallet.mobile": "bWalletX" };
1174
+ async function probe(substrate, ms) {
1003
1175
  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);
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;
1007
1181
  }
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);
1182
+ }
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 }));
1195
+ };
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 }));
1026
1205
  }
1027
- return { response, verified, wallet };
1028
- } catch (e) {
1029
- throw toAccessError(e);
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)" }));
1030
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);
1031
1214
  }
1032
1215
 
1033
- // src/element.ts
1034
- var SIGNIN_TAG = "bwalletx-signin";
1035
- var GET_URL = "https://bwalletx.com/get";
1036
- var WEB_WALLET_URL = "https://web.bwalletx.com";
1037
- var VERBS = { signin: "Sign in with", continue: "Continue with", connect: "Connect" };
1038
- function signinText(label = "signin", brand = "bWalletX") {
1039
- return `${VERBS[label] ?? VERBS.signin} ${brand}`;
1040
- }
1041
- var uid = 0;
1042
- function markSvg(px = 20) {
1043
- const n = ++uid;
1044
- return `<svg viewBox="23 8 74 100" width="${Math.round(px * 0.74)}" height="${px}" aria-hidden="true" focusable="false"><defs><linearGradient id="bwxg${n}" gradientUnits="userSpaceOnUse" x1="0" y1="12" x2="0" y2="105"><stop offset="0" stop-color="#FFE58A"/><stop offset=".55" stop-color="#FFD24D"/><stop offset="1" stop-color="#C98F00"/></linearGradient><mask id="bwxh${n}"><rect width="140" height="140" fill="#fff"/><circle cx="60" cy="72" r="15" fill="#000"/></mask></defs><g fill="url(#bwxg${n})" mask="url(#bwxh${n})"><polygon points="45,12 45,76 27,76 27,30"/><circle cx="60" cy="72" r="33"/></g></svg>`;
1045
- }
1046
- var SIGNIN_CSS = `
1047
- :host{display:inline-block;vertical-align:middle;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
1048
- :host([full-width]){display:block;width:100%}
1049
- :host([hidden]){display:none}
1050
- button{all:unset;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:12px;width:100%;cursor:pointer;
1051
- padding:16px 20px;border-radius:12px;font-size:16px;font-weight:700;line-height:1.2;text-align:left;
1052
- background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;
1053
- box-shadow:0 10px 15px -3px rgba(245,158,11,.2),0 4px 6px -4px rgba(245,158,11,.2);
1054
- transition:background .15s,transform .1s,box-shadow .15s,border-color .15s;-webkit-tap-highlight-color:transparent}
1055
- button:hover{background:linear-gradient(180deg,#fde68a,#fbbf24)}
1056
- button:active{transform:translateY(1px);background:linear-gradient(180deg,#fbbf24,#d97706)}
1057
- button:focus-visible{outline:2px solid #b45309;outline-offset:3px}
1058
- button:disabled{opacity:.6;cursor:not-allowed;transform:none}
1059
- button[aria-busy=true]{cursor:wait}
1060
- .mark{display:inline-flex;align-items:center;justify-content:center;flex:none;width:28px;height:28px;border-radius:50%;background:#000}
1061
- .txt{display:flex;flex-direction:column;align-items:flex-start;min-width:0}
1062
- .txt b{font-weight:700;white-space:nowrap}
1063
- .txt small{font-size:11px;font-weight:500;color:rgba(0,0,0,.65);white-space:nowrap}
1064
- .spin{width:16px;height:16px;border-radius:50%;border:2px solid rgba(255,210,77,.35);border-top-color:#ffd24d;animation:r .8s linear infinite}
1065
- @keyframes r{to{transform:rotate(360deg)}}
1066
- @media (prefers-reduced-motion:reduce){button{transition:none}button:active{transform:none}.spin{animation:none;border-color:#ffd24d;border-right-color:transparent}}
1067
- /* sizes */
1068
- :host([size=md]) button{padding:10px 16px;border-radius:10px;font-size:15px;gap:10px}
1069
- :host([size=md]) .mark{width:24px;height:24px}
1070
- :host([size=md]) .txt small{font-size:10.5px}
1071
- :host([size=sm]) button{padding:6px 12px 6px 6px;border-radius:8px;font-size:13px;gap:8px;min-height:36px}
1072
- :host([size=sm]) .mark{width:22px;height:22px}
1073
- :host([size=icon]) button{width:48px;height:48px;padding:0;border-radius:50%;gap:0}
1074
- :host([size=icon]) .mark{width:34px;height:34px}
1075
- /* themes */
1076
- :host([theme=dark]) button{background:#0a0a0a;color:#fafafa;border:1px solid #3f3f46;box-shadow:none}
1077
- :host([theme=dark]) button:hover{background:#18181b;border-color:rgba(251,191,36,.6)}
1078
- :host([theme=dark]) button:active{background:#000}
1079
- :host([theme=dark]) button:focus-visible{outline-color:#fcd34d}
1080
- :host([theme=dark]) .mark{box-shadow:inset 0 0 0 1px #3f3f46}
1081
- :host([theme=dark]) .txt small{color:rgba(255,255,255,.65)}
1082
- :host([theme=light]) button{background:#fff;color:#09090b;border:1px solid #d4d4d8;box-shadow:0 1px 2px rgba(0,0,0,.06)}
1083
- :host([theme=light]) button:hover{background:#fafafa;border-color:#f59e0b}
1084
- :host([theme=light]) button:active{background:#f4f4f5}
1085
- :host([theme=light]) .txt small{color:#52525b}
1086
- /* not found */
1087
- .nf{margin-top:8px;font-size:12px;line-height:1.4;color:#71717a;text-align:center}
1088
- .nf a{color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px}
1089
- .nf a:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:2px}
1090
- :host([theme=dark]) .nf,:host-context(.dark) .nf{color:#a1a1aa}
1091
- .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
1092
- `;
1093
- function b64url2(bytes) {
1094
- let s = "";
1095
- for (const b of bytes) s += String.fromCharCode(b);
1096
- return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1097
- }
1098
- async function buildAuthorizeUrl(p) {
1099
- const rnd = (n) => b64url2(crypto.getRandomValues(new Uint8Array(n)));
1100
- const verifier = rnd(32);
1101
- const state = rnd(16);
1102
- const nonce = rnd(16);
1103
- const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)));
1104
- const u = new URL(p.authorizeUrl, typeof location !== "undefined" ? location.href : void 0);
1105
- const set = (k, v) => {
1106
- if (v) u.searchParams.set(k, v);
1107
- };
1108
- set("response_type", "code");
1109
- set("client_id", p.clientId);
1110
- set("redirect_uri", p.redirectUri);
1111
- set("scope", p.scope || "openid profile");
1112
- set("state", state);
1113
- set("nonce", nonce);
1114
- set("code_challenge", b64url2(digest));
1115
- set("code_challenge_method", "S256");
1216
+ // src/pay.ts
1217
+ var import_sdk6 = require("@bsv/sdk");
1218
+ var BRC29_PROTOCOL = [2, "3241645161d8"];
1219
+ var PaymentError = class extends Error {
1220
+ constructor(code, message, maybeSent = false, cause) {
1221
+ super(message);
1222
+ this.code = code;
1223
+ this.maybeSent = maybeSent;
1224
+ this.cause = cause;
1225
+ this.name = "PaymentError";
1226
+ }
1227
+ code;
1228
+ maybeSent;
1229
+ cause;
1230
+ };
1231
+ var isLegacy = (s) => typeof s.pay === "function" && typeof s.createAction !== "function";
1232
+ var isIdentityKeyHex = (s) => /^0[23][0-9a-fA-F]{64}$/.test(s);
1233
+ var isPaymail = (s) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
1234
+ function randomB64(n) {
1235
+ const b = new Uint8Array(n);
1236
+ globalThis.crypto.getRandomValues(b);
1237
+ return import_sdk6.Utils.toBase64(Array.from(b));
1238
+ }
1239
+ function opReturn(text) {
1240
+ const d = import_sdk6.Utils.toArray(text, "utf8");
1241
+ const len = d.length;
1242
+ const push = len < 76 ? [len] : len <= 255 ? [76, len] : [77, len & 255, len >> 8];
1243
+ return import_sdk6.Utils.toHex([0, 106, ...push, ...d]);
1244
+ }
1245
+ var clip = (s, n) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
1246
+ var describe = (s) => clip(s.length < 5 ? `${s} payment`.trim() : s, 50);
1247
+ async function usdRate(o) {
1248
+ if (typeof o.usdPerBsv === "number") return o.usdPerBsv;
1116
1249
  try {
1117
- sessionStorage.setItem("bwalletx:oauth", JSON.stringify({ verifier, state, nonce }));
1118
- } catch {
1250
+ if (typeof o.usdPerBsv === "function") return await o.usdPerBsv();
1251
+ const r = await (o.fetch ?? fetch)("https://api.whatsonchain.com/v1/bsv/main/exchangerate");
1252
+ const j = await r.json();
1253
+ if (!r.ok || !(Number(j.rate) > 0)) throw new Error(`status ${r.status}`);
1254
+ return Number(j.rate);
1255
+ } catch (e) {
1256
+ throw new PaymentError("RATE_FAILED", `Could not get the BSV/USD rate: ${e.message}`, false, e);
1119
1257
  }
1120
- return u.toString();
1121
1258
  }
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;
1259
+ async function amountInSats(req, o = {}) {
1260
+ if (req.sats != null) {
1261
+ if (!Number.isSafeInteger(req.sats) || req.sats <= 0) throw new PaymentError("BAD_REQUEST", "sats must be a positive integer");
1262
+ return req.sats;
1263
+ }
1264
+ if (req.usd != null) {
1265
+ if (!(req.usd > 0)) throw new PaymentError("BAD_REQUEST", "usd must be positive");
1266
+ return Math.max(1, Math.ceil(req.usd / await usdRate(o) * 1e8));
1267
+ }
1268
+ throw new PaymentError("BAD_REQUEST", "Give sats or usd");
1127
1269
  }
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}` }));
1270
+ async function resolvePaymailP2P(paymail, sats, f = fetch) {
1271
+ const [alias, domain] = paymail.split("@");
1272
+ const fail = (m, e) => new PaymentError("RESOLVE_FAILED", `Could not resolve ${paymail}: ${m}`, false, e);
1273
+ let caps;
1274
+ try {
1275
+ const r = await f(`https://${domain}/.well-known/bsvalias`);
1276
+ caps = (await r.json()).capabilities;
1277
+ } catch (e) {
1278
+ throw fail("no bsvalias document (or CORS blocked it; resolve on your server and pass resolvePaymail)", e);
1279
+ }
1280
+ const fill = (u) => u.replace("{alias}", encodeURIComponent(alias)).replace("{domain.tld}", domain);
1281
+ const dest = caps?.["2a40af698840"];
1282
+ if (!dest) throw fail("it does not offer P2P payment destinations");
1283
+ let body;
1284
+ try {
1285
+ const r = await f(fill(dest), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ satoshis: sats }) });
1286
+ if (!r.ok) throw new Error(`status ${r.status}`);
1287
+ body = await r.json();
1288
+ } catch (e) {
1289
+ throw fail(e.message, e);
1290
+ }
1291
+ if (!body.outputs?.length) throw fail("no outputs returned");
1292
+ const receive = caps["5f1323cddf31"];
1293
+ return {
1294
+ outputs: body.outputs.map((o) => ({ script: o.script, satoshis: o.satoshis })),
1295
+ reference: body.reference,
1296
+ submit: receive ? async (hex) => {
1297
+ await f(fill(receive), {
1298
+ method: "POST",
1299
+ headers: { "content-type": "application/json" },
1300
+ body: JSON.stringify({ hex, reference: body.reference, metadata: {} })
1301
+ });
1302
+ } : void 0
1303
+ };
1131
1304
  }
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();
1305
+ async function runAction(wallet, description, outputs, labels) {
1306
+ try {
1307
+ return await wallet.createAction({
1308
+ description: describe(description),
1309
+ outputs: outputs.map((o) => ({ lockingScript: o.script, satoshis: o.satoshis, outputDescription: describe(o.description ?? "payment") })),
1310
+ labels: labels?.length ? labels : void 0,
1311
+ options: { randomizeOutputs: false }
1312
+ });
1313
+ } catch (e) {
1314
+ const msg = e?.message ?? String(e);
1315
+ const rejected = /denied|reject|cancel|abort|user/i.test(msg) || e?.code === 4001;
1316
+ 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);
1317
+ }
1318
+ }
1319
+ async function pay(source, req, o = {}) {
1320
+ const to = (req.to ?? "").trim();
1321
+ if (!to) throw new PaymentError("BAD_REQUEST", "Give a recipient (to)");
1322
+ if (req.memo && req.memo.length > 1e3) throw new PaymentError("BAD_REQUEST", "memo is too long");
1323
+ const sats = await amountInSats(req, o);
1324
+ if (isLegacy(source)) return source.pay({ ...req, to, sats });
1325
+ let outputs;
1326
+ let remittance;
1327
+ let submit;
1328
+ if (isIdentityKeyHex(to)) {
1329
+ const derivationPrefix = randomB64(8);
1330
+ const derivationSuffix = randomB64(8);
1331
+ let pk;
1332
+ let me;
1333
+ try {
1334
+ ({ publicKey: pk } = await source.getPublicKey({ protocolID: BRC29_PROTOCOL, keyID: `${derivationPrefix} ${derivationSuffix}`, counterparty: to.toLowerCase() }));
1335
+ ({ publicKey: me } = await source.getPublicKey({ identityKey: true }));
1336
+ } catch (e) {
1337
+ throw new PaymentError("WALLET_ERROR", `The wallet could not derive the payment key: ${e.message}`, false, e);
1190
1338
  }
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
- }
1339
+ outputs = [{ script: new import_sdk6.P2PKH().lock(import_sdk6.PublicKey.fromString(pk).toHash()).toHex(), satoshis: sats, description: "payment" }];
1340
+ remittance = { senderIdentityKey: me, derivationPrefix, derivationSuffix, outputIndex: 0 };
1341
+ } else if (isPaymail(to)) {
1342
+ const r2 = await (o.resolvePaymail ?? ((p, s) => resolvePaymailP2P(p, s, o.fetch ?? fetch)))(to, sats);
1343
+ outputs = r2.outputs;
1344
+ submit = r2.submit;
1345
+ } else {
1346
+ try {
1347
+ outputs = [{ script: new import_sdk6.P2PKH().lock(to).toHex(), satoshis: sats, description: "payment" }];
1348
+ } catch {
1349
+ throw new PaymentError("BAD_REQUEST", `Not a paymail, address or identity key: ${to}`);
1245
1350
  }
1246
- };
1351
+ }
1352
+ if (req.memo && req.memoOnChain) outputs = [...outputs, { script: opReturn(req.memo), satoshis: 0, description: "memo" }];
1353
+ const r = await runAction(source, req.memo || `Pay ${clip(to, 30)}`, outputs, req.labels);
1354
+ if (!r.txid) throw new PaymentError("WALLET_ERROR", "The wallet did not return a txid", true);
1355
+ const tx = r.tx ? Array.from(r.tx) : void 0;
1356
+ if (submit && tx) {
1357
+ await submit(import_sdk6.Transaction.fromAtomicBEEF(tx).toHex()).catch(() => {
1358
+ });
1359
+ }
1360
+ return { txid: r.txid, tx, sats, outputs, remittance };
1247
1361
  }
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;
1362
+ function tip(source, req, o = {}) {
1363
+ return pay(source, { memo: "Tip", ...req, labels: [...req.labels ?? [], "tip"] }, o);
1364
+ }
1365
+ async function buyToken(source, req) {
1366
+ if (isLegacy(source)) throw new PaymentError("NOT_SUPPORTED", "This wallet cannot buy tokens. Use a BRC-100 wallet such as bWalletX.");
1367
+ const q = await req.quote({ token: req.token, amount: req.amount });
1368
+ if (!q?.outputs?.length) throw new PaymentError("BAD_REQUEST", "The quote returned no outputs");
1369
+ const sats = q.outputs.reduce((n, x) => n + x.satoshis, 0);
1370
+ const r = await runAction(source, q.description ?? `Buy ${clip(req.token, 30)}`, q.outputs, [...req.labels ?? [], "token-buy"]);
1371
+ if (!r.txid) throw new PaymentError("WALLET_ERROR", "The wallet did not return a txid", true);
1372
+ const res = { txid: r.txid, tx: r.tx ? Array.from(r.tx) : void 0, sats, outputs: q.outputs };
1373
+ await q.onPaid?.(res);
1374
+ return res;
1255
1375
  }
1256
1376
 
1257
1377
  // src/react.ts
@@ -1333,6 +1453,12 @@ function BwalletXProvider(p) {
1333
1453
  props.current.onError?.(err);
1334
1454
  return err;
1335
1455
  }, []);
1456
+ const themeOn = !!p.theme && (state.status === "connecting" || state.status === "connected");
1457
+ (0, import_react.useEffect)(() => {
1458
+ if (!themeOn) return;
1459
+ declareWalletTheme(p.theme);
1460
+ return () => declareWalletTheme(null);
1461
+ }, [themeOn, p.theme]);
1336
1462
  (0, import_react.useEffect)(() => {
1337
1463
  const url = p.sessionUrl;
1338
1464
  if (!url) return;
@@ -1383,7 +1509,7 @@ function BwalletXProvider(p) {
1383
1509
  wallet: cw.wallet,
1384
1510
  challenge: (identityKey) => postJson2(c.challengeUrl, { identityKey })
1385
1511
  });
1386
- const v = await postJson2(c.verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature });
1512
+ const v = await atStage("verify", () => postJson2(c.verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature }));
1387
1513
  handle = v.handle ?? handle;
1388
1514
  paymail = v.paymail ?? paymail;
1389
1515
  signedIn = true;