@bwalletx/connect 0.4.1 → 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/CHANGELOG.md +20 -0
- package/README.md +6 -2
- package/assets/protocol/401.jpg +0 -0
- package/assets/protocol/401.mp4 +0 -0
- package/assets/protocol/402.jpg +0 -0
- package/assets/protocol/402.mp4 +0 -0
- package/assets/protocol/403.jpg +0 -0
- package/assets/protocol/403.mp4 +0 -0
- package/dist/access-Dp0eKIOc.d.cts +118 -0
- package/dist/access-Dp0eKIOc.d.ts +118 -0
- package/dist/bwalletx-connect.global.js +20 -17
- package/dist/bwalletx-connect.global.js.map +1 -1
- package/dist/{chunk-NNMUSWPG.js → chunk-IUDD7YTQ.js} +200 -10
- package/dist/chunk-IUDD7YTQ.js.map +1 -0
- package/dist/chunk-MWD4BCO5.js +182 -0
- package/dist/chunk-MWD4BCO5.js.map +1 -0
- package/dist/{chunk-FUQ46RJV.js → chunk-PI42H3XY.js} +65 -10
- package/dist/chunk-PI42H3XY.js.map +1 -0
- package/dist/{chunk-7NAKOJGZ.js → chunk-QRXFL66W.js} +220 -10
- package/dist/chunk-QRXFL66W.js.map +1 -0
- package/dist/element.cjs +290 -14
- package/dist/element.cjs.map +1 -1
- package/dist/element.js +2 -1
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +437 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -1
- package/dist/index.d.ts +71 -1
- package/dist/index.js +36 -4
- package/dist/index.js.map +1 -1
- package/dist/next.cjs +377 -9
- package/dist/next.cjs.map +1 -1
- package/dist/next.d.cts +52 -2
- package/dist/next.d.ts +52 -2
- package/dist/next.js +92 -1
- package/dist/next.js.map +1 -1
- package/dist/react.cjs +561 -226
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +5 -0
- package/dist/react.d.ts +5 -0
- package/dist/react.js +12 -3
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +406 -9
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +111 -1
- package/dist/server.d.ts +111 -1
- package/dist/server.js +47 -1
- package/package.json +12 -11
- package/dist/chunk-7NAKOJGZ.js.map +0 -1
- package/dist/chunk-FUQ46RJV.js.map +0 -1
- package/dist/chunk-NNMUSWPG.js.map +0 -1
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
|
-
|
|
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,19 +530,442 @@ 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
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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);
|
|
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}`);
|
|
597
|
+
}
|
|
598
|
+
function challengeBytes(challenge) {
|
|
599
|
+
return Array.from(new TextEncoder().encode(challenge));
|
|
600
|
+
}
|
|
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");
|
|
622
|
+
}
|
|
623
|
+
out.token.minBps = t.minBps;
|
|
624
|
+
}
|
|
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");
|
|
629
|
+
}
|
|
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;
|
|
657
|
+
}
|
|
658
|
+
|
|
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");
|
|
671
|
+
}
|
|
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 });
|
|
675
|
+
}
|
|
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;
|
|
679
|
+
}
|
|
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;
|
|
692
|
+
}
|
|
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()];
|
|
711
|
+
}
|
|
712
|
+
async function requestAccess(o) {
|
|
713
|
+
let wallet;
|
|
714
|
+
try {
|
|
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);
|
|
718
|
+
}
|
|
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);
|
|
737
|
+
}
|
|
738
|
+
return { response, verified, wallet };
|
|
739
|
+
} catch (e) {
|
|
740
|
+
throw toAccessError(e);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
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}`;
|
|
751
|
+
}
|
|
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>`;
|
|
756
|
+
}
|
|
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");
|
|
827
|
+
try {
|
|
828
|
+
sessionStorage.setItem("bwalletx:oauth", JSON.stringify({ verifier, state, nonce }));
|
|
829
|
+
} catch {
|
|
830
|
+
}
|
|
831
|
+
return u.toString();
|
|
832
|
+
}
|
|
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;
|
|
838
|
+
}
|
|
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}` }));
|
|
842
|
+
}
|
|
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;
|
|
848
|
+
}
|
|
849
|
+
#root;
|
|
850
|
+
#notFound = false;
|
|
851
|
+
#busy = false;
|
|
852
|
+
constructor() {
|
|
853
|
+
super();
|
|
854
|
+
this.#root = this.attachShadow({ mode: "open" });
|
|
855
|
+
}
|
|
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) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[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
|
+
};
|
|
958
|
+
}
|
|
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;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/modal.ts
|
|
472
969
|
var CSS = `
|
|
473
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}
|
|
474
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)}
|
|
@@ -480,6 +977,9 @@ var CSS = `
|
|
|
480
977
|
.bwx-row{display:flex;gap:8px;margin-top:14px}
|
|
481
978
|
.bwx-btn{flex:1;border:1px solid #3a3220;background:#15130e;color:#f4f1e8;border-radius:10px;padding:10px 12px;font:inherit;cursor:pointer}
|
|
482
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}
|
|
483
983
|
.bwx-list{display:flex;flex-direction:column;gap:8px}
|
|
484
984
|
.bwx-opt{display:flex;align-items:center;gap:10px;text-align:left}
|
|
485
985
|
.bwx-opt img{width:24px;height:24px;border-radius:6px}
|
|
@@ -510,6 +1010,18 @@ function qrSvg(text) {
|
|
|
510
1010
|
q.make();
|
|
511
1011
|
return q.createSvgTag({ cellSize: 4, margin: 2, scalable: true });
|
|
512
1012
|
}
|
|
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;
|
|
1019
|
+
try {
|
|
1020
|
+
return !!env.matchMedia?.("(pointer: coarse)").matches && !env.matchMedia?.("(hover: hover)").matches;
|
|
1021
|
+
} catch {
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
513
1025
|
function pairingModal(o = {}) {
|
|
514
1026
|
const ac = new AbortController();
|
|
515
1027
|
let ui = null;
|
|
@@ -526,20 +1038,37 @@ function pairingModal(o = {}) {
|
|
|
526
1038
|
if (typeof document === "undefined") return;
|
|
527
1039
|
close();
|
|
528
1040
|
ui = mount();
|
|
1041
|
+
const app = o.mode === "app" || (o.mode ?? "auto") === "auto" && isTouchDevice();
|
|
529
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
|
+
}
|
|
530
1066
|
const p = el("p", "", "Scan with bWalletX on your phone, or paste the link into web.bwalletx.com.");
|
|
531
1067
|
const qr = el("div", "bwx-qr");
|
|
532
1068
|
qr.innerHTML = qrSvg(link);
|
|
533
|
-
codeEl = el("div", "bwx-code", "");
|
|
534
|
-
codeEl.setAttribute("aria-live", "polite");
|
|
535
1069
|
const row = el("div", "bwx-row");
|
|
536
1070
|
const copy = el("button", "bwx-btn", "Copy link");
|
|
537
1071
|
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
1072
|
row.append(copy, cancel);
|
|
544
1073
|
ui.box.append(h, p, qr, codeEl, row);
|
|
545
1074
|
},
|
|
@@ -585,7 +1114,7 @@ function pickWallet(options, o = {}) {
|
|
|
585
1114
|
}
|
|
586
1115
|
|
|
587
1116
|
// src/wallets.ts
|
|
588
|
-
var
|
|
1117
|
+
var import_sdk5 = require("@bsv/sdk");
|
|
589
1118
|
async function brc100Connected(wallet, o) {
|
|
590
1119
|
await wallet.waitForAuthentication({}).catch(() => {
|
|
591
1120
|
});
|
|
@@ -644,7 +1173,7 @@ function handcashAdapter(o) {
|
|
|
644
1173
|
var NAMES = { [RDNS_EXTENSION]: "bWalletX", "space.bwallet.mobile": "bWalletX" };
|
|
645
1174
|
async function probe(substrate, ms) {
|
|
646
1175
|
try {
|
|
647
|
-
const w = new
|
|
1176
|
+
const w = new import_sdk5.WalletClient(substrate);
|
|
648
1177
|
const ok = await Promise.race([w.isAuthenticated({}).then(() => true), new Promise((r) => setTimeout(() => r(false), ms))]);
|
|
649
1178
|
return ok ? w : null;
|
|
650
1179
|
} catch {
|
|
@@ -685,7 +1214,7 @@ async function discoverWallets(o = {}) {
|
|
|
685
1214
|
}
|
|
686
1215
|
|
|
687
1216
|
// src/pay.ts
|
|
688
|
-
var
|
|
1217
|
+
var import_sdk6 = require("@bsv/sdk");
|
|
689
1218
|
var BRC29_PROTOCOL = [2, "3241645161d8"];
|
|
690
1219
|
var PaymentError = class extends Error {
|
|
691
1220
|
constructor(code, message, maybeSent = false, cause) {
|
|
@@ -705,13 +1234,13 @@ var isPaymail = (s) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
|
|
|
705
1234
|
function randomB64(n) {
|
|
706
1235
|
const b = new Uint8Array(n);
|
|
707
1236
|
globalThis.crypto.getRandomValues(b);
|
|
708
|
-
return
|
|
1237
|
+
return import_sdk6.Utils.toBase64(Array.from(b));
|
|
709
1238
|
}
|
|
710
1239
|
function opReturn(text) {
|
|
711
|
-
const d =
|
|
1240
|
+
const d = import_sdk6.Utils.toArray(text, "utf8");
|
|
712
1241
|
const len = d.length;
|
|
713
1242
|
const push = len < 76 ? [len] : len <= 255 ? [76, len] : [77, len & 255, len >> 8];
|
|
714
|
-
return
|
|
1243
|
+
return import_sdk6.Utils.toHex([0, 106, ...push, ...d]);
|
|
715
1244
|
}
|
|
716
1245
|
var clip = (s, n) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
|
|
717
1246
|
var describe = (s) => clip(s.length < 5 ? `${s} payment`.trim() : s, 50);
|
|
@@ -807,7 +1336,7 @@ async function pay(source, req, o = {}) {
|
|
|
807
1336
|
} catch (e) {
|
|
808
1337
|
throw new PaymentError("WALLET_ERROR", `The wallet could not derive the payment key: ${e.message}`, false, e);
|
|
809
1338
|
}
|
|
810
|
-
outputs = [{ script: new
|
|
1339
|
+
outputs = [{ script: new import_sdk6.P2PKH().lock(import_sdk6.PublicKey.fromString(pk).toHash()).toHex(), satoshis: sats, description: "payment" }];
|
|
811
1340
|
remittance = { senderIdentityKey: me, derivationPrefix, derivationSuffix, outputIndex: 0 };
|
|
812
1341
|
} else if (isPaymail(to)) {
|
|
813
1342
|
const r2 = await (o.resolvePaymail ?? ((p, s) => resolvePaymailP2P(p, s, o.fetch ?? fetch)))(to, sats);
|
|
@@ -815,7 +1344,7 @@ async function pay(source, req, o = {}) {
|
|
|
815
1344
|
submit = r2.submit;
|
|
816
1345
|
} else {
|
|
817
1346
|
try {
|
|
818
|
-
outputs = [{ script: new
|
|
1347
|
+
outputs = [{ script: new import_sdk6.P2PKH().lock(to).toHex(), satoshis: sats, description: "payment" }];
|
|
819
1348
|
} catch {
|
|
820
1349
|
throw new PaymentError("BAD_REQUEST", `Not a paymail, address or identity key: ${to}`);
|
|
821
1350
|
}
|
|
@@ -825,7 +1354,7 @@ async function pay(source, req, o = {}) {
|
|
|
825
1354
|
if (!r.txid) throw new PaymentError("WALLET_ERROR", "The wallet did not return a txid", true);
|
|
826
1355
|
const tx = r.tx ? Array.from(r.tx) : void 0;
|
|
827
1356
|
if (submit && tx) {
|
|
828
|
-
await submit(
|
|
1357
|
+
await submit(import_sdk6.Transaction.fromAtomicBEEF(tx).toHex()).catch(() => {
|
|
829
1358
|
});
|
|
830
1359
|
}
|
|
831
1360
|
return { txid: r.txid, tx, sats, outputs, remittance };
|
|
@@ -845,206 +1374,6 @@ async function buyToken(source, req) {
|
|
|
845
1374
|
return res;
|
|
846
1375
|
}
|
|
847
1376
|
|
|
848
|
-
// src/element.ts
|
|
849
|
-
var SIGNIN_TAG = "bwalletx-signin";
|
|
850
|
-
var GET_URL = "https://bwalletx.com/get";
|
|
851
|
-
var WEB_WALLET_URL = "https://web.bwalletx.com";
|
|
852
|
-
var VERBS = { signin: "Sign in with", continue: "Continue with", connect: "Connect" };
|
|
853
|
-
function signinText(label = "signin", brand = "bWalletX") {
|
|
854
|
-
return `${VERBS[label] ?? VERBS.signin} ${brand}`;
|
|
855
|
-
}
|
|
856
|
-
var uid = 0;
|
|
857
|
-
function markSvg(px = 20) {
|
|
858
|
-
const n = ++uid;
|
|
859
|
-
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>`;
|
|
860
|
-
}
|
|
861
|
-
var SIGNIN_CSS = `
|
|
862
|
-
:host{display:inline-block;vertical-align:middle;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
|
863
|
-
:host([full-width]){display:block;width:100%}
|
|
864
|
-
:host([hidden]){display:none}
|
|
865
|
-
button{all:unset;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:12px;width:100%;cursor:pointer;
|
|
866
|
-
padding:16px 20px;border-radius:12px;font-size:16px;font-weight:700;line-height:1.2;text-align:left;
|
|
867
|
-
background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;
|
|
868
|
-
box-shadow:0 10px 15px -3px rgba(245,158,11,.2),0 4px 6px -4px rgba(245,158,11,.2);
|
|
869
|
-
transition:background .15s,transform .1s,box-shadow .15s,border-color .15s;-webkit-tap-highlight-color:transparent}
|
|
870
|
-
button:hover{background:linear-gradient(180deg,#fde68a,#fbbf24)}
|
|
871
|
-
button:active{transform:translateY(1px);background:linear-gradient(180deg,#fbbf24,#d97706)}
|
|
872
|
-
button:focus-visible{outline:2px solid #b45309;outline-offset:3px}
|
|
873
|
-
button:disabled{opacity:.6;cursor:not-allowed;transform:none}
|
|
874
|
-
button[aria-busy=true]{cursor:wait}
|
|
875
|
-
.mark{display:inline-flex;align-items:center;justify-content:center;flex:none;width:28px;height:28px;border-radius:50%;background:#000}
|
|
876
|
-
.txt{display:flex;flex-direction:column;align-items:flex-start;min-width:0}
|
|
877
|
-
.txt b{font-weight:700;white-space:nowrap}
|
|
878
|
-
.txt small{font-size:11px;font-weight:500;color:rgba(0,0,0,.65);white-space:nowrap}
|
|
879
|
-
.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}
|
|
880
|
-
@keyframes r{to{transform:rotate(360deg)}}
|
|
881
|
-
@media (prefers-reduced-motion:reduce){button{transition:none}button:active{transform:none}.spin{animation:none;border-color:#ffd24d;border-right-color:transparent}}
|
|
882
|
-
/* sizes */
|
|
883
|
-
:host([size=md]) button{padding:10px 16px;border-radius:10px;font-size:15px;gap:10px}
|
|
884
|
-
:host([size=md]) .mark{width:24px;height:24px}
|
|
885
|
-
:host([size=md]) .txt small{font-size:10.5px}
|
|
886
|
-
:host([size=sm]) button{padding:6px 12px 6px 6px;border-radius:8px;font-size:13px;gap:8px;min-height:36px}
|
|
887
|
-
:host([size=sm]) .mark{width:22px;height:22px}
|
|
888
|
-
:host([size=icon]) button{width:48px;height:48px;padding:0;border-radius:50%;gap:0}
|
|
889
|
-
:host([size=icon]) .mark{width:34px;height:34px}
|
|
890
|
-
/* themes */
|
|
891
|
-
:host([theme=dark]) button{background:#0a0a0a;color:#fafafa;border:1px solid #3f3f46;box-shadow:none}
|
|
892
|
-
:host([theme=dark]) button:hover{background:#18181b;border-color:rgba(251,191,36,.6)}
|
|
893
|
-
:host([theme=dark]) button:active{background:#000}
|
|
894
|
-
:host([theme=dark]) button:focus-visible{outline-color:#fcd34d}
|
|
895
|
-
:host([theme=dark]) .mark{box-shadow:inset 0 0 0 1px #3f3f46}
|
|
896
|
-
:host([theme=dark]) .txt small{color:rgba(255,255,255,.65)}
|
|
897
|
-
:host([theme=light]) button{background:#fff;color:#09090b;border:1px solid #d4d4d8;box-shadow:0 1px 2px rgba(0,0,0,.06)}
|
|
898
|
-
:host([theme=light]) button:hover{background:#fafafa;border-color:#f59e0b}
|
|
899
|
-
:host([theme=light]) button:active{background:#f4f4f5}
|
|
900
|
-
:host([theme=light]) .txt small{color:#52525b}
|
|
901
|
-
/* not found */
|
|
902
|
-
.nf{margin-top:8px;font-size:12px;line-height:1.4;color:#71717a;text-align:center}
|
|
903
|
-
.nf a{color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px}
|
|
904
|
-
.nf a:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:2px}
|
|
905
|
-
:host([theme=dark]) .nf,:host-context(.dark) .nf{color:#a1a1aa}
|
|
906
|
-
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
907
|
-
`;
|
|
908
|
-
function b64url2(bytes) {
|
|
909
|
-
let s = "";
|
|
910
|
-
for (const b of bytes) s += String.fromCharCode(b);
|
|
911
|
-
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
912
|
-
}
|
|
913
|
-
async function buildAuthorizeUrl(p) {
|
|
914
|
-
const rnd = (n) => b64url2(crypto.getRandomValues(new Uint8Array(n)));
|
|
915
|
-
const verifier = rnd(32);
|
|
916
|
-
const state = rnd(16);
|
|
917
|
-
const nonce = rnd(16);
|
|
918
|
-
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)));
|
|
919
|
-
const u = new URL(p.authorizeUrl, typeof location !== "undefined" ? location.href : void 0);
|
|
920
|
-
const set = (k, v) => {
|
|
921
|
-
if (v) u.searchParams.set(k, v);
|
|
922
|
-
};
|
|
923
|
-
set("response_type", "code");
|
|
924
|
-
set("client_id", p.clientId);
|
|
925
|
-
set("redirect_uri", p.redirectUri);
|
|
926
|
-
set("scope", p.scope || "openid profile");
|
|
927
|
-
set("state", state);
|
|
928
|
-
set("nonce", nonce);
|
|
929
|
-
set("code_challenge", b64url2(digest));
|
|
930
|
-
set("code_challenge_method", "S256");
|
|
931
|
-
try {
|
|
932
|
-
sessionStorage.setItem("bwalletx:oauth", JSON.stringify({ verifier, state, nonce }));
|
|
933
|
-
} catch {
|
|
934
|
-
}
|
|
935
|
-
return u.toString();
|
|
936
|
-
}
|
|
937
|
-
async function postJson(url, body) {
|
|
938
|
-
const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
939
|
-
const data = await res.json().catch(() => ({}));
|
|
940
|
-
if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
941
|
-
return data;
|
|
942
|
-
}
|
|
943
|
-
var OBSERVED = ["size", "theme", "label", "brand", "subtitle", "loading", "disabled", "full-width", "aria-label", "get-url", "web-url"];
|
|
944
|
-
function createClass() {
|
|
945
|
-
return class BwalletxSigninElement extends HTMLElement {
|
|
946
|
-
static get observedAttributes() {
|
|
947
|
-
return OBSERVED;
|
|
948
|
-
}
|
|
949
|
-
#root;
|
|
950
|
-
#notFound = false;
|
|
951
|
-
#busy = false;
|
|
952
|
-
constructor() {
|
|
953
|
-
super();
|
|
954
|
-
this.#root = this.attachShadow({ mode: "open" });
|
|
955
|
-
}
|
|
956
|
-
connectedCallback() {
|
|
957
|
-
this.#render();
|
|
958
|
-
}
|
|
959
|
-
attributeChangedCallback() {
|
|
960
|
-
if (this.isConnected) this.#render();
|
|
961
|
-
}
|
|
962
|
-
/** The inner <button>, for focus() and tests. */
|
|
963
|
-
get button() {
|
|
964
|
-
return this.#root.querySelector("button");
|
|
965
|
-
}
|
|
966
|
-
get notFound() {
|
|
967
|
-
return this.#notFound;
|
|
968
|
-
}
|
|
969
|
-
focus(opts) {
|
|
970
|
-
this.button?.focus(opts);
|
|
971
|
-
}
|
|
972
|
-
#attr(name) {
|
|
973
|
-
return this.getAttribute(name);
|
|
974
|
-
}
|
|
975
|
-
#render() {
|
|
976
|
-
const size = this.#attr("size") ?? "lg";
|
|
977
|
-
const brand = this.#attr("brand") === "bWallet" ? "bWallet" : "bWalletX";
|
|
978
|
-
const text = signinText(this.#attr("label") ?? "signin", brand);
|
|
979
|
-
const subAttr = this.#attr("subtitle");
|
|
980
|
-
const subDefault = brand === "bWalletX" && (size === "lg" || size === "md") ? "also works with bWallet" : null;
|
|
981
|
-
const subtitle = subAttr === null || subAttr === "" || subAttr === "on" ? subAttr === "on" ? "also works with bWallet" : subDefault : subAttr === "off" ? null : subAttr;
|
|
982
|
-
const loading = this.hasAttribute("loading") || this.#busy;
|
|
983
|
-
const disabled = this.hasAttribute("disabled");
|
|
984
|
-
const markPx = size === "icon" ? 24 : size === "sm" ? 15 : size === "md" ? 17 : 20;
|
|
985
|
-
const aria = this.#attr("aria-label") ?? (subtitle && size !== "icon" ? `${text}, ${subtitle}` : text);
|
|
986
|
-
const getUrl = this.#attr("get-url") ?? GET_URL;
|
|
987
|
-
const webUrl = this.#attr("web-url") ?? WEB_WALLET_URL;
|
|
988
|
-
const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
989
|
-
const label = size === "icon" ? "" : `<span class="txt" part="text"><b part="label">${esc(text)}</b>${subtitle ? `<small part="subtitle">${esc(subtitle)}</small>` : ""}</span>`;
|
|
990
|
-
const mark = loading ? '<span class="spin" aria-hidden="true"></span>' : markSvg(markPx);
|
|
991
|
-
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>` : "";
|
|
992
|
-
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}`;
|
|
993
|
-
this.button.addEventListener("click", () => void this.#click());
|
|
994
|
-
}
|
|
995
|
-
#emit(type, detail, cancelable = false) {
|
|
996
|
-
return this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable }));
|
|
997
|
-
}
|
|
998
|
-
#setBusy(on) {
|
|
999
|
-
this.#busy = on;
|
|
1000
|
-
this.#render();
|
|
1001
|
-
}
|
|
1002
|
-
async #click() {
|
|
1003
|
-
if (this.hasAttribute("disabled") || this.hasAttribute("loading") || this.#busy) return;
|
|
1004
|
-
if (!this.#emit("bwalletx-signin", { element: this }, true)) return;
|
|
1005
|
-
const href = this.#attr("href");
|
|
1006
|
-
const authorizeUrl = this.#attr("authorize-url");
|
|
1007
|
-
if (authorizeUrl) {
|
|
1008
|
-
location.assign(await buildAuthorizeUrl({ authorizeUrl, clientId: this.#attr("client-id"), redirectUri: this.#attr("redirect-uri"), scope: this.#attr("scope") }));
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
if (href) {
|
|
1012
|
-
location.assign(href);
|
|
1013
|
-
return;
|
|
1014
|
-
}
|
|
1015
|
-
const challengeUrl = this.#attr("challenge-url");
|
|
1016
|
-
if (!challengeUrl) return;
|
|
1017
|
-
this.#notFound = false;
|
|
1018
|
-
this.#setBusy(true);
|
|
1019
|
-
try {
|
|
1020
|
-
const ms = Number(this.#attr("discover-ms")) || 1200;
|
|
1021
|
-
const found = await discoverBwalletX(ms);
|
|
1022
|
-
if (!found) {
|
|
1023
|
-
this.#notFound = true;
|
|
1024
|
-
this.#emit("bwalletx-not-found");
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
const r = await signInWithBwalletX({ wallet: found.wallet, challenge: (identityKey) => postJson(challengeUrl, { identityKey }) });
|
|
1028
|
-
const verifyUrl = this.#attr("verify-url");
|
|
1029
|
-
const response = verifyUrl ? await postJson(verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature }) : null;
|
|
1030
|
-
this.#emit("bwalletx-signed-in", { identityKey: r.identityKey, method: found.method, nonce: r.nonce, signature: r.signature, response });
|
|
1031
|
-
} catch (error) {
|
|
1032
|
-
this.#emit("bwalletx-error", { error });
|
|
1033
|
-
} finally {
|
|
1034
|
-
this.#setBusy(false);
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
};
|
|
1038
|
-
}
|
|
1039
|
-
function defineBwalletxSignin(tag = SIGNIN_TAG) {
|
|
1040
|
-
if (typeof window === "undefined" || typeof customElements === "undefined") return void 0;
|
|
1041
|
-
const existing = customElements.get(tag);
|
|
1042
|
-
if (existing) return existing;
|
|
1043
|
-
const C = createClass();
|
|
1044
|
-
customElements.define(tag, C);
|
|
1045
|
-
return C;
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
1377
|
// src/react.ts
|
|
1049
1378
|
function BwalletxSignin(props) {
|
|
1050
1379
|
const ref = (0, import_react.useRef)(null);
|
|
@@ -1124,6 +1453,12 @@ function BwalletXProvider(p) {
|
|
|
1124
1453
|
props.current.onError?.(err);
|
|
1125
1454
|
return err;
|
|
1126
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]);
|
|
1127
1462
|
(0, import_react.useEffect)(() => {
|
|
1128
1463
|
const url = p.sessionUrl;
|
|
1129
1464
|
if (!url) return;
|
|
@@ -1174,7 +1509,7 @@ function BwalletXProvider(p) {
|
|
|
1174
1509
|
wallet: cw.wallet,
|
|
1175
1510
|
challenge: (identityKey) => postJson2(c.challengeUrl, { identityKey })
|
|
1176
1511
|
});
|
|
1177
|
-
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 }));
|
|
1178
1513
|
handle = v.handle ?? handle;
|
|
1179
1514
|
paymail = v.paymail ?? paymail;
|
|
1180
1515
|
signedIn = true;
|