@bwalletx/connect 0.5.0 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,10 @@
1
1
  import {
2
2
  BWALLETX_RDNS,
3
3
  RDNS_EXTENSION,
4
+ WEB_WALLET_URL,
4
5
  pairBwalletX,
5
6
  restorePairing
6
- } from "./chunk-CVLXFNAG.js";
7
+ } from "./chunk-EXRQJTXF.js";
7
8
 
8
9
  // src/pay.ts
9
10
  import { P2PKH, PublicKey, Transaction, Utils } from "@bsv/sdk";
@@ -235,7 +236,7 @@ async function probe(substrate, ms) {
235
236
  }
236
237
  async function discoverWallets(o = {}) {
237
238
  const out = [];
238
- const ms = o.ms ?? 1200;
239
+ const ms = o.ms ?? 300;
239
240
  if (typeof window !== "undefined") {
240
241
  const seen = /* @__PURE__ */ new Map();
241
242
  const onAnnounce = (e) => {
@@ -249,7 +250,7 @@ async function discoverWallets(o = {}) {
249
250
  window.addEventListener("brc100:announceWallet", onAnnounce);
250
251
  window.dispatchEvent(new Event("brc100:requestWallet"));
251
252
  const local = o.probeLocal ? Promise.all([probe("json-api", ms), probe("Cicada", ms)]) : Promise.resolve([null, null]);
252
- await new Promise((r) => setTimeout(r, ms));
253
+ await new Promise((r) => setTimeout(r, seen.size > 0 || window.CWI ? Math.min(ms, 50) : ms));
253
254
  window.removeEventListener("brc100:announceWallet", onAnnounce);
254
255
  out.push(...seen.values());
255
256
  const cwi = window.CWI;
@@ -279,6 +280,9 @@ var CSS = `
279
280
  .bwx-row{display:flex;gap:8px;margin-top:14px}
280
281
  .bwx-btn{flex:1;border:1px solid #3a3220;background:#15130e;color:#f4f1e8;border-radius:10px;padding:10px 12px;font:inherit;cursor:pointer}
281
282
  .bwx-btn:hover{border-color:#e8c766}
283
+ a.bwx-btn{display:block;text-align:center;text-decoration:none;box-sizing:border-box}
284
+ .bwx-primary{background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;border:0;font-weight:700;min-height:48px;padding:13px 12px}
285
+ .bwx-stack{display:flex;flex-direction:column;gap:8px}
282
286
  .bwx-list{display:flex;flex-direction:column;gap:8px}
283
287
  .bwx-opt{display:flex;align-items:center;gap:10px;text-align:left}
284
288
  .bwx-opt img{width:24px;height:24px;border-radius:6px}
@@ -309,6 +313,18 @@ function qrSvg(text) {
309
313
  q.make();
310
314
  return q.createSvgTag({ cellSize: 4, margin: 2, scalable: true });
311
315
  }
316
+ function isTouchDevice(env = globalThis) {
317
+ const nav = env.navigator;
318
+ if (!nav) return false;
319
+ const ua = nav.userAgent ?? "";
320
+ if (/Android|iPhone|iPad|iPod|Mobile|Silk|Kindle/i.test(ua)) return true;
321
+ if (/Macintosh/.test(ua) && (nav.maxTouchPoints ?? 0) > 1) return true;
322
+ try {
323
+ return !!env.matchMedia?.("(pointer: coarse)").matches && !env.matchMedia?.("(hover: hover)").matches;
324
+ } catch {
325
+ return false;
326
+ }
327
+ }
312
328
  function pairingModal(o = {}) {
313
329
  const ac = new AbortController();
314
330
  let ui = null;
@@ -325,20 +341,37 @@ function pairingModal(o = {}) {
325
341
  if (typeof document === "undefined") return;
326
342
  close();
327
343
  ui = mount();
344
+ const app = o.mode === "app" || (o.mode ?? "auto") === "auto" && isTouchDevice();
328
345
  const h = el("h2", "", o.title ?? "Connect bWalletX");
346
+ const cancel = el("button", "bwx-btn", "Cancel");
347
+ cancel.onclick = () => {
348
+ o.onCancel?.();
349
+ ac.abort();
350
+ };
351
+ codeEl = el("div", "bwx-code", "");
352
+ codeEl.setAttribute("aria-live", "polite");
353
+ if (app) {
354
+ const p2 = el("p", "", "Open bWalletX, approve, then come back to this page.");
355
+ const stack = el("div", "bwx-stack");
356
+ const open = el("a", "bwx-btn bwx-primary", "Open bWalletX");
357
+ open.href = link;
358
+ open.rel = "noopener";
359
+ const web = el("button", "bwx-btn", "Use web.bwalletx.com instead");
360
+ web.onclick = () => {
361
+ void navigator.clipboard?.writeText(link).catch(() => void 0);
362
+ window.open(WEB_WALLET_URL, "_blank", "noopener");
363
+ p2.textContent = "Link copied. In web.bwalletx.com tap Connect and paste it, then come back here.";
364
+ };
365
+ stack.append(open, web, cancel);
366
+ ui.box.append(h, p2, codeEl, stack);
367
+ return;
368
+ }
329
369
  const p = el("p", "", "Scan with bWalletX on your phone, or paste the link into web.bwalletx.com.");
330
370
  const qr = el("div", "bwx-qr");
331
371
  qr.innerHTML = qrSvg(link);
332
- codeEl = el("div", "bwx-code", "");
333
- codeEl.setAttribute("aria-live", "polite");
334
372
  const row = el("div", "bwx-row");
335
373
  const copy = el("button", "bwx-btn", "Copy link");
336
374
  copy.onclick = () => void navigator.clipboard?.writeText(link).then(() => copy.textContent = "Copied");
337
- const cancel = el("button", "bwx-btn", "Cancel");
338
- cancel.onclick = () => {
339
- o.onCancel?.();
340
- ac.abort();
341
- };
342
375
  row.append(copy, cancel);
343
376
  ui.box.append(h, p, qr, codeEl, row);
344
377
  },
@@ -383,6 +416,25 @@ function pickWallet(options, o = {}) {
383
416
  });
384
417
  }
385
418
 
419
+ // src/theme.ts
420
+ var WALLET_THEME_META = "bwalletx-theme";
421
+ var ID = /^[a-z0-9-]{1,32}$/;
422
+ function declareWalletTheme(theme) {
423
+ if (typeof document === "undefined") return;
424
+ const id = (theme ?? "").trim().toLowerCase();
425
+ let meta = document.head.querySelector(`meta[name="${WALLET_THEME_META}"]`);
426
+ if (!id || !ID.test(id)) {
427
+ meta?.remove();
428
+ return;
429
+ }
430
+ if (!meta) {
431
+ meta = document.createElement("meta");
432
+ meta.name = WALLET_THEME_META;
433
+ document.head.appendChild(meta);
434
+ }
435
+ if (meta.content !== id) meta.content = id;
436
+ }
437
+
386
438
  export {
387
439
  BRC29_PROTOCOL,
388
440
  PaymentError,
@@ -398,7 +450,10 @@ export {
398
450
  handcashAdapter,
399
451
  discoverWallets,
400
452
  qrSvg,
453
+ isTouchDevice,
401
454
  pairingModal,
402
- pickWallet
455
+ pickWallet,
456
+ WALLET_THEME_META,
457
+ declareWalletTheme
403
458
  };
404
- //# sourceMappingURL=chunk-GF6GUDZL.js.map
459
+ //# sourceMappingURL=chunk-RIOHG5JH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pay.ts","../src/wallets.ts","../src/modal.ts","../src/theme.ts"],"sourcesContent":["/**\n * Browser-side payment helpers on top of the BRC-100 wallet's createAction. Works with any\n * BRC-100 wallet. Each helper asks the wallet once; it never retries a spend. If a call fails\n * the error says whether the wallet may have sent anything (`maybeSent`).\n */\nimport { P2PKH, PublicKey, Transaction, Utils, type WalletInterface } from '@bsv/sdk';\n\n/** BRC-29 payment protocol id. */\nexport const BRC29_PROTOCOL: [2, string] = [2, '3241645161d8'];\n\nexport type PayTo = string; // paymail (name@domain), P2PKH address, or 66-hex identity key\n\nexport interface PayOutput {\n /** Locking script hex. */\n script: string;\n satoshis: number;\n description?: string;\n}\n\nexport interface PayRequest {\n to: PayTo;\n /** Amount in satoshis. Give this or `usd`. */\n sats?: number;\n /** Amount in US dollars, converted at `usdPerBsv` (or the live WhatsOnChain rate). */\n usd?: number;\n /** Shown in the wallet and, with `memoOnChain`, written in an OP_RETURN output. */\n memo?: string;\n memoOnChain?: boolean;\n /** Labels for the wallet's history. */\n labels?: string[];\n}\n\nexport interface PayOptions {\n /** Fixed rate for `usd`. Otherwise fetched from WhatsOnChain. */\n usdPerBsv?: number | (() => Promise<number>);\n /** Resolve a paymail to outputs. Default: bsvalias P2P payment destination (needs CORS on the paymail host). */\n resolvePaymail?: (paymail: string, sats: number) => Promise<{ outputs: PayOutput[]; reference?: string; submit?: (rawTxHex: string) => Promise<void> }>;\n fetch?: typeof fetch;\n}\n\nexport interface Brc29Remittance {\n senderIdentityKey: string;\n derivationPrefix: string;\n derivationSuffix: string;\n outputIndex: number;\n}\n\nexport interface PayResult {\n txid: string;\n /** Atomic BEEF (BRC-95) if the wallet returned it. Send it to your server's verifyPayment. */\n tx?: number[];\n sats: number;\n outputs: PayOutput[];\n /** For an identity-key payment: what the recipient needs to internalize the output (BRC-29). Deliver it to them. */\n remittance?: Brc29Remittance;\n}\n\nexport type PaymentErrorCode = 'BAD_REQUEST' | 'RESOLVE_FAILED' | 'RATE_FAILED' | 'WALLET_REJECTED' | 'WALLET_ERROR' | 'NOT_SUPPORTED';\n\nexport class PaymentError extends Error {\n constructor(\n readonly code: PaymentErrorCode,\n message: string,\n /** true when the wallet was asked to spend and the outcome is unknown. Check before trying again. */\n readonly maybeSent = false,\n readonly cause?: unknown,\n ) {\n super(message);\n this.name = 'PaymentError';\n }\n}\n\n/** A wallet that is not BRC-100 (the HandCash transitional adapter) can still pay through this. */\nexport interface LegacyPayer {\n pay(req: PayRequest & { sats: number }): Promise<PayResult>;\n}\nexport type PaySource = WalletInterface | LegacyPayer;\n\nconst isLegacy = (s: PaySource): s is LegacyPayer => typeof (s as LegacyPayer).pay === 'function' && typeof (s as WalletInterface).createAction !== 'function';\n\nexport const isIdentityKeyHex = (s: string) => /^0[23][0-9a-fA-F]{64}$/.test(s);\nexport const isPaymail = (s: string) => /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(s);\n\nfunction randomB64(n: number) {\n const b = new Uint8Array(n);\n globalThis.crypto.getRandomValues(b);\n return Utils.toBase64(Array.from(b));\n}\n\nfunction opReturn(text: string): string {\n const d = Utils.toArray(text, 'utf8');\n const len = d.length;\n const push = len < 0x4c ? [len] : len <= 0xff ? [0x4c, len] : [0x4d, len & 0xff, len >> 8];\n return Utils.toHex([0x00, 0x6a, ...push, ...d]);\n}\n\nconst clip = (s: string, n: number) => (s.length > n ? s.slice(0, n - 1) + '…' : s);\n/** createAction descriptions must be 5–50 bytes. */\nconst describe = (s: string) => clip(s.length < 5 ? `${s} payment`.trim() : s, 50);\n\nasync function usdRate(o: PayOptions): Promise<number> {\n if (typeof o.usdPerBsv === 'number') return o.usdPerBsv;\n try {\n if (typeof o.usdPerBsv === 'function') return await o.usdPerBsv();\n const r = await (o.fetch ?? fetch)('https://api.whatsonchain.com/v1/bsv/main/exchangerate');\n const j = (await r.json()) as { rate?: number };\n if (!r.ok || !(Number(j.rate) > 0)) throw new Error(`status ${r.status}`);\n return Number(j.rate);\n } catch (e) {\n throw new PaymentError('RATE_FAILED', `Could not get the BSV/USD rate: ${(e as Error).message}`, false, e);\n }\n}\n\n/** Satoshis for a request (sats as given, or usd converted, rounded up). */\nexport async function amountInSats(req: Pick<PayRequest, 'sats' | 'usd'>, o: PayOptions = {}): Promise<number> {\n if (req.sats != null) {\n if (!Number.isSafeInteger(req.sats) || req.sats <= 0) throw new PaymentError('BAD_REQUEST', 'sats must be a positive integer');\n return req.sats;\n }\n if (req.usd != null) {\n if (!(req.usd > 0)) throw new PaymentError('BAD_REQUEST', 'usd must be positive');\n return Math.max(1, Math.ceil((req.usd / (await usdRate(o))) * 1e8));\n }\n throw new PaymentError('BAD_REQUEST', 'Give sats or usd');\n}\n\n/** Default paymail resolver: bsvalias P2P Payment Destination (BRC-? / capability 2a40af698840). */\nexport async function resolvePaymailP2P(paymail: string, sats: number, f: typeof fetch = fetch) {\n const [alias, domain] = paymail.split('@');\n const fail = (m: string, e?: unknown) => new PaymentError('RESOLVE_FAILED', `Could not resolve ${paymail}: ${m}`, false, e);\n let caps: Record<string, string>;\n try {\n const r = await f(`https://${domain}/.well-known/bsvalias`);\n caps = ((await r.json()) as { capabilities: Record<string, string> }).capabilities;\n } catch (e) {\n throw fail('no bsvalias document (or CORS blocked it; resolve on your server and pass resolvePaymail)', e);\n }\n const fill = (u: string) => u.replace('{alias}', encodeURIComponent(alias)).replace('{domain.tld}', domain);\n const dest = caps?.['2a40af698840'];\n if (!dest) throw fail('it does not offer P2P payment destinations');\n let body: { outputs?: { script: string; satoshis: number }[]; reference?: string };\n try {\n const r = await f(fill(dest), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ satoshis: sats }) });\n if (!r.ok) throw new Error(`status ${r.status}`);\n body = await r.json();\n } catch (e) {\n throw fail((e as Error).message, e);\n }\n if (!body.outputs?.length) throw fail('no outputs returned');\n const receive = caps['5f1323cddf31'];\n return {\n outputs: body.outputs.map((o) => ({ script: o.script, satoshis: o.satoshis })),\n reference: body.reference,\n submit: receive\n ? async (hex: string) => {\n await f(fill(receive), {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ hex, reference: body.reference, metadata: {} }),\n });\n }\n : undefined,\n };\n}\n\nasync function runAction(wallet: WalletInterface, description: string, outputs: PayOutput[], labels?: string[]) {\n try {\n return await wallet.createAction({\n description: describe(description),\n outputs: outputs.map((o) => ({ lockingScript: o.script, satoshis: o.satoshis, outputDescription: describe(o.description ?? 'payment') })),\n labels: labels?.length ? labels : undefined,\n options: { randomizeOutputs: false },\n });\n } catch (e) {\n const msg = (e as Error)?.message ?? String(e);\n const rejected = /denied|reject|cancel|abort|user/i.test(msg) || (e as { code?: unknown })?.code === 4001;\n // Not retried: a lost reply could mean the wallet already broadcast it.\n 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);\n }\n}\n\n/** Pay a paymail, address or identity key. One wallet prompt; never retried. */\nexport async function pay(source: PaySource, req: PayRequest, o: PayOptions = {}): Promise<PayResult> {\n const to = (req.to ?? '').trim();\n if (!to) throw new PaymentError('BAD_REQUEST', 'Give a recipient (to)');\n if (req.memo && req.memo.length > 1000) throw new PaymentError('BAD_REQUEST', 'memo is too long');\n const sats = await amountInSats(req, o);\n if (isLegacy(source)) return source.pay({ ...req, to, sats });\n\n let outputs: PayOutput[];\n let remittance: Brc29Remittance | undefined;\n let submit: ((hex: string) => Promise<void>) | undefined;\n if (isIdentityKeyHex(to)) {\n const derivationPrefix = randomB64(8);\n const derivationSuffix = randomB64(8);\n let pk: string;\n let me: string;\n try {\n ({ publicKey: pk } = await source.getPublicKey({ protocolID: BRC29_PROTOCOL, keyID: `${derivationPrefix} ${derivationSuffix}`, counterparty: to.toLowerCase() }));\n ({ publicKey: me } = await source.getPublicKey({ identityKey: true }));\n } catch (e) {\n throw new PaymentError('WALLET_ERROR', `The wallet could not derive the payment key: ${(e as Error).message}`, false, e);\n }\n outputs = [{ script: new P2PKH().lock(PublicKey.fromString(pk).toHash() as number[]).toHex(), satoshis: sats, description: 'payment' }];\n remittance = { senderIdentityKey: me, derivationPrefix, derivationSuffix, outputIndex: 0 };\n } else if (isPaymail(to)) {\n const r = await (o.resolvePaymail ?? ((p, s) => resolvePaymailP2P(p, s, o.fetch ?? fetch)))(to, sats);\n outputs = r.outputs;\n submit = r.submit;\n } else {\n try {\n outputs = [{ script: new P2PKH().lock(to).toHex(), satoshis: sats, description: 'payment' }];\n } catch {\n throw new PaymentError('BAD_REQUEST', `Not a paymail, address or identity key: ${to}`);\n }\n }\n if (req.memo && req.memoOnChain) outputs = [...outputs, { script: opReturn(req.memo), satoshis: 0, description: 'memo' }];\n\n const r = await runAction(source, req.memo || `Pay ${clip(to, 30)}`, outputs, req.labels);\n if (!r.txid) throw new PaymentError('WALLET_ERROR', 'The wallet did not return a txid', true);\n const tx = r.tx ? Array.from(r.tx) : undefined;\n if (submit && tx) {\n // Tell the paymail host about the tx. Best effort: the payment itself is already made.\n await submit(Transaction.fromAtomicBEEF(tx).toHex()).catch(() => {});\n }\n return { txid: r.txid, tx, sats, outputs, remittance };\n}\n\n/** pay() labelled as a tip. */\nexport function tip(source: PaySource, req: PayRequest, o: PayOptions = {}): Promise<PayResult> {\n return pay(source, { memo: 'Tip', ...req, labels: [...(req.labels ?? []), 'tip'] }, o);\n}\n\nexport interface TokenQuote {\n /** Outputs to fund, as your server priced them (e.g. payment to the seller + fee). */\n outputs: PayOutput[];\n description?: string;\n /** Called with the result after the wallet signs; e.g. POST txid + tx to your server for verifyPayment. */\n onPaid?: (r: PayResult) => Promise<void> | void;\n}\n\nexport interface BuyTokenRequest {\n token: string;\n amount: number | string;\n /** The hook point: return the outputs for this purchase (usually from your server). */\n quote: (req: { token: string; amount: number | string }) => Promise<TokenQuote>;\n labels?: string[];\n}\n\n/** Buy a token: your `quote` hook prices it, the wallet pays once, `onPaid` delivers it. */\nexport async function buyToken(source: PaySource, req: BuyTokenRequest): Promise<PayResult> {\n if (isLegacy(source)) throw new PaymentError('NOT_SUPPORTED', 'This wallet cannot buy tokens. Use a BRC-100 wallet such as bWalletX.');\n const q = await req.quote({ token: req.token, amount: req.amount });\n if (!q?.outputs?.length) throw new PaymentError('BAD_REQUEST', 'The quote returned no outputs');\n const sats = q.outputs.reduce((n, x) => n + x.satoshis, 0);\n const r = await runAction(source, q.description ?? `Buy ${clip(req.token, 30)}`, q.outputs, [...(req.labels ?? []), 'token-buy']);\n if (!r.txid) throw new PaymentError('WALLET_ERROR', 'The wallet did not return a txid', true);\n const res: PayResult = { txid: r.txid, tx: r.tx ? Array.from(r.tx) : undefined, sats, outputs: q.outputs };\n await q.onPaid?.(res);\n return res;\n}\n","/**\n * Every wallet the user could connect with, behind one interface. BRC-100 first: bWalletX, then any\n * other wallet that announces itself (Yours, Metanet, ...), the in-app browser, optional localhost\n * desktop wallets, phone pairing, and app-supplied adapters (e.g. the transitional HandCash one).\n */\nimport { WalletClient, type WalletInterface } from '@bsv/sdk';\nimport { BWALLETX_RDNS, RDNS_EXTENSION } from './discover.js';\nimport { pairBwalletX, restorePairing, type PairingOptions } from './pair.js';\nimport type { PaySource } from './pay.js';\n\nexport type WalletKind = 'brc100' | 'pairing' | 'handcash' | 'custom';\n\nexport interface ConnectedWallet {\n /** The BRC-100 wallet. null for a non-BRC-100 adapter (HandCash today). */\n wallet: WalletInterface | null;\n /** What pay/tip use: the BRC-100 wallet, or the adapter's own payer. */\n payer: PaySource | null;\n /** The identity public key (hex) when the wallet has one. */\n identityKey: string | null;\n handle?: string;\n paymail?: string;\n /** Which option connected. */\n optionId: string;\n kind: WalletKind;\n /** True for adapters kept only until the vendor ships BRC-100. */\n transitional?: boolean;\n}\n\nexport interface WalletOption {\n id: string;\n name: string;\n icon?: string;\n kind: WalletKind;\n rdns?: string;\n /** True for bWalletX (extension, in-app or paired). */\n isBwalletX: boolean;\n connect(): Promise<ConnectedWallet>;\n}\n\nasync function brc100Connected(wallet: WalletInterface, o: Pick<WalletOption, 'id' | 'kind'>): Promise<ConnectedWallet> {\n await wallet.waitForAuthentication({}).catch(() => {});\n const { publicKey } = await wallet.getPublicKey({ identityKey: true });\n return { wallet, payer: wallet, identityKey: publicKey, optionId: o.id, kind: o.kind };\n}\n\n/** Wrap any BRC-100 wallet as a picker option. */\nexport function brc100Option(wallet: WalletInterface, info: { id: string; name: string; icon?: string; rdns?: string; isBwalletX?: boolean }): WalletOption {\n const opt: WalletOption = {\n id: info.id,\n name: info.name,\n icon: info.icon,\n rdns: info.rdns,\n kind: 'brc100',\n isBwalletX: info.isBwalletX ?? false,\n connect: () => brc100Connected(wallet, opt),\n };\n return opt;\n}\n\n/** Phone / web.bwalletx.com pairing as an option (shows a QR through `pairing`). Reuses a stored pairing first. */\nexport function pairingOption(pairing: PairingOptions | (() => PairingOptions)): WalletOption {\n const opt: WalletOption = {\n id: 'bwalletx-pairing',\n name: 'bWalletX on your phone',\n kind: 'pairing',\n isBwalletX: true,\n async connect() {\n const p = typeof pairing === 'function' ? pairing() : pairing;\n const w = (await restorePairing(p)) ?? (await pairBwalletX(p));\n return brc100Connected(w, opt);\n },\n };\n return opt;\n}\n\nexport interface HandCashAdapterOptions {\n /**\n * Your existing HandCash Connect login (redirect/popup + your server route). Resolve with the\n * profile once the user is signed in. This package does not import any HandCash SDK.\n */\n login(): Promise<{ handle: string; paymail?: string; identityKey?: string }>;\n /** Your existing HandCash pay call (usually a server route using the auth token). Optional. */\n pay?: Extract<PaySource, { pay: unknown }>['pay'];\n icon?: string;\n}\n\n/**\n * TRANSITIONAL. Keeps existing HandCash users working behind the same picker until HandCash's\n * BRC-100 support ships; then remove this and HandCash appears as an ordinary BRC-100 wallet.\n */\nexport function handcashAdapter(o: HandCashAdapterOptions): WalletOption {\n const opt: WalletOption = {\n id: 'handcash',\n name: 'HandCash',\n icon: o.icon,\n kind: 'handcash',\n isBwalletX: false,\n async connect() {\n const p = await o.login();\n return {\n wallet: null,\n payer: o.pay ? { pay: o.pay } : null,\n identityKey: p.identityKey ?? null,\n handle: p.handle,\n paymail: p.paymail ?? `${p.handle.replace(/^\\$/, '')}@handcash.io`,\n optionId: opt.id,\n kind: 'handcash',\n transitional: true,\n };\n },\n };\n return opt;\n}\n\nexport interface DiscoverWalletsOptions {\n /** Longest wait for BRC-100 announcements, ms. Default 300; ends early once a wallet has announced. */\n ms?: number;\n /**\n * Also probe desktop wallets on localhost (Metanet / json-api on 3321, Cicada on 3301).\n * Off by default: some browsers prompt for local-network access.\n */\n probeLocal?: boolean;\n /** Add the phone pairing option (needs a way to show the QR, e.g. pairingModal()). */\n pairing?: PairingOptions | (() => PairingOptions);\n /** App-supplied options appended at the end (e.g. handcashAdapter(...)). */\n extra?: WalletOption[];\n}\n\nconst NAMES: Record<string, string> = { [RDNS_EXTENSION]: 'bWalletX', 'space.bwallet.mobile': 'bWalletX' };\n\nasync function probe(substrate: 'json-api' | 'Cicada', ms: number): Promise<WalletInterface | null> {\n try {\n const w = new WalletClient(substrate);\n const ok = await Promise.race([w.isAuthenticated({}).then(() => true), new Promise<boolean>((r) => setTimeout(() => r(false), ms))]);\n return ok ? w : null;\n } catch {\n return null;\n }\n}\n\n/** All wallets we can see, bWalletX first. SSR-safe: resolves [] (plus `extra`) outside a browser. */\nexport async function discoverWallets(o: DiscoverWalletsOptions = {}): Promise<WalletOption[]> {\n const out: WalletOption[] = [];\n const ms = o.ms ?? 300;\n if (typeof window !== 'undefined') {\n const seen = new Map<string, WalletOption>();\n const onAnnounce = (e: Event) => {\n const d = (e as CustomEvent).detail as { info?: { rdns?: string; name?: string; icon?: string; uuid?: string }; wallet?: WalletInterface } | undefined;\n if (!d?.wallet) return;\n const rdns = d.info?.rdns ?? d.info?.uuid ?? `wallet-${seen.size}`;\n if (seen.has(rdns)) return;\n const isB = (BWALLETX_RDNS as readonly string[]).includes(rdns);\n seen.set(rdns, brc100Option(d.wallet, { id: rdns, rdns, name: NAMES[rdns] ?? d.info?.name ?? rdns, icon: d.info?.icon, isBwalletX: isB }));\n };\n window.addEventListener('brc100:announceWallet', onAnnounce);\n window.dispatchEvent(new Event('brc100:requestWallet'));\n const local = o.probeLocal ? Promise.all([probe('json-api', ms), probe('Cicada', ms)]) : Promise.resolve([null, null]);\n // Wallets answer requestWallet synchronously; once one has, a short grace catches any slower sibling.\n await new Promise((r) => setTimeout(r, seen.size > 0 || (window as unknown as { CWI?: unknown }).CWI ? Math.min(ms, 50) : ms));\n window.removeEventListener('brc100:announceWallet', onAnnounce);\n out.push(...seen.values());\n const cwi = (window as unknown as { CWI?: WalletInterface }).CWI;\n if (cwi && ![...seen.values()].some((x) => x.rdns === 'space.bwallet.mobile')) {\n out.push(brc100Option(cwi, { id: 'window.CWI', name: 'This app’s wallet', isBwalletX: false }));\n }\n const [json, cicada] = await local;\n if (json) out.push(brc100Option(json, { id: 'localhost:3321', name: 'Desktop wallet (localhost:3321)' }));\n if (cicada) out.push(brc100Option(cicada, { id: 'localhost:3301', name: 'Desktop wallet (localhost:3301)' }));\n }\n if (o.pairing) out.push(pairingOption(o.pairing));\n out.push(...(o.extra ?? []));\n // bWalletX first, transitional last; otherwise keep discovery order.\n const rank = (w: WalletOption) => (w.isBwalletX ? 0 : w.kind === 'handcash' ? 2 : 1);\n return out.map((w, i) => ({ w, i })).sort((a, b) => rank(a.w) - rank(b.w) || a.i - b.i).map((x) => x.w);\n}\n","/**\n * Built-in UI: the pairing QR modal and the wallet picker. Plain DOM, no framework, only created\n * when called in a browser.\n */\nimport qrcode from 'qrcode-generator';\nimport type { PairingOptions } from './pair.js';\nimport type { WalletOption } from './wallets.js';\nimport { WEB_WALLET_URL } from './element.js';\n\nconst CSS = `\n.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}\n.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)}\n.bwx-box h2{margin:0 0 6px;font-size:18px;color:#e8c766}\n.bwx-box p{margin:0 0 14px;color:#bdb6a3;font-size:14px}\n.bwx-qr{background:#fff;border-radius:12px;padding:12px;display:flex;justify-content:center}\n.bwx-qr svg{width:100%;height:auto;max-width:260px}\n.bwx-code{margin:14px 0 0;text-align:center;font-size:28px;letter-spacing:.3em;color:#e8c766;font-variant-numeric:tabular-nums}\n.bwx-row{display:flex;gap:8px;margin-top:14px}\n.bwx-btn{flex:1;border:1px solid #3a3220;background:#15130e;color:#f4f1e8;border-radius:10px;padding:10px 12px;font:inherit;cursor:pointer}\n.bwx-btn:hover{border-color:#e8c766}\na.bwx-btn{display:block;text-align:center;text-decoration:none;box-sizing:border-box}\n.bwx-primary{background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;border:0;font-weight:700;min-height:48px;padding:13px 12px}\n.bwx-stack{display:flex;flex-direction:column;gap:8px}\n.bwx-list{display:flex;flex-direction:column;gap:8px}\n.bwx-opt{display:flex;align-items:center;gap:10px;text-align:left}\n.bwx-opt img{width:24px;height:24px;border-radius:6px}\n.bwx-tag{margin-left:auto;font-size:11px;color:#8d866f}\n`;\n\nfunction mount(): { root: HTMLDivElement; box: HTMLDivElement; close: () => void } {\n const root = document.createElement('div');\n root.className = 'bwx-ov';\n root.setAttribute('role', 'dialog');\n root.setAttribute('aria-modal', 'true');\n const style = document.createElement('style');\n style.textContent = CSS;\n const box = document.createElement('div');\n box.className = 'bwx-box';\n root.append(style, box);\n document.body.appendChild(root);\n return { root, box, close: () => root.remove() };\n}\n\nfunction el<K extends keyof HTMLElementTagNameMap>(tag: K, cls?: string, text?: string) {\n const e = document.createElement(tag);\n if (cls) e.className = cls;\n if (text != null) e.textContent = text;\n return e;\n}\n\n/** An SVG QR code for `text` (error correction M). */\nexport function qrSvg(text: string): string {\n const q = qrcode(0, 'M');\n q.addData(text);\n q.make();\n return q.createSvgTag({ cellSize: 4, margin: 2, scalable: true });\n}\n\n\n/**\n * True on a phone or tablet: a coarse pointer with no hover, or a mobile user agent (iPadOS reports\n * a Mac UA, so touch points count too). Scanning a QR shown on the same screen is impossible there.\n */\nexport function isTouchDevice(env: { navigator?: Navigator; matchMedia?: (q: string) => MediaQueryList } = globalThis as never): boolean {\n const nav = env.navigator;\n if (!nav) return false;\n const ua = nav.userAgent ?? '';\n if (/Android|iPhone|iPad|iPod|Mobile|Silk|Kindle/i.test(ua)) return true;\n if (/Macintosh/.test(ua) && (nav.maxTouchPoints ?? 0) > 1) return true;\n try {\n return !!env.matchMedia?.('(pointer: coarse)').matches && !env.matchMedia?.('(hover: hover)').matches;\n } catch {\n return false;\n }\n}\n\nexport interface PairingModalOptions {\n title?: string;\n /**\n * 'qr' shows the pairing QR (desktop). 'app' hands off to the bWalletX app on this device: the\n * pairing link is a universal link / Android app link that opens the app, and the bwallet.space\n * page behind it explains what to do when the app is not installed. Default 'auto': 'app' on touch\n * devices, 'qr' elsewhere.\n */\n mode?: 'auto' | 'qr' | 'app';\n /** Called when the user closes the modal (the pairing is then aborted). */\n onCancel?: () => void;\n}\n\n/**\n * PairingOptions that show a QR modal. Pass to signInWithBwalletX({ pairing: pairingModal() }) or\n * connectBwalletX. The modal opens when the link is ready and shows the match code. Call `close()`\n * once pairing settles (useBwalletX does this for you); Cancel aborts the pairing.\n */\nexport function pairingModal(o: PairingModalOptions = {}): PairingOptions & { close(): void } {\n const ac = new AbortController();\n let ui: ReturnType<typeof mount> | null = null;\n let codeEl: HTMLDivElement | null = null;\n const close = () => {\n ui?.close();\n ui = null;\n };\n ac.signal.addEventListener('abort', close);\n return {\n signal: ac.signal,\n close,\n onLink(link) {\n if (typeof document === 'undefined') return;\n close();\n ui = mount();\n const app = o.mode === 'app' || ((o.mode ?? 'auto') === 'auto' && isTouchDevice());\n const h = el('h2', '', o.title ?? 'Connect bWalletX');\n const cancel = el('button', 'bwx-btn', 'Cancel');\n cancel.onclick = () => {\n o.onCancel?.();\n ac.abort();\n };\n codeEl = el('div', 'bwx-code', '');\n codeEl.setAttribute('aria-live', 'polite');\n if (app) {\n // Same device: no QR to scan. The link opens the bWalletX app (universal link); approve there,\n // then come back to this tab, which is still waiting on the pairing channel.\n const p = el('p', '', 'Open bWalletX, approve, then come back to this page.');\n const stack = el('div', 'bwx-stack');\n const open = el('a', 'bwx-btn bwx-primary', 'Open bWalletX');\n open.href = link;\n open.rel = 'noopener';\n const web = el('button', 'bwx-btn', 'Use web.bwalletx.com instead');\n web.onclick = () => {\n void navigator.clipboard?.writeText(link).catch(() => undefined);\n window.open(WEB_WALLET_URL, '_blank', 'noopener');\n p.textContent = 'Link copied. In web.bwalletx.com tap Connect and paste it, then come back here.';\n };\n stack.append(open, web, cancel);\n ui.box.append(h, p, codeEl, stack);\n return;\n }\n const p = el('p', '', 'Scan with bWalletX on your phone, or paste the link into web.bwalletx.com.');\n const qr = el('div', 'bwx-qr');\n qr.innerHTML = qrSvg(link);\n const row = el('div', 'bwx-row');\n const copy = el('button', 'bwx-btn', 'Copy link');\n copy.onclick = () => void navigator.clipboard?.writeText(link).then(() => (copy.textContent = 'Copied'));\n row.append(copy, cancel);\n ui.box.append(h, p, qr, codeEl, row);\n },\n onCode(code) {\n if (codeEl) {\n codeEl.textContent = code;\n const p = ui?.box.querySelector('p');\n if (p) p.textContent = 'Check bWalletX shows the same code, then tap Connect.';\n }\n },\n };\n}\n\n/** Show the picker; resolves with the chosen option, or null if closed. */\nexport function pickWallet(options: WalletOption[], o: { title?: string } = {}): Promise<WalletOption | null> {\n if (typeof document === 'undefined') return Promise.resolve(null);\n return new Promise((resolve) => {\n const ui = mount();\n const done = (w: WalletOption | null) => {\n ui.close();\n resolve(w);\n };\n ui.box.append(el('h2', '', o.title ?? 'Choose a wallet'));\n if (!options.length) ui.box.append(el('p', '', 'No wallet found. Get bWalletX at bwalletx.com.'));\n const list = el('div', 'bwx-list');\n for (const w of options) {\n const b = el('button', 'bwx-btn bwx-opt');\n if (w.icon) {\n const img = el('img');\n img.src = w.icon;\n img.alt = '';\n b.append(img);\n }\n b.append(el('span', '', w.name));\n if (w.kind === 'handcash') b.append(el('span', 'bwx-tag', 'until BRC-100'));\n b.onclick = () => done(w);\n list.append(b);\n }\n const cancel = el('button', 'bwx-btn', 'Cancel');\n cancel.onclick = () => done(null);\n ui.root.onclick = (e) => e.target === ui.root && done(null);\n ui.box.append(list, el('div', 'bwx-row'));\n (ui.box.lastChild as HTMLElement).append(cancel);\n });\n}\n","/**\n * Site theme for the wallet. A site may ask bWalletX to wear one of the themes the wallet already bundles\n * (e.g. 'npgx') while the site is connected and is the active tab; the wallet puts the user's own theme back on\n * any other site or when this declaration is removed. Only an id is declared: the wallet decides whether this\n * origin may wear it (brand editions are locked to their own sites) and never loads colours, CSS or URLs from here.\n */\nexport const WALLET_THEME_META = 'bwalletx-theme';\n\nconst ID = /^[a-z0-9-]{1,32}$/;\n\n/** Declare (or with null, withdraw) the theme the wallet should wear for this page. No-op outside a browser. */\nexport function declareWalletTheme(theme: string | null | undefined): void {\n if (typeof document === 'undefined') return;\n const id = (theme ?? '').trim().toLowerCase();\n let meta = document.head.querySelector<HTMLMetaElement>(`meta[name=\"${WALLET_THEME_META}\"]`);\n if (!id || !ID.test(id)) {\n meta?.remove();\n return;\n }\n if (!meta) {\n meta = document.createElement('meta');\n meta.name = WALLET_THEME_META;\n document.head.appendChild(meta);\n }\n if (meta.content !== id) meta.content = id;\n}\n"],"mappings":";;;;;;;;;AAKA,SAAS,OAAO,WAAW,aAAa,aAAmC;AAGpE,IAAM,iBAA8B,CAAC,GAAG,cAAc;AAmDtD,IAAM,eAAN,cAA2B,MAAM;AAAA,EACtC,YACW,MACT,SAES,YAAY,OACZ,OACT;AACA,UAAM,OAAO;AANJ;AAGA;AACA;AAGT,SAAK,OAAO;AAAA,EACd;AAAA,EARW;AAAA,EAGA;AAAA,EACA;AAKb;AAQA,IAAM,WAAW,CAAC,MAAmC,OAAQ,EAAkB,QAAQ,cAAc,OAAQ,EAAsB,iBAAiB;AAE7I,IAAM,mBAAmB,CAAC,MAAc,yBAAyB,KAAK,CAAC;AACvE,IAAM,YAAY,CAAC,MAAc,6BAA6B,KAAK,CAAC;AAE3E,SAAS,UAAU,GAAW;AAC5B,QAAM,IAAI,IAAI,WAAW,CAAC;AAC1B,aAAW,OAAO,gBAAgB,CAAC;AACnC,SAAO,MAAM,SAAS,MAAM,KAAK,CAAC,CAAC;AACrC;AAEA,SAAS,SAAS,MAAsB;AACtC,QAAM,IAAI,MAAM,QAAQ,MAAM,MAAM;AACpC,QAAM,MAAM,EAAE;AACd,QAAM,OAAO,MAAM,KAAO,CAAC,GAAG,IAAI,OAAO,MAAO,CAAC,IAAM,GAAG,IAAI,CAAC,IAAM,MAAM,KAAM,OAAO,CAAC;AACzF,SAAO,MAAM,MAAM,CAAC,GAAM,KAAM,GAAG,MAAM,GAAG,CAAC,CAAC;AAChD;AAEA,IAAM,OAAO,CAAC,GAAW,MAAe,EAAE,SAAS,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC,IAAI,WAAM;AAEjF,IAAM,WAAW,CAAC,MAAc,KAAK,EAAE,SAAS,IAAI,GAAG,CAAC,WAAW,KAAK,IAAI,GAAG,EAAE;AAEjF,eAAe,QAAQ,GAAgC;AACrD,MAAI,OAAO,EAAE,cAAc,SAAU,QAAO,EAAE;AAC9C,MAAI;AACF,QAAI,OAAO,EAAE,cAAc,WAAY,QAAO,MAAM,EAAE,UAAU;AAChE,UAAM,IAAI,OAAO,EAAE,SAAS,OAAO,uDAAuD;AAC1F,UAAM,IAAK,MAAM,EAAE,KAAK;AACxB,QAAI,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,IAAI,GAAI,OAAM,IAAI,MAAM,UAAU,EAAE,MAAM,EAAE;AACxE,WAAO,OAAO,EAAE,IAAI;AAAA,EACtB,SAAS,GAAG;AACV,UAAM,IAAI,aAAa,eAAe,mCAAoC,EAAY,OAAO,IAAI,OAAO,CAAC;AAAA,EAC3G;AACF;AAGA,eAAsB,aAAa,KAAuC,IAAgB,CAAC,GAAoB;AAC7G,MAAI,IAAI,QAAQ,MAAM;AACpB,QAAI,CAAC,OAAO,cAAc,IAAI,IAAI,KAAK,IAAI,QAAQ,EAAG,OAAM,IAAI,aAAa,eAAe,iCAAiC;AAC7H,WAAO,IAAI;AAAA,EACb;AACA,MAAI,IAAI,OAAO,MAAM;AACnB,QAAI,EAAE,IAAI,MAAM,GAAI,OAAM,IAAI,aAAa,eAAe,sBAAsB;AAChF,WAAO,KAAK,IAAI,GAAG,KAAK,KAAM,IAAI,MAAO,MAAM,QAAQ,CAAC,IAAM,GAAG,CAAC;AAAA,EACpE;AACA,QAAM,IAAI,aAAa,eAAe,kBAAkB;AAC1D;AAGA,eAAsB,kBAAkB,SAAiB,MAAc,IAAkB,OAAO;AAC9F,QAAM,CAAC,OAAO,MAAM,IAAI,QAAQ,MAAM,GAAG;AACzC,QAAM,OAAO,CAAC,GAAW,MAAgB,IAAI,aAAa,kBAAkB,qBAAqB,OAAO,KAAK,CAAC,IAAI,OAAO,CAAC;AAC1H,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,MAAM,EAAE,WAAW,MAAM,uBAAuB;AAC1D,YAAS,MAAM,EAAE,KAAK,GAAgD;AAAA,EACxE,SAAS,GAAG;AACV,UAAM,KAAK,6FAA6F,CAAC;AAAA,EAC3G;AACA,QAAM,OAAO,CAAC,MAAc,EAAE,QAAQ,WAAW,mBAAmB,KAAK,CAAC,EAAE,QAAQ,gBAAgB,MAAM;AAC1G,QAAM,OAAO,OAAO,cAAc;AAClC,MAAI,CAAC,KAAM,OAAM,KAAK,4CAA4C;AAClE,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,MAAM,EAAE,KAAK,IAAI,GAAG,EAAE,QAAQ,QAAQ,SAAS,EAAE,gBAAgB,mBAAmB,GAAG,MAAM,KAAK,UAAU,EAAE,UAAU,KAAK,CAAC,EAAE,CAAC;AAC3I,QAAI,CAAC,EAAE,GAAI,OAAM,IAAI,MAAM,UAAU,EAAE,MAAM,EAAE;AAC/C,WAAO,MAAM,EAAE,KAAK;AAAA,EACtB,SAAS,GAAG;AACV,UAAM,KAAM,EAAY,SAAS,CAAC;AAAA,EACpC;AACA,MAAI,CAAC,KAAK,SAAS,OAAQ,OAAM,KAAK,qBAAqB;AAC3D,QAAM,UAAU,KAAK,cAAc;AACnC,SAAO;AAAA,IACL,SAAS,KAAK,QAAQ,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,UAAU,EAAE,SAAS,EAAE;AAAA,IAC7E,WAAW,KAAK;AAAA,IAChB,QAAQ,UACJ,OAAO,QAAgB;AACrB,YAAM,EAAE,KAAK,OAAO,GAAG;AAAA,QACrB,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,KAAK,WAAW,KAAK,WAAW,UAAU,CAAC,EAAE,CAAC;AAAA,MACvE,CAAC;AAAA,IACH,IACA;AAAA,EACN;AACF;AAEA,eAAe,UAAU,QAAyB,aAAqB,SAAsB,QAAmB;AAC9G,MAAI;AACF,WAAO,MAAM,OAAO,aAAa;AAAA,MAC/B,aAAa,SAAS,WAAW;AAAA,MACjC,SAAS,QAAQ,IAAI,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,UAAU,EAAE,UAAU,mBAAmB,SAAS,EAAE,eAAe,SAAS,EAAE,EAAE;AAAA,MACxI,QAAQ,QAAQ,SAAS,SAAS;AAAA,MAClC,SAAS,EAAE,kBAAkB,MAAM;AAAA,IACrC,CAAC;AAAA,EACH,SAAS,GAAG;AACV,UAAM,MAAO,GAAa,WAAW,OAAO,CAAC;AAC7C,UAAM,WAAW,mCAAmC,KAAK,GAAG,KAAM,GAA0B,SAAS;AAErG,UAAM,IAAI,aAAa,WAAW,oBAAoB,gBAAgB,WAAW,4CAA4C,0CAA0C,GAAG,IAAI,CAAC,UAAU,CAAC;AAAA,EAC5L;AACF;AAGA,eAAsB,IAAI,QAAmB,KAAiB,IAAgB,CAAC,GAAuB;AACpG,QAAM,MAAM,IAAI,MAAM,IAAI,KAAK;AAC/B,MAAI,CAAC,GAAI,OAAM,IAAI,aAAa,eAAe,uBAAuB;AACtE,MAAI,IAAI,QAAQ,IAAI,KAAK,SAAS,IAAM,OAAM,IAAI,aAAa,eAAe,kBAAkB;AAChG,QAAM,OAAO,MAAM,aAAa,KAAK,CAAC;AACtC,MAAI,SAAS,MAAM,EAAG,QAAO,OAAO,IAAI,EAAE,GAAG,KAAK,IAAI,KAAK,CAAC;AAE5D,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI,iBAAiB,EAAE,GAAG;AACxB,UAAM,mBAAmB,UAAU,CAAC;AACpC,UAAM,mBAAmB,UAAU,CAAC;AACpC,QAAI;AACJ,QAAI;AACJ,QAAI;AACF,OAAC,EAAE,WAAW,GAAG,IAAI,MAAM,OAAO,aAAa,EAAE,YAAY,gBAAgB,OAAO,GAAG,gBAAgB,IAAI,gBAAgB,IAAI,cAAc,GAAG,YAAY,EAAE,CAAC;AAC/J,OAAC,EAAE,WAAW,GAAG,IAAI,MAAM,OAAO,aAAa,EAAE,aAAa,KAAK,CAAC;AAAA,IACtE,SAAS,GAAG;AACV,YAAM,IAAI,aAAa,gBAAgB,gDAAiD,EAAY,OAAO,IAAI,OAAO,CAAC;AAAA,IACzH;AACA,cAAU,CAAC,EAAE,QAAQ,IAAI,MAAM,EAAE,KAAK,UAAU,WAAW,EAAE,EAAE,OAAO,CAAa,EAAE,MAAM,GAAG,UAAU,MAAM,aAAa,UAAU,CAAC;AACtI,iBAAa,EAAE,mBAAmB,IAAI,kBAAkB,kBAAkB,aAAa,EAAE;AAAA,EAC3F,WAAW,UAAU,EAAE,GAAG;AACxB,UAAMA,KAAI,OAAO,EAAE,mBAAmB,CAAC,GAAG,MAAM,kBAAkB,GAAG,GAAG,EAAE,SAAS,KAAK,IAAI,IAAI,IAAI;AACpG,cAAUA,GAAE;AACZ,aAASA,GAAE;AAAA,EACb,OAAO;AACL,QAAI;AACF,gBAAU,CAAC,EAAE,QAAQ,IAAI,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,GAAG,UAAU,MAAM,aAAa,UAAU,CAAC;AAAA,IAC7F,QAAQ;AACN,YAAM,IAAI,aAAa,eAAe,2CAA2C,EAAE,EAAE;AAAA,IACvF;AAAA,EACF;AACA,MAAI,IAAI,QAAQ,IAAI,YAAa,WAAU,CAAC,GAAG,SAAS,EAAE,QAAQ,SAAS,IAAI,IAAI,GAAG,UAAU,GAAG,aAAa,OAAO,CAAC;AAExH,QAAM,IAAI,MAAM,UAAU,QAAQ,IAAI,QAAQ,OAAO,KAAK,IAAI,EAAE,CAAC,IAAI,SAAS,IAAI,MAAM;AACxF,MAAI,CAAC,EAAE,KAAM,OAAM,IAAI,aAAa,gBAAgB,oCAAoC,IAAI;AAC5F,QAAM,KAAK,EAAE,KAAK,MAAM,KAAK,EAAE,EAAE,IAAI;AACrC,MAAI,UAAU,IAAI;AAEhB,UAAM,OAAO,YAAY,eAAe,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EACrE;AACA,SAAO,EAAE,MAAM,EAAE,MAAM,IAAI,MAAM,SAAS,WAAW;AACvD;AAGO,SAAS,IAAI,QAAmB,KAAiB,IAAgB,CAAC,GAAuB;AAC9F,SAAO,IAAI,QAAQ,EAAE,MAAM,OAAO,GAAG,KAAK,QAAQ,CAAC,GAAI,IAAI,UAAU,CAAC,GAAI,KAAK,EAAE,GAAG,CAAC;AACvF;AAmBA,eAAsB,SAAS,QAAmB,KAA0C;AAC1F,MAAI,SAAS,MAAM,EAAG,OAAM,IAAI,aAAa,iBAAiB,uEAAuE;AACrI,QAAM,IAAI,MAAM,IAAI,MAAM,EAAE,OAAO,IAAI,OAAO,QAAQ,IAAI,OAAO,CAAC;AAClE,MAAI,CAAC,GAAG,SAAS,OAAQ,OAAM,IAAI,aAAa,eAAe,+BAA+B;AAC9F,QAAM,OAAO,EAAE,QAAQ,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,UAAU,CAAC;AACzD,QAAM,IAAI,MAAM,UAAU,QAAQ,EAAE,eAAe,OAAO,KAAK,IAAI,OAAO,EAAE,CAAC,IAAI,EAAE,SAAS,CAAC,GAAI,IAAI,UAAU,CAAC,GAAI,WAAW,CAAC;AAChI,MAAI,CAAC,EAAE,KAAM,OAAM,IAAI,aAAa,gBAAgB,oCAAoC,IAAI;AAC5F,QAAM,MAAiB,EAAE,MAAM,EAAE,MAAM,IAAI,EAAE,KAAK,MAAM,KAAK,EAAE,EAAE,IAAI,QAAW,MAAM,SAAS,EAAE,QAAQ;AACzG,QAAM,EAAE,SAAS,GAAG;AACpB,SAAO;AACT;;;AC/PA,SAAS,oBAA0C;AAkCnD,eAAe,gBAAgB,QAAyB,GAAgE;AACtH,QAAM,OAAO,sBAAsB,CAAC,CAAC,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AACrD,QAAM,EAAE,UAAU,IAAI,MAAM,OAAO,aAAa,EAAE,aAAa,KAAK,CAAC;AACrE,SAAO,EAAE,QAAQ,OAAO,QAAQ,aAAa,WAAW,UAAU,EAAE,IAAI,MAAM,EAAE,KAAK;AACvF;AAGO,SAAS,aAAa,QAAyB,MAAsG;AAC1J,QAAM,MAAoB;AAAA,IACxB,IAAI,KAAK;AAAA,IACT,MAAM,KAAK;AAAA,IACX,MAAM,KAAK;AAAA,IACX,MAAM,KAAK;AAAA,IACX,MAAM;AAAA,IACN,YAAY,KAAK,cAAc;AAAA,IAC/B,SAAS,MAAM,gBAAgB,QAAQ,GAAG;AAAA,EAC5C;AACA,SAAO;AACT;AAGO,SAAS,cAAc,SAAgE;AAC5F,QAAM,MAAoB;AAAA,IACxB,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,MAAM,UAAU;AACd,YAAM,IAAI,OAAO,YAAY,aAAa,QAAQ,IAAI;AACtD,YAAM,IAAK,MAAM,eAAe,CAAC,KAAO,MAAM,aAAa,CAAC;AAC5D,aAAO,gBAAgB,GAAG,GAAG;AAAA,IAC/B;AAAA,EACF;AACA,SAAO;AACT;AAiBO,SAAS,gBAAgB,GAAyC;AACvE,QAAM,MAAoB;AAAA,IACxB,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,MAAM,EAAE;AAAA,IACR,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,MAAM,UAAU;AACd,YAAM,IAAI,MAAM,EAAE,MAAM;AACxB,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,IAAI;AAAA,QAChC,aAAa,EAAE,eAAe;AAAA,QAC9B,QAAQ,EAAE;AAAA,QACV,SAAS,EAAE,WAAW,GAAG,EAAE,OAAO,QAAQ,OAAO,EAAE,CAAC;AAAA,QACpD,UAAU,IAAI;AAAA,QACd,MAAM;AAAA,QACN,cAAc;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAgBA,IAAM,QAAgC,EAAE,CAAC,cAAc,GAAG,YAAY,wBAAwB,WAAW;AAEzG,eAAe,MAAM,WAAkC,IAA6C;AAClG,MAAI;AACF,UAAM,IAAI,IAAI,aAAa,SAAS;AACpC,UAAM,KAAK,MAAM,QAAQ,KAAK,CAAC,EAAE,gBAAgB,CAAC,CAAC,EAAE,KAAK,MAAM,IAAI,GAAG,IAAI,QAAiB,CAAC,MAAM,WAAW,MAAM,EAAE,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC;AACnI,WAAO,KAAK,IAAI;AAAA,EAClB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,gBAAgB,IAA4B,CAAC,GAA4B;AAC7F,QAAM,MAAsB,CAAC;AAC7B,QAAM,KAAK,EAAE,MAAM;AACnB,MAAI,OAAO,WAAW,aAAa;AACjC,UAAM,OAAO,oBAAI,IAA0B;AAC3C,UAAM,aAAa,CAAC,MAAa;AAC/B,YAAM,IAAK,EAAkB;AAC7B,UAAI,CAAC,GAAG,OAAQ;AAChB,YAAM,OAAO,EAAE,MAAM,QAAQ,EAAE,MAAM,QAAQ,UAAU,KAAK,IAAI;AAChE,UAAI,KAAK,IAAI,IAAI,EAAG;AACpB,YAAM,MAAO,cAAoC,SAAS,IAAI;AAC9D,WAAK,IAAI,MAAM,aAAa,EAAE,QAAQ,EAAE,IAAI,MAAM,MAAM,MAAM,MAAM,IAAI,KAAK,EAAE,MAAM,QAAQ,MAAM,MAAM,EAAE,MAAM,MAAM,YAAY,IAAI,CAAC,CAAC;AAAA,IAC3I;AACA,WAAO,iBAAiB,yBAAyB,UAAU;AAC3D,WAAO,cAAc,IAAI,MAAM,sBAAsB,CAAC;AACtD,UAAM,QAAQ,EAAE,aAAa,QAAQ,IAAI,CAAC,MAAM,YAAY,EAAE,GAAG,MAAM,UAAU,EAAE,CAAC,CAAC,IAAI,QAAQ,QAAQ,CAAC,MAAM,IAAI,CAAC;AAErH,UAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,KAAK,OAAO,KAAM,OAAwC,MAAM,KAAK,IAAI,IAAI,EAAE,IAAI,EAAE,CAAC;AAC7H,WAAO,oBAAoB,yBAAyB,UAAU;AAC9D,QAAI,KAAK,GAAG,KAAK,OAAO,CAAC;AACzB,UAAM,MAAO,OAAgD;AAC7D,QAAI,OAAO,CAAC,CAAC,GAAG,KAAK,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,sBAAsB,GAAG;AAC7E,UAAI,KAAK,aAAa,KAAK,EAAE,IAAI,cAAc,MAAM,0BAAqB,YAAY,MAAM,CAAC,CAAC;AAAA,IAChG;AACA,UAAM,CAAC,MAAM,MAAM,IAAI,MAAM;AAC7B,QAAI,KAAM,KAAI,KAAK,aAAa,MAAM,EAAE,IAAI,kBAAkB,MAAM,kCAAkC,CAAC,CAAC;AACxG,QAAI,OAAQ,KAAI,KAAK,aAAa,QAAQ,EAAE,IAAI,kBAAkB,MAAM,kCAAkC,CAAC,CAAC;AAAA,EAC9G;AACA,MAAI,EAAE,QAAS,KAAI,KAAK,cAAc,EAAE,OAAO,CAAC;AAChD,MAAI,KAAK,GAAI,EAAE,SAAS,CAAC,CAAE;AAE3B,QAAM,OAAO,CAAC,MAAqB,EAAE,aAAa,IAAI,EAAE,SAAS,aAAa,IAAI;AAClF,SAAO,IAAI,IAAI,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,CAAC,GAAG,MAAM,KAAK,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;AACxG;;;AC1KA,OAAO,YAAY;AAKnB,IAAM,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBZ,SAAS,QAA0E;AACjF,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,OAAK,YAAY;AACjB,OAAK,aAAa,QAAQ,QAAQ;AAClC,OAAK,aAAa,cAAc,MAAM;AACtC,QAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,QAAM,cAAc;AACpB,QAAM,MAAM,SAAS,cAAc,KAAK;AACxC,MAAI,YAAY;AAChB,OAAK,OAAO,OAAO,GAAG;AACtB,WAAS,KAAK,YAAY,IAAI;AAC9B,SAAO,EAAE,MAAM,KAAK,OAAO,MAAM,KAAK,OAAO,EAAE;AACjD;AAEA,SAAS,GAA0C,KAAQ,KAAc,MAAe;AACtF,QAAM,IAAI,SAAS,cAAc,GAAG;AACpC,MAAI,IAAK,GAAE,YAAY;AACvB,MAAI,QAAQ,KAAM,GAAE,cAAc;AAClC,SAAO;AACT;AAGO,SAAS,MAAM,MAAsB;AAC1C,QAAM,IAAI,OAAO,GAAG,GAAG;AACvB,IAAE,QAAQ,IAAI;AACd,IAAE,KAAK;AACP,SAAO,EAAE,aAAa,EAAE,UAAU,GAAG,QAAQ,GAAG,UAAU,KAAK,CAAC;AAClE;AAOO,SAAS,cAAc,MAA6E,YAA8B;AACvI,QAAM,MAAM,IAAI;AAChB,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,KAAK,IAAI,aAAa;AAC5B,MAAI,+CAA+C,KAAK,EAAE,EAAG,QAAO;AACpE,MAAI,YAAY,KAAK,EAAE,MAAM,IAAI,kBAAkB,KAAK,EAAG,QAAO;AAClE,MAAI;AACF,WAAO,CAAC,CAAC,IAAI,aAAa,mBAAmB,EAAE,WAAW,CAAC,IAAI,aAAa,gBAAgB,EAAE;AAAA,EAChG,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAoBO,SAAS,aAAa,IAAyB,CAAC,GAAuC;AAC5F,QAAM,KAAK,IAAI,gBAAgB;AAC/B,MAAI,KAAsC;AAC1C,MAAI,SAAgC;AACpC,QAAM,QAAQ,MAAM;AAClB,QAAI,MAAM;AACV,SAAK;AAAA,EACP;AACA,KAAG,OAAO,iBAAiB,SAAS,KAAK;AACzC,SAAO;AAAA,IACL,QAAQ,GAAG;AAAA,IACX;AAAA,IACA,OAAO,MAAM;AACX,UAAI,OAAO,aAAa,YAAa;AACrC,YAAM;AACN,WAAK,MAAM;AACX,YAAM,MAAM,EAAE,SAAS,UAAW,EAAE,QAAQ,YAAY,UAAU,cAAc;AAChF,YAAM,IAAI,GAAG,MAAM,IAAI,EAAE,SAAS,kBAAkB;AACpD,YAAM,SAAS,GAAG,UAAU,WAAW,QAAQ;AAC/C,aAAO,UAAU,MAAM;AACrB,UAAE,WAAW;AACb,WAAG,MAAM;AAAA,MACX;AACA,eAAS,GAAG,OAAO,YAAY,EAAE;AACjC,aAAO,aAAa,aAAa,QAAQ;AACzC,UAAI,KAAK;AAGP,cAAMC,KAAI,GAAG,KAAK,IAAI,sDAAsD;AAC5E,cAAM,QAAQ,GAAG,OAAO,WAAW;AACnC,cAAM,OAAO,GAAG,KAAK,uBAAuB,eAAe;AAC3D,aAAK,OAAO;AACZ,aAAK,MAAM;AACX,cAAM,MAAM,GAAG,UAAU,WAAW,8BAA8B;AAClE,YAAI,UAAU,MAAM;AAClB,eAAK,UAAU,WAAW,UAAU,IAAI,EAAE,MAAM,MAAM,MAAS;AAC/D,iBAAO,KAAK,gBAAgB,UAAU,UAAU;AAChD,UAAAA,GAAE,cAAc;AAAA,QAClB;AACA,cAAM,OAAO,MAAM,KAAK,MAAM;AAC9B,WAAG,IAAI,OAAO,GAAGA,IAAG,QAAQ,KAAK;AACjC;AAAA,MACF;AACA,YAAM,IAAI,GAAG,KAAK,IAAI,4EAA4E;AAClG,YAAM,KAAK,GAAG,OAAO,QAAQ;AAC7B,SAAG,YAAY,MAAM,IAAI;AACzB,YAAM,MAAM,GAAG,OAAO,SAAS;AAC/B,YAAM,OAAO,GAAG,UAAU,WAAW,WAAW;AAChD,WAAK,UAAU,MAAM,KAAK,UAAU,WAAW,UAAU,IAAI,EAAE,KAAK,MAAO,KAAK,cAAc,QAAS;AACvG,UAAI,OAAO,MAAM,MAAM;AACvB,SAAG,IAAI,OAAO,GAAG,GAAG,IAAI,QAAQ,GAAG;AAAA,IACrC;AAAA,IACA,OAAO,MAAM;AACX,UAAI,QAAQ;AACV,eAAO,cAAc;AACrB,cAAM,IAAI,IAAI,IAAI,cAAc,GAAG;AACnC,YAAI,EAAG,GAAE,cAAc;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AACF;AAGO,SAAS,WAAW,SAAyB,IAAwB,CAAC,GAAiC;AAC5G,MAAI,OAAO,aAAa,YAAa,QAAO,QAAQ,QAAQ,IAAI;AAChE,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,KAAK,MAAM;AACjB,UAAM,OAAO,CAAC,MAA2B;AACvC,SAAG,MAAM;AACT,cAAQ,CAAC;AAAA,IACX;AACA,OAAG,IAAI,OAAO,GAAG,MAAM,IAAI,EAAE,SAAS,iBAAiB,CAAC;AACxD,QAAI,CAAC,QAAQ,OAAQ,IAAG,IAAI,OAAO,GAAG,KAAK,IAAI,gDAAgD,CAAC;AAChG,UAAM,OAAO,GAAG,OAAO,UAAU;AACjC,eAAW,KAAK,SAAS;AACvB,YAAM,IAAI,GAAG,UAAU,iBAAiB;AACxC,UAAI,EAAE,MAAM;AACV,cAAM,MAAM,GAAG,KAAK;AACpB,YAAI,MAAM,EAAE;AACZ,YAAI,MAAM;AACV,UAAE,OAAO,GAAG;AAAA,MACd;AACA,QAAE,OAAO,GAAG,QAAQ,IAAI,EAAE,IAAI,CAAC;AAC/B,UAAI,EAAE,SAAS,WAAY,GAAE,OAAO,GAAG,QAAQ,WAAW,eAAe,CAAC;AAC1E,QAAE,UAAU,MAAM,KAAK,CAAC;AACxB,WAAK,OAAO,CAAC;AAAA,IACf;AACA,UAAM,SAAS,GAAG,UAAU,WAAW,QAAQ;AAC/C,WAAO,UAAU,MAAM,KAAK,IAAI;AAChC,OAAG,KAAK,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,QAAQ,KAAK,IAAI;AAC1D,OAAG,IAAI,OAAO,MAAM,GAAG,OAAO,SAAS,CAAC;AACxC,IAAC,GAAG,IAAI,UAA0B,OAAO,MAAM;AAAA,EACjD,CAAC;AACH;;;ACrLO,IAAM,oBAAoB;AAEjC,IAAM,KAAK;AAGJ,SAAS,mBAAmB,OAAwC;AACzE,MAAI,OAAO,aAAa,YAAa;AACrC,QAAM,MAAM,SAAS,IAAI,KAAK,EAAE,YAAY;AAC5C,MAAI,OAAO,SAAS,KAAK,cAA+B,cAAc,iBAAiB,IAAI;AAC3F,MAAI,CAAC,MAAM,CAAC,GAAG,KAAK,EAAE,GAAG;AACvB,UAAM,OAAO;AACb;AAAA,EACF;AACA,MAAI,CAAC,MAAM;AACT,WAAO,SAAS,cAAc,MAAM;AACpC,SAAK,OAAO;AACZ,aAAS,KAAK,YAAY,IAAI;AAAA,EAChC;AACA,MAAI,KAAK,YAAY,GAAI,MAAK,UAAU;AAC1C;","names":["r","p"]}
package/dist/element.cjs CHANGED
@@ -32,16 +32,20 @@ __export(define_exports, {
32
32
  module.exports = __toCommonJS(define_exports);
33
33
 
34
34
  // src/discover.ts
35
+ var import_sdk = require("@bsv/sdk");
35
36
  var RDNS_EXTENSION = "com.bwalletx.extension";
36
37
  var RDNS_MOBILE = "space.bwallet.mobile";
37
38
  var BWALLETX_RDNS = [RDNS_EXTENSION, RDNS_MOBILE];
38
- function discoverBwalletX(ms = 1200) {
39
+ var DEFAULT_DISCOVER_MS = 300;
40
+ function discoverBwalletX(ms = DEFAULT_DISCOVER_MS) {
39
41
  if (typeof window === "undefined") return Promise.resolve(null);
40
42
  return new Promise((resolve) => {
41
43
  let done = false;
44
+ let timer;
42
45
  const finish = (r) => {
43
46
  if (done) return;
44
47
  done = true;
48
+ if (timer !== void 0) clearTimeout(timer);
45
49
  window.removeEventListener("brc100:announceWallet", onAnnounce);
46
50
  resolve(r);
47
51
  };
@@ -52,17 +56,94 @@ function discoverBwalletX(ms = 1200) {
52
56
  finish({ wallet: d.wallet, method: rdns === RDNS_EXTENSION ? "extension" : "in-app", rdns });
53
57
  }
54
58
  };
59
+ const cwiNow = () => {
60
+ const cwi = window.CWI;
61
+ if (cwi) finish({ wallet: cwi, method: "in-app", rdns: null });
62
+ return !!cwi;
63
+ };
55
64
  window.addEventListener("brc100:announceWallet", onAnnounce);
56
65
  window.dispatchEvent(new Event("brc100:requestWallet"));
57
- setTimeout(() => {
58
- const cwi = window.CWI;
59
- finish(cwi ? { wallet: cwi, method: "in-app", rdns: null } : null);
66
+ if (done || cwiNow()) return;
67
+ if (isFramed()) {
68
+ void probeXdm(Math.max(ms, 1200)).then((w) => finish(w ? { wallet: w, method: "in-app", rdns: null } : null));
69
+ return;
70
+ }
71
+ timer = setTimeout(() => {
72
+ timer = void 0;
73
+ if (!cwiNow()) finish(null);
60
74
  }, ms);
61
75
  });
62
76
  }
77
+ var isFramed = () => {
78
+ try {
79
+ return window.parent !== window;
80
+ } catch {
81
+ return true;
82
+ }
83
+ };
84
+ var BWALLETX_PARENT_ORIGINS = [
85
+ "https://web.bwalletx.com",
86
+ "https://desktop.bwalletx.com",
87
+ "https://bwalletx.com",
88
+ "https://www.bwallet.space",
89
+ "https://bwallet.space",
90
+ "capacitor://localhost",
91
+ // iOS app shell
92
+ "https://localhost",
93
+ // Android app shell
94
+ "tauri://localhost",
95
+ // bWalletX Desktop (macOS)
96
+ "https://tauri.localhost",
97
+ // bWalletX Desktop (Windows)
98
+ "chrome-extension://bombmmcjghjdlbmcclnhjodgcelnehgl"
99
+ // bWalletX for Chrome (fixed id)
100
+ ];
101
+ var parentOrigin = () => {
102
+ try {
103
+ const anc = window.location.ancestorOrigins;
104
+ if (anc && anc.length > 0) return anc[0] ?? null;
105
+ if (!document.referrer) return null;
106
+ const u = new URL(document.referrer);
107
+ return `${u.protocol}//${u.host}`;
108
+ } catch {
109
+ return null;
110
+ }
111
+ };
112
+ async function probeXdm(ms = 1200, allowed = BWALLETX_PARENT_ORIGINS) {
113
+ const origin = parentOrigin();
114
+ if (!origin || !allowed.includes(origin)) return null;
115
+ try {
116
+ const w = new import_sdk.WalletClient(new import_sdk.XDM(origin));
117
+ const ok = await Promise.race([
118
+ w.isAuthenticated({}).then(
119
+ () => true,
120
+ () => false
121
+ ),
122
+ new Promise((r) => setTimeout(() => r(false), ms))
123
+ ]);
124
+ return ok ? w : null;
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+ var warmedAt = 0;
130
+ function prewarmBwalletX() {
131
+ if (typeof window === "undefined" || Date.now() - warmedAt < 2e4) return;
132
+ warmedAt = Date.now();
133
+ void discoverBwalletX(0).then((f) => {
134
+ if (f && f.method === "extension") void f.wallet.isAuthenticated({}).catch(() => {
135
+ });
136
+ });
137
+ }
138
+ function bindPrewarm(el) {
139
+ const on = () => prewarmBwalletX();
140
+ const evs = ["pointerenter", "focus", "touchstart"];
141
+ for (const e of evs) el.addEventListener(e, on, { passive: true });
142
+ return () => evs.forEach((e) => el.removeEventListener(e, on));
143
+ }
63
144
 
64
145
  // src/pair.ts
65
- var import_sdk2 = require("@bsv/sdk");
146
+ var import_sdk3 = require("@bsv/sdk");
66
147
 
67
148
  // src/in-order.ts
68
149
  function inOrder(handle, onError) {
@@ -74,7 +155,7 @@ function inOrder(handle, onError) {
74
155
  }
75
156
 
76
157
  // src/protocol.ts
77
- var import_sdk = require("@bsv/sdk");
158
+ var import_sdk2 = require("@bsv/sdk");
78
159
  var PAIR_VERSION = "1";
79
160
  var PAIR_INFO = "bwallet-pair-v1";
80
161
  var PAIR_HOST = "www.bwallet.space";
@@ -100,14 +181,26 @@ function pairUrl(link) {
100
181
  const q = new URLSearchParams({ v: link.v, r: link.r, c: link.c, k: link.k, o: link.o, e: String(link.e) });
101
182
  return `https://${PAIR_HOST}/pair?${q}`;
102
183
  }
103
- var relaySocketUrl = (relay, channel, role, expiry) => {
184
+ var newClaim = () => b64url(crypto.getRandomValues(new Uint8Array(16)));
185
+ var WALLET_CLAIM_INFO = "bwallet-pair-wallet-claim-v1";
186
+ var hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
187
+ async function walletClaimFor(channel, sitePub) {
188
+ const d = await crypto.subtle.digest("SHA-256", enc.encode(`${WALLET_CLAIM_INFO}|${channel}|${sitePub}`));
189
+ return b64url(new Uint8Array(d));
190
+ }
191
+ async function claimDigest(claim) {
192
+ return hex(new Uint8Array(await crypto.subtle.digest("SHA-256", enc.encode(claim))));
193
+ }
194
+ var relaySocketUrl = (relay, channel, role, expiry, claim, walletClaimHash) => {
104
195
  const local = /^(localhost|127\.0\.0\.1)(:\d+)?$/.test(relay);
105
196
  const q = new URLSearchParams({ role });
106
197
  if (expiry) q.set("e", String(expiry));
198
+ if (claim) q.set("claim", claim);
199
+ if (walletClaimHash && role === "site") q.set("wc", walletClaimHash);
107
200
  return `${local ? "ws" : "wss"}://${relay}/v1/c/${channel}?${q}`;
108
201
  };
109
202
  async function deriveSession(mine, theirs, channel) {
110
- const point = mine.deriveSharedSecret(import_sdk.PublicKey.fromString(theirs));
203
+ const point = mine.deriveSharedSecret(import_sdk2.PublicKey.fromString(theirs));
111
204
  const ikm = new Uint8Array(point.encode(true));
112
205
  const base = await crypto.subtle.importKey("raw", ikm, "HKDF", false, ["deriveBits"]);
113
206
  const bits = new Uint8Array(
@@ -201,6 +294,7 @@ function parseStored(raw) {
201
294
  if (typeof p.s !== "string" || !/^[0-9a-f]{64}$/i.test(p.s)) return null;
202
295
  if (typeof p.k !== "string" || !/^0[23][0-9a-f]{64}$/i.test(p.k)) return null;
203
296
  if (!n(p.sent) || !n(p.lastSeen) || !n(p.pairedAt)) return null;
297
+ if (p.sc !== void 0 && (typeof p.sc !== "string" || !/^[A-Za-z0-9_-]{22,64}$/.test(p.sc))) return null;
204
298
  return p;
205
299
  } catch {
206
300
  return null;
@@ -258,7 +352,12 @@ var Link = class {
258
352
  await this.opening;
259
353
  }
260
354
  reopen() {
261
- const ws = this.socket(relaySocketUrl(this.p.r, this.p.c, "site", Math.floor(Date.now() / 1e3) + QR_LIFETIME_S - 10));
355
+ if (!this.p.sc) {
356
+ this.p = { ...this.p, sc: newClaim() };
357
+ this.persist();
358
+ }
359
+ const e = Math.floor(Date.now() / 1e3) + QR_LIFETIME_S - 10;
360
+ const ws = this.socket(relaySocketUrl(this.p.r, this.p.c, "site", e, this.p.sc));
262
361
  return new Promise((resolve, reject) => {
263
362
  ws.onopen = () => {
264
363
  this.ws = ws;
@@ -349,14 +448,16 @@ function pairBwalletX(opts) {
349
448
  const relay = opts.relay ?? DEFAULT_RELAY;
350
449
  const origin = opts.origin ?? (typeof location !== "undefined" ? location.origin : "");
351
450
  if (!origin) return Promise.reject(new Error("pairBwalletX needs options.origin outside a browser"));
352
- const siteKey = import_sdk2.PrivateKey.fromRandom();
451
+ const siteKey = import_sdk3.PrivateKey.fromRandom();
353
452
  const channel = newChannel();
354
453
  const expiry = Math.floor(Date.now() / 1e3) + QR_LIFETIME_S;
355
- return new Promise((resolve, reject) => {
454
+ const siteClaim = newClaim();
455
+ const sitePub = siteKey.toPublicKey().toString();
456
+ return walletClaimFor(channel, sitePub).then(claimDigest).then((wc) => new Promise((resolve, reject) => {
356
457
  if (opts.signal?.aborted) return reject(new Error("cancelled"));
357
458
  let ws;
358
459
  try {
359
- ws = socket(relaySocketUrl(relay, channel, "site", expiry));
460
+ ws = socket(relaySocketUrl(relay, channel, "site", expiry, siteClaim, wc));
360
461
  } catch (e) {
361
462
  return reject(e);
362
463
  }
@@ -395,7 +496,7 @@ function pairBwalletX(opts) {
395
496
  if (msg.t === "ready") {
396
497
  settled = true;
397
498
  clearTimeout(timer);
398
- const p = { v: 1, c: channel, r: relay, s: siteKey.toHex(), k: walletKey, ...sealer.counters, pairedAt: Date.now() };
499
+ const p = { v: 1, c: channel, r: relay, s: siteKey.toHex(), k: walletKey, ...sealer.counters, pairedAt: Date.now(), sc: siteClaim };
399
500
  await Promise.resolve(storage.setItem(key, JSON.stringify(p))).catch(() => {
400
501
  });
401
502
  const link = new Link(p, sealer, storage, key, socket, opts.callTimeoutMs ?? 12e4);
@@ -405,15 +506,15 @@ function pairBwalletX(opts) {
405
506
  fail(new Error("Pairing was declined in bWalletX"));
406
507
  }
407
508
  });
408
- opts.onLink(pairUrl({ v: PAIR_VERSION, r: relay, c: channel, k: siteKey.toPublicKey().toString(), o: origin, e: expiry }));
409
- });
509
+ opts.onLink(pairUrl({ v: PAIR_VERSION, r: relay, c: channel, k: sitePub, o: origin, e: expiry }));
510
+ }));
410
511
  }
411
512
  async function restorePairing(opts = {}) {
412
513
  const storage = opts.storage === void 0 ? defaultStorage() : opts.storage ?? memoryStorage();
413
514
  const key = opts.storageKey ?? DEFAULT_KEY;
414
515
  const p = parseStored(await Promise.resolve(storage.getItem(key)).catch(() => null));
415
516
  if (!p) return null;
416
- const { key: aes } = await deriveSession(import_sdk2.PrivateKey.fromHex(p.s), p.k, p.c);
517
+ const { key: aes } = await deriveSession(import_sdk3.PrivateKey.fromHex(p.s), p.k, p.c);
417
518
  const sealer = new Sealer(aes, "site");
418
519
  sealer.restore({ sent: p.sent, lastSeen: p.lastSeen });
419
520
  return new Link(p, sealer, storage, key, opts.socket ?? defaultSocket(), opts.callTimeoutMs ?? 12e4).wallet();
@@ -422,35 +523,68 @@ async function restorePairing(opts = {}) {
422
523
  // src/signin.ts
423
524
  var LOGIN_PROTOCOL = [2, "bwallet sign in"];
424
525
  async function connectBwalletX(opts = {}) {
425
- if (opts.wallet) return { wallet: opts.wallet, method: "wallet" };
426
- const found = await discoverBwalletX(opts.discoverMs);
427
- if (found) return { wallet: found.wallet, method: found.method };
526
+ const status = (s) => {
527
+ try {
528
+ opts.onStatus?.(s);
529
+ } catch {
530
+ }
531
+ };
532
+ if (opts.wallet) {
533
+ status("opening");
534
+ return { wallet: opts.wallet, method: "wallet" };
535
+ }
536
+ status("finding");
537
+ const found = await discoverBwalletX(opts.discoverMs ?? DEFAULT_DISCOVER_MS);
538
+ if (found) {
539
+ status("opening");
540
+ return { wallet: found.wallet, method: found.method };
541
+ }
428
542
  if (opts.reusePairing !== false) {
429
543
  const restored = await restorePairing(opts.pairing ?? {});
430
- if (restored) return { wallet: restored, method: "pairing" };
544
+ if (restored) {
545
+ status("opening");
546
+ return { wallet: restored, method: "pairing" };
547
+ }
431
548
  }
432
549
  if (!opts.pairing) {
433
550
  throw Object.assign(new Error("bWalletX was not found in this browser"), { code: "NOT_FOUND" });
434
551
  }
552
+ status("pairing");
435
553
  return { wallet: await pairBwalletX(opts.pairing), method: "pairing" };
436
554
  }
437
555
  async function signInWithBwalletX(opts) {
438
556
  const { wallet, method } = await connectBwalletX(opts);
439
557
  if (method !== "pairing") await wallet.waitForAuthentication({}).catch(() => {
440
558
  });
441
- const { publicKey: identityKey } = await wallet.getPublicKey({ identityKey: true });
442
- const c = await opts.challenge(identityKey);
443
- const { signature } = await wallet.createSignature({
444
- protocolID: c.protocolID ?? LOGIN_PROTOCOL,
445
- keyID: c.keyID,
446
- counterparty: "anyone",
447
- data: Array.from(new TextEncoder().encode(c.message))
448
- });
559
+ const { publicKey: identityKey } = await atStage("identity", () => wallet.getPublicKey({ identityKey: true }));
560
+ try {
561
+ opts.onStatus?.("signing");
562
+ } catch {
563
+ }
564
+ const c = await atStage("challenge", () => opts.challenge(identityKey));
565
+ const { signature } = await atStage(
566
+ "signature",
567
+ () => wallet.createSignature({
568
+ protocolID: c.protocolID ?? LOGIN_PROTOCOL,
569
+ keyID: c.keyID,
570
+ counterparty: "anyone",
571
+ data: Array.from(new TextEncoder().encode(c.message))
572
+ })
573
+ );
449
574
  return { identityKey, signature: Array.from(signature), nonce: c.nonce, method, wallet };
450
575
  }
576
+ async function atStage(stage, run) {
577
+ try {
578
+ return await run();
579
+ } catch (e) {
580
+ const err = e instanceof Error ? e : new Error(String(e));
581
+ if (!err.stage) Object.assign(err, { stage });
582
+ throw err;
583
+ }
584
+ }
451
585
 
452
586
  // src/access-client.ts
453
- var import_sdk3 = require("@bsv/sdk");
587
+ var import_sdk4 = require("@bsv/sdk");
454
588
 
455
589
  // src/access.ts
456
590
  var ACCESS_PROTOCOL = [2, "bwalletx access"];
@@ -588,7 +722,7 @@ async function proveHoldings(wallet, challenge, tokenId, basket = "bsv21") {
588
722
  const counterparty = ci.counterparty ?? "self";
589
723
  for (const protocolID of ci.protocolID ? [ci.protocolID] : [ONESAT, LEGACY_ONESAT]) {
590
724
  const { publicKey } = await wallet.getPublicKey({ protocolID, keyID: ci.keyID, counterparty });
591
- const h = import_sdk3.Utils.toHex(import_sdk3.Hash.hash160(import_sdk3.PublicKey.fromString(publicKey).encode(true)));
725
+ const h = import_sdk4.Utils.toHex(import_sdk4.Hash.hash160(import_sdk4.PublicKey.fromString(publicKey).encode(true)));
592
726
  if (want && h !== want) continue;
593
727
  const existing = byKey.get(publicKey);
594
728
  if (existing) {
@@ -784,6 +918,7 @@ function createClass() {
784
918
  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>` : "";
785
919
  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}`;
786
920
  this.button.addEventListener("click", () => void this.#click());
921
+ bindPrewarm(this.button);
787
922
  }
788
923
  #emit(type, detail, cancelable = false) {
789
924
  return this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable }));
@@ -810,7 +945,7 @@ function createClass() {
810
945
  this.#notFound = false;
811
946
  this.#setBusy(true);
812
947
  try {
813
- const ms = Number(this.#attr("discover-ms")) || 1200;
948
+ const ms = Number(this.#attr("discover-ms")) || DEFAULT_DISCOVER_MS;
814
949
  const found = await discoverBwalletX(ms);
815
950
  if (!found) {
816
951
  this.#notFound = true;