@bwalletx/connect 0.5.4 → 0.7.0
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 +56 -0
- package/README.md +97 -9
- package/dist/bwalletx-connect.global.js +97 -25
- package/dist/bwalletx-connect.global.js.map +1 -1
- package/dist/choice-B97AjqF4.d.cts +442 -0
- package/dist/choice-B97AjqF4.d.ts +442 -0
- package/dist/chunk-6EFFOIHK.js +2441 -0
- package/dist/chunk-6EFFOIHK.js.map +1 -0
- package/dist/{chunk-QRXFL66W.js → chunk-A2WLGS25.js} +6 -4
- package/dist/chunk-A2WLGS25.js.map +1 -0
- package/dist/chunk-DIKJFKND.js +239 -0
- package/dist/chunk-DIKJFKND.js.map +1 -0
- package/dist/chunk-TNRI7ARM.js +206 -0
- package/dist/chunk-TNRI7ARM.js.map +1 -0
- package/dist/element-BXEoOdXY.d.cts +53 -0
- package/dist/element-BXEoOdXY.d.ts +53 -0
- package/dist/element.cjs +1576 -60
- package/dist/element.cjs.map +1 -1
- package/dist/element.d.cts +26 -26
- package/dist/element.d.ts +26 -26
- package/dist/element.js +35 -2
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +2046 -454
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +323 -16
- package/dist/index.d.ts +323 -16
- package/dist/index.js +186 -12
- package/dist/index.js.map +1 -1
- package/dist/next.cjs +5 -3
- package/dist/next.cjs.map +1 -1
- package/dist/next.js +1 -1
- package/dist/react.cjs +1601 -210
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +115 -9
- package/dist/react.d.ts +115 -9
- package/dist/react.js +262 -46
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +5 -3
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +8 -3
- package/dist/server.d.ts +8 -3
- package/dist/server.js +1 -1
- package/package.json +10 -9
- package/dist/chunk-EXRQJTXF.js +0 -910
- package/dist/chunk-EXRQJTXF.js.map +0 -1
- package/dist/chunk-QRXFL66W.js.map +0 -1
- package/dist/chunk-RIOHG5JH.js +0 -459
- package/dist/chunk-RIOHG5JH.js.map +0 -1
- package/dist/wallets-CbzMtxrf.d.cts +0 -250
- package/dist/wallets-CbzMtxrf.d.ts +0 -250
|
@@ -0,0 +1,2441 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACCESS_PROTOCOL,
|
|
3
|
+
AccessError,
|
|
4
|
+
MAX_HOLDING_PROOFS,
|
|
5
|
+
challengeBytes,
|
|
6
|
+
parseAccessChallenge
|
|
7
|
+
} from "./chunk-MWD4BCO5.js";
|
|
8
|
+
|
|
9
|
+
// src/discover.ts
|
|
10
|
+
import { WalletClient, XDM } from "@bsv/sdk";
|
|
11
|
+
var RDNS_EXTENSION = "com.bwalletx.extension";
|
|
12
|
+
var RDNS_MOBILE = "space.bwallet.mobile";
|
|
13
|
+
var BWALLETX_RDNS = [RDNS_EXTENSION, RDNS_MOBILE];
|
|
14
|
+
var DEFAULT_DISCOVER_MS = 300;
|
|
15
|
+
function discoverBwalletX(ms = DEFAULT_DISCOVER_MS) {
|
|
16
|
+
if (typeof window === "undefined") return Promise.resolve(null);
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
let done = false;
|
|
19
|
+
let timer;
|
|
20
|
+
const finish2 = (r) => {
|
|
21
|
+
if (done) return;
|
|
22
|
+
done = true;
|
|
23
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
24
|
+
window.removeEventListener("brc100:announceWallet", onAnnounce);
|
|
25
|
+
resolve(r);
|
|
26
|
+
};
|
|
27
|
+
const onAnnounce = (e) => {
|
|
28
|
+
const d = e.detail;
|
|
29
|
+
const rdns = d?.info?.rdns ?? "";
|
|
30
|
+
if (d?.wallet && BWALLETX_RDNS.includes(rdns)) {
|
|
31
|
+
finish2({ wallet: d.wallet, method: rdns === RDNS_EXTENSION ? "extension" : "in-app", rdns });
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
window.addEventListener("brc100:announceWallet", onAnnounce);
|
|
35
|
+
window.dispatchEvent(new Event("brc100:requestWallet"));
|
|
36
|
+
if (done) return;
|
|
37
|
+
if (isFramed()) {
|
|
38
|
+
void probeXdm(Math.max(ms, 1200)).then((w) => finish2(w ? { wallet: w, method: "in-app", rdns: null } : null));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
timer = setTimeout(() => {
|
|
42
|
+
timer = void 0;
|
|
43
|
+
finish2(null);
|
|
44
|
+
}, ms);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function findBwalletX(ms = DEFAULT_DISCOVER_MS) {
|
|
48
|
+
return (await discoverBwalletX(ms))?.wallet ?? null;
|
|
49
|
+
}
|
|
50
|
+
var isFramed = () => {
|
|
51
|
+
try {
|
|
52
|
+
return window.parent !== window;
|
|
53
|
+
} catch {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var BWALLETX_PARENT_ORIGINS = [
|
|
58
|
+
"https://web.bwalletx.com",
|
|
59
|
+
"https://desktop.bwalletx.com",
|
|
60
|
+
"https://bwalletx.com",
|
|
61
|
+
"https://www.bwallet.space",
|
|
62
|
+
"https://bwallet.space",
|
|
63
|
+
"capacitor://localhost",
|
|
64
|
+
// iOS app shell
|
|
65
|
+
"https://localhost",
|
|
66
|
+
// Android app shell
|
|
67
|
+
"tauri://localhost",
|
|
68
|
+
// bWalletX Desktop (macOS)
|
|
69
|
+
"https://tauri.localhost",
|
|
70
|
+
// bWalletX Desktop (Windows)
|
|
71
|
+
"chrome-extension://bombmmcjghjdlbmcclnhjodgcelnehgl"
|
|
72
|
+
// bWalletX for Chrome (fixed id)
|
|
73
|
+
];
|
|
74
|
+
var parentOrigin = () => {
|
|
75
|
+
try {
|
|
76
|
+
const anc = window.location.ancestorOrigins;
|
|
77
|
+
if (anc && anc.length > 0) return anc[0] ?? null;
|
|
78
|
+
if (!document.referrer) return null;
|
|
79
|
+
const u = new URL(document.referrer);
|
|
80
|
+
return `${u.protocol}//${u.host}`;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
async function probeXdm(ms = 1200, allowed = BWALLETX_PARENT_ORIGINS) {
|
|
86
|
+
const origin = parentOrigin();
|
|
87
|
+
if (!origin || !allowed.includes(origin)) return null;
|
|
88
|
+
try {
|
|
89
|
+
const w = new WalletClient(new XDM(origin));
|
|
90
|
+
const ok = await Promise.race([
|
|
91
|
+
w.isAuthenticated({}).then(
|
|
92
|
+
() => true,
|
|
93
|
+
() => false
|
|
94
|
+
),
|
|
95
|
+
new Promise((r) => setTimeout(() => r(false), ms))
|
|
96
|
+
]);
|
|
97
|
+
return ok ? w : null;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
var warmedAt = 0;
|
|
103
|
+
function prewarmBwalletX() {
|
|
104
|
+
if (typeof window === "undefined" || Date.now() - warmedAt < 2e4) return;
|
|
105
|
+
warmedAt = Date.now();
|
|
106
|
+
void discoverBwalletX(0).then((f) => {
|
|
107
|
+
if (f && f.method === "extension") void f.wallet.isAuthenticated({}).catch(() => {
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function bindPrewarm(el2) {
|
|
112
|
+
const on = () => prewarmBwalletX();
|
|
113
|
+
const evs = ["pointerenter", "focus", "touchstart"];
|
|
114
|
+
for (const e of evs) el2.addEventListener(e, on, { passive: true });
|
|
115
|
+
return () => evs.forEach((e) => el2.removeEventListener(e, on));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/in-order.ts
|
|
119
|
+
function inOrder(handle, onError) {
|
|
120
|
+
let queue = Promise.resolve();
|
|
121
|
+
return (frame) => {
|
|
122
|
+
queue = queue.then(() => handle(frame)).catch((e) => onError?.(e));
|
|
123
|
+
return queue;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/pair.ts
|
|
128
|
+
import { PrivateKey as PrivateKey2 } from "@bsv/sdk";
|
|
129
|
+
|
|
130
|
+
// src/protocol.ts
|
|
131
|
+
import { PublicKey } from "@bsv/sdk";
|
|
132
|
+
var PAIR_VERSION = "1";
|
|
133
|
+
var PAIR_INFO = "bwallet-pair-v1";
|
|
134
|
+
var PAIR_HOST = "www.bwallet.space";
|
|
135
|
+
var DEFAULT_RELAY = "relay.bwallet.space";
|
|
136
|
+
var QR_LIFETIME_S = 120;
|
|
137
|
+
var enc = new TextEncoder();
|
|
138
|
+
var dec = new TextDecoder();
|
|
139
|
+
var binary = (bytes) => {
|
|
140
|
+
let s = "";
|
|
141
|
+
for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
142
|
+
return s;
|
|
143
|
+
};
|
|
144
|
+
var b64url = (bytes) => btoa(binary(bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
145
|
+
var fromB64url = (s) => {
|
|
146
|
+
const t = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
147
|
+
const bin = atob(t + "=".repeat((4 - t.length % 4) % 4));
|
|
148
|
+
return Uint8Array.from(bin, (ch) => ch.charCodeAt(0));
|
|
149
|
+
};
|
|
150
|
+
var b64 = (bytes) => btoa(binary(bytes));
|
|
151
|
+
var fromB64 = (s) => Uint8Array.from(atob(s), (ch) => ch.charCodeAt(0));
|
|
152
|
+
var newChannel = () => b64url(crypto.getRandomValues(new Uint8Array(16)));
|
|
153
|
+
function pairUrl(link) {
|
|
154
|
+
const q = new URLSearchParams({ v: link.v, r: link.r, c: link.c, k: link.k, o: link.o, e: String(link.e) });
|
|
155
|
+
return `https://${PAIR_HOST}/pair?${q}`;
|
|
156
|
+
}
|
|
157
|
+
var newClaim = () => b64url(crypto.getRandomValues(new Uint8Array(16)));
|
|
158
|
+
var WALLET_CLAIM_INFO = "bwallet-pair-wallet-claim-v1";
|
|
159
|
+
var hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
160
|
+
async function walletClaimFor(channel, sitePub) {
|
|
161
|
+
const d = await crypto.subtle.digest("SHA-256", enc.encode(`${WALLET_CLAIM_INFO}|${channel}|${sitePub}`));
|
|
162
|
+
return b64url(new Uint8Array(d));
|
|
163
|
+
}
|
|
164
|
+
async function claimDigest(claim) {
|
|
165
|
+
return hex(new Uint8Array(await crypto.subtle.digest("SHA-256", enc.encode(claim))));
|
|
166
|
+
}
|
|
167
|
+
var relaySocketUrl = (relay, channel, role, expiry, claim, walletClaimHash) => {
|
|
168
|
+
const local = /^(localhost|127\.0\.0\.1)(:\d+)?$/.test(relay);
|
|
169
|
+
const q = new URLSearchParams({ role });
|
|
170
|
+
if (expiry) q.set("e", String(expiry));
|
|
171
|
+
if (claim) q.set("claim", claim);
|
|
172
|
+
if (walletClaimHash && role === "site") q.set("wc", walletClaimHash);
|
|
173
|
+
return `${local ? "ws" : "wss"}://${relay}/v1/c/${channel}?${q}`;
|
|
174
|
+
};
|
|
175
|
+
async function deriveSession(mine, theirs, channel) {
|
|
176
|
+
const point = mine.deriveSharedSecret(PublicKey.fromString(theirs));
|
|
177
|
+
const ikm = new Uint8Array(point.encode(true));
|
|
178
|
+
const base = await crypto.subtle.importKey("raw", ikm, "HKDF", false, ["deriveBits"]);
|
|
179
|
+
const bits = new Uint8Array(
|
|
180
|
+
await crypto.subtle.deriveBits(
|
|
181
|
+
{ name: "HKDF", hash: "SHA-256", salt: fromB64url(channel), info: enc.encode(PAIR_INFO) },
|
|
182
|
+
base,
|
|
183
|
+
256
|
|
184
|
+
)
|
|
185
|
+
);
|
|
186
|
+
const key = await crypto.subtle.importKey("raw", bits, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
187
|
+
const h = new Uint8Array(await crypto.subtle.digest("SHA-256", bits));
|
|
188
|
+
const code = String((h[0] << 8 | h[1]) % 1e4).padStart(4, "0");
|
|
189
|
+
return { key, code };
|
|
190
|
+
}
|
|
191
|
+
var Sealer = class {
|
|
192
|
+
constructor(key, me) {
|
|
193
|
+
this.key = key;
|
|
194
|
+
this.me = me;
|
|
195
|
+
}
|
|
196
|
+
key;
|
|
197
|
+
me;
|
|
198
|
+
sent = 0;
|
|
199
|
+
lastSeen = 0;
|
|
200
|
+
async seal(msg) {
|
|
201
|
+
const s = ++this.sent;
|
|
202
|
+
const n = crypto.getRandomValues(new Uint8Array(12));
|
|
203
|
+
const ct = new Uint8Array(
|
|
204
|
+
await crypto.subtle.encrypt(
|
|
205
|
+
{ name: "AES-GCM", iv: n, additionalData: enc.encode(`${this.me}|${s}`) },
|
|
206
|
+
this.key,
|
|
207
|
+
enc.encode(JSON.stringify(msg))
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
return { s, n: b64(n), d: b64(ct) };
|
|
211
|
+
}
|
|
212
|
+
/** Decrypts a frame from the other side; null for replays, reflections or tampering. */
|
|
213
|
+
async open(f) {
|
|
214
|
+
if (!Number.isInteger(f.s) || f.s <= this.lastSeen) return null;
|
|
215
|
+
const from = this.me === "site" ? "wallet" : "site";
|
|
216
|
+
try {
|
|
217
|
+
const pt = await crypto.subtle.decrypt(
|
|
218
|
+
{ name: "AES-GCM", iv: fromB64(f.n), additionalData: enc.encode(`${from}|${f.s}`) },
|
|
219
|
+
this.key,
|
|
220
|
+
fromB64(f.d)
|
|
221
|
+
);
|
|
222
|
+
this.lastSeen = f.s;
|
|
223
|
+
return JSON.parse(dec.decode(pt));
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** For resuming a session after a reload: continue counters where they were. */
|
|
229
|
+
get counters() {
|
|
230
|
+
return { sent: this.sent, lastSeen: this.lastSeen };
|
|
231
|
+
}
|
|
232
|
+
restore(c) {
|
|
233
|
+
this.sent = c.sent;
|
|
234
|
+
this.lastSeen = c.lastSeen;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
var isSealed = (x) => !!x && typeof x === "object" && "d" in x && "n" in x && "s" in x;
|
|
238
|
+
|
|
239
|
+
// src/pair.ts
|
|
240
|
+
var DEFAULT_KEY = "bwalletx.connect.pairing.v1";
|
|
241
|
+
var OPEN = 1;
|
|
242
|
+
function memoryStorage() {
|
|
243
|
+
const m = /* @__PURE__ */ new Map();
|
|
244
|
+
return { getItem: (k) => m.get(k) ?? null, setItem: (k, v) => void m.set(k, v), removeItem: (k) => void m.delete(k) };
|
|
245
|
+
}
|
|
246
|
+
function defaultStorage() {
|
|
247
|
+
try {
|
|
248
|
+
if (typeof localStorage !== "undefined") return localStorage;
|
|
249
|
+
} catch {
|
|
250
|
+
}
|
|
251
|
+
return memoryStorage();
|
|
252
|
+
}
|
|
253
|
+
function defaultSocket() {
|
|
254
|
+
return (url) => {
|
|
255
|
+
if (typeof WebSocket === "undefined") throw new Error("No WebSocket here. Pass options.socket.");
|
|
256
|
+
return new WebSocket(url);
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function parseStored(raw) {
|
|
260
|
+
if (!raw) return null;
|
|
261
|
+
try {
|
|
262
|
+
const p = JSON.parse(raw);
|
|
263
|
+
const n = (x) => Number.isInteger(x) && x >= 0;
|
|
264
|
+
if (p.v !== 1) return null;
|
|
265
|
+
if (typeof p.c !== "string" || !/^[A-Za-z0-9_-]{22}$/.test(p.c)) return null;
|
|
266
|
+
if (typeof p.r !== "string" || !/^[a-z0-9.-]+(:\d+)?$/i.test(p.r)) return null;
|
|
267
|
+
if (typeof p.s !== "string" || !/^[0-9a-f]{64}$/i.test(p.s)) return null;
|
|
268
|
+
if (typeof p.k !== "string" || !/^0[23][0-9a-f]{64}$/i.test(p.k)) return null;
|
|
269
|
+
if (!n(p.sent) || !n(p.lastSeen) || !n(p.pairedAt)) return null;
|
|
270
|
+
if (p.sc !== void 0 && (typeof p.sc !== "string" || !/^[A-Za-z0-9_-]{22,64}$/.test(p.sc))) return null;
|
|
271
|
+
return p;
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
var Link = class {
|
|
277
|
+
constructor(p, sealer, storage, key, socket, timeoutMs) {
|
|
278
|
+
this.p = p;
|
|
279
|
+
this.sealer = sealer;
|
|
280
|
+
this.storage = storage;
|
|
281
|
+
this.key = key;
|
|
282
|
+
this.socket = socket;
|
|
283
|
+
this.timeoutMs = timeoutMs;
|
|
284
|
+
}
|
|
285
|
+
p;
|
|
286
|
+
sealer;
|
|
287
|
+
storage;
|
|
288
|
+
key;
|
|
289
|
+
socket;
|
|
290
|
+
timeoutMs;
|
|
291
|
+
ws = null;
|
|
292
|
+
opening = null;
|
|
293
|
+
waiting = /* @__PURE__ */ new Map();
|
|
294
|
+
forgotten = false;
|
|
295
|
+
/** Take over a socket that is already open (the one pairing used). */
|
|
296
|
+
adopt(ws) {
|
|
297
|
+
this.ws = ws;
|
|
298
|
+
this.wire(ws);
|
|
299
|
+
}
|
|
300
|
+
wire(ws) {
|
|
301
|
+
ws.onmessage = inOrder((ev) => this.onFrame(String(ev.data)));
|
|
302
|
+
ws.onerror = null;
|
|
303
|
+
ws.onclose = () => {
|
|
304
|
+
if (this.ws === ws) this.ws = null;
|
|
305
|
+
this.failAll(new Error("The connection to bWalletX closed"));
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
persist() {
|
|
309
|
+
if (this.forgotten) return;
|
|
310
|
+
this.p = { ...this.p, ...this.sealer.counters };
|
|
311
|
+
void Promise.resolve(this.storage.setItem(this.key, JSON.stringify(this.p))).catch(() => {
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
failAll(e) {
|
|
315
|
+
for (const w of this.waiting.values()) {
|
|
316
|
+
clearTimeout(w.timer);
|
|
317
|
+
w.reject(e);
|
|
318
|
+
}
|
|
319
|
+
this.waiting.clear();
|
|
320
|
+
}
|
|
321
|
+
async ensure() {
|
|
322
|
+
if (this.forgotten) throw new Error("This pairing was removed. Pair again.");
|
|
323
|
+
if (this.ws && this.ws.readyState === OPEN) return;
|
|
324
|
+
this.opening ??= this.reopen().finally(() => this.opening = null);
|
|
325
|
+
await this.opening;
|
|
326
|
+
}
|
|
327
|
+
reopen() {
|
|
328
|
+
if (!this.p.sc) {
|
|
329
|
+
this.p = { ...this.p, sc: newClaim() };
|
|
330
|
+
this.persist();
|
|
331
|
+
}
|
|
332
|
+
const e = Math.floor(Date.now() / 1e3) + QR_LIFETIME_S - 10;
|
|
333
|
+
const ws = this.socket(relaySocketUrl(this.p.r, this.p.c, "site", e, this.p.sc));
|
|
334
|
+
return new Promise((resolve, reject) => {
|
|
335
|
+
ws.onopen = () => {
|
|
336
|
+
this.ws = ws;
|
|
337
|
+
this.wire(ws);
|
|
338
|
+
resolve();
|
|
339
|
+
};
|
|
340
|
+
ws.onerror = () => reject(new Error("Could not reach the bWalletX pairing service"));
|
|
341
|
+
ws.onclose = () => reject(new Error("Could not reach the bWalletX pairing service"));
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
async onFrame(raw) {
|
|
345
|
+
let f;
|
|
346
|
+
try {
|
|
347
|
+
f = JSON.parse(raw);
|
|
348
|
+
} catch {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const t = f.t;
|
|
352
|
+
if (t === "relay" && f.error) {
|
|
353
|
+
return this.failAll(new Error(`bWalletX pairing service: ${f.error}`));
|
|
354
|
+
}
|
|
355
|
+
if (!isSealed(f)) return;
|
|
356
|
+
const msg = await this.sealer.open(f);
|
|
357
|
+
if (!msg) return;
|
|
358
|
+
this.persist();
|
|
359
|
+
if (msg.t === "res") {
|
|
360
|
+
const w = this.waiting.get(msg.id);
|
|
361
|
+
if (!w) return;
|
|
362
|
+
clearTimeout(w.timer);
|
|
363
|
+
this.waiting.delete(msg.id);
|
|
364
|
+
if (msg.error) w.reject(Object.assign(new Error(msg.error.message), { code: msg.error.code }));
|
|
365
|
+
else w.resolve(msg.result);
|
|
366
|
+
} else if (msg.t === "close") {
|
|
367
|
+
this.drop();
|
|
368
|
+
this.failAll(new Error("bWalletX disconnected this site. Pair again."));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async call(action, params) {
|
|
372
|
+
await this.ensure();
|
|
373
|
+
const ws = this.ws;
|
|
374
|
+
const id = `${action}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
375
|
+
const result = new Promise((resolve, reject) => {
|
|
376
|
+
const timer = setTimeout(() => {
|
|
377
|
+
if (this.waiting.delete(id)) reject(new Error("No answer from bWalletX. Open it, unlocked, and approve."));
|
|
378
|
+
}, this.timeoutMs);
|
|
379
|
+
this.waiting.set(id, { resolve, reject, timer });
|
|
380
|
+
});
|
|
381
|
+
ws.send(JSON.stringify(await this.sealer.seal({ t: "req", id, action, params })));
|
|
382
|
+
this.persist();
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
close() {
|
|
386
|
+
const ws = this.ws;
|
|
387
|
+
this.ws = null;
|
|
388
|
+
ws?.close();
|
|
389
|
+
}
|
|
390
|
+
drop() {
|
|
391
|
+
this.forgotten = true;
|
|
392
|
+
void Promise.resolve(this.storage.removeItem(this.key)).catch(() => {
|
|
393
|
+
});
|
|
394
|
+
this.close();
|
|
395
|
+
}
|
|
396
|
+
async forget() {
|
|
397
|
+
if (this.forgotten) return;
|
|
398
|
+
try {
|
|
399
|
+
if (this.ws && this.ws.readyState === OPEN) {
|
|
400
|
+
this.ws.send(JSON.stringify(await this.sealer.seal({ t: "close", reason: "site unpaired" })));
|
|
401
|
+
}
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
this.drop();
|
|
405
|
+
}
|
|
406
|
+
wallet() {
|
|
407
|
+
return new Proxy({}, {
|
|
408
|
+
get: (_t, method) => {
|
|
409
|
+
if (method === "close") return () => this.close();
|
|
410
|
+
if (method === "forget") return () => this.forget();
|
|
411
|
+
if (typeof method !== "string" || method === "then" || method === "toJSON") return void 0;
|
|
412
|
+
return (params) => this.call(method, params ?? {});
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
function pairBwalletX(opts) {
|
|
418
|
+
const storage = opts.storage === void 0 ? defaultStorage() : opts.storage ?? memoryStorage();
|
|
419
|
+
const key = opts.storageKey ?? DEFAULT_KEY;
|
|
420
|
+
const socket = opts.socket ?? defaultSocket();
|
|
421
|
+
const relay = opts.relay ?? DEFAULT_RELAY;
|
|
422
|
+
const origin = opts.origin ?? (typeof location !== "undefined" ? location.origin : "");
|
|
423
|
+
if (!origin) return Promise.reject(new Error("pairBwalletX needs options.origin outside a browser"));
|
|
424
|
+
const siteKey = PrivateKey2.fromRandom();
|
|
425
|
+
const channel = newChannel();
|
|
426
|
+
const expiry = Math.floor(Date.now() / 1e3) + QR_LIFETIME_S;
|
|
427
|
+
const siteClaim = newClaim();
|
|
428
|
+
const sitePub = siteKey.toPublicKey().toString();
|
|
429
|
+
return walletClaimFor(channel, sitePub).then(claimDigest).then((wc) => new Promise((resolve, reject) => {
|
|
430
|
+
if (opts.signal?.aborted) return reject(new Error("cancelled"));
|
|
431
|
+
let ws;
|
|
432
|
+
try {
|
|
433
|
+
ws = socket(relaySocketUrl(relay, channel, "site", expiry, siteClaim, wc));
|
|
434
|
+
} catch (e) {
|
|
435
|
+
return reject(e);
|
|
436
|
+
}
|
|
437
|
+
let sealer = null;
|
|
438
|
+
let walletKey = "";
|
|
439
|
+
let settled = false;
|
|
440
|
+
const fail = (e) => {
|
|
441
|
+
if (settled) return;
|
|
442
|
+
settled = true;
|
|
443
|
+
clearTimeout(timer);
|
|
444
|
+
ws.close();
|
|
445
|
+
reject(e);
|
|
446
|
+
};
|
|
447
|
+
const timer = setTimeout(() => fail(new Error("The pairing code expired. Show a new one.")), QR_LIFETIME_S * 1e3);
|
|
448
|
+
opts.signal?.addEventListener("abort", () => fail(new Error("cancelled")));
|
|
449
|
+
ws.onerror = () => fail(new Error("Could not reach the bWalletX pairing service"));
|
|
450
|
+
ws.onclose = () => fail(new Error("The connection to bWalletX closed before pairing finished"));
|
|
451
|
+
ws.onmessage = inOrder(async (ev) => {
|
|
452
|
+
if (settled) return;
|
|
453
|
+
let f;
|
|
454
|
+
try {
|
|
455
|
+
f = JSON.parse(String(ev.data));
|
|
456
|
+
} catch {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (f.t === "hello" && !sealer) {
|
|
460
|
+
walletKey = f.k;
|
|
461
|
+
const s = await deriveSession(siteKey, walletKey, channel);
|
|
462
|
+
sealer = new Sealer(s.key, "site");
|
|
463
|
+
opts.onCode(s.code);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (!sealer || !isSealed(f)) return;
|
|
467
|
+
const msg = await sealer.open(f);
|
|
468
|
+
if (!msg) return;
|
|
469
|
+
if (msg.t === "ready") {
|
|
470
|
+
settled = true;
|
|
471
|
+
clearTimeout(timer);
|
|
472
|
+
const p = { v: 1, c: channel, r: relay, s: siteKey.toHex(), k: walletKey, ...sealer.counters, pairedAt: Date.now(), sc: siteClaim };
|
|
473
|
+
await Promise.resolve(storage.setItem(key, JSON.stringify(p))).catch(() => {
|
|
474
|
+
});
|
|
475
|
+
const link = new Link(p, sealer, storage, key, socket, opts.callTimeoutMs ?? 12e4);
|
|
476
|
+
link.adopt(ws);
|
|
477
|
+
resolve(link.wallet());
|
|
478
|
+
} else if (msg.t === "close") {
|
|
479
|
+
fail(new Error("Pairing was declined in bWalletX"));
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
opts.onLink(pairUrl({ v: PAIR_VERSION, r: relay, c: channel, k: sitePub, o: origin, e: expiry }));
|
|
483
|
+
}));
|
|
484
|
+
}
|
|
485
|
+
async function restorePairing(opts = {}) {
|
|
486
|
+
const storage = opts.storage === void 0 ? defaultStorage() : opts.storage ?? memoryStorage();
|
|
487
|
+
const key = opts.storageKey ?? DEFAULT_KEY;
|
|
488
|
+
const p = parseStored(await Promise.resolve(storage.getItem(key)).catch(() => null));
|
|
489
|
+
if (!p) return null;
|
|
490
|
+
const { key: aes } = await deriveSession(PrivateKey2.fromHex(p.s), p.k, p.c);
|
|
491
|
+
const sealer = new Sealer(aes, "site");
|
|
492
|
+
sealer.restore({ sent: p.sent, lastSeen: p.lastSeen });
|
|
493
|
+
return new Link(p, sealer, storage, key, opts.socket ?? defaultSocket(), opts.callTimeoutMs ?? 12e4).wallet();
|
|
494
|
+
}
|
|
495
|
+
async function clearPairing(opts = {}) {
|
|
496
|
+
const storage = opts.storage === void 0 ? defaultStorage() : opts.storage ?? memoryStorage();
|
|
497
|
+
await Promise.resolve(storage.removeItem(opts.storageKey ?? DEFAULT_KEY)).catch(() => {
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/signin.ts
|
|
502
|
+
var LOGIN_PROTOCOL = [2, "bwallet sign in"];
|
|
503
|
+
async function connectBwalletX(opts = {}) {
|
|
504
|
+
const status = (s) => {
|
|
505
|
+
try {
|
|
506
|
+
opts.onStatus?.(s);
|
|
507
|
+
} catch {
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
if (opts.wallet) {
|
|
511
|
+
status("opening");
|
|
512
|
+
return { wallet: opts.wallet, method: "wallet" };
|
|
513
|
+
}
|
|
514
|
+
status("finding");
|
|
515
|
+
const found = await discoverBwalletX(opts.discoverMs ?? DEFAULT_DISCOVER_MS);
|
|
516
|
+
if (found) {
|
|
517
|
+
status("opening");
|
|
518
|
+
return { wallet: found.wallet, method: found.method };
|
|
519
|
+
}
|
|
520
|
+
if (opts.reusePairing !== false) {
|
|
521
|
+
const restored = await restorePairing(opts.pairing ?? {});
|
|
522
|
+
if (restored) {
|
|
523
|
+
status("opening");
|
|
524
|
+
return { wallet: restored, method: "pairing" };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (!opts.pairing) {
|
|
528
|
+
throw Object.assign(new Error("bWalletX was not found in this browser"), { code: "NOT_FOUND" });
|
|
529
|
+
}
|
|
530
|
+
status("pairing");
|
|
531
|
+
return { wallet: await pairBwalletX(opts.pairing), method: "pairing" };
|
|
532
|
+
}
|
|
533
|
+
async function waitIfLocked(wallet) {
|
|
534
|
+
let ok = false;
|
|
535
|
+
try {
|
|
536
|
+
ok = !!(await wallet.isAuthenticated({}))?.authenticated;
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
539
|
+
if (!ok) await Promise.resolve().then(() => wallet.waitForAuthentication({})).catch(() => {
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
function challengeWantsIdentityKey(e) {
|
|
543
|
+
const msg = e instanceof Error ? e.message : String(e ?? "");
|
|
544
|
+
return /identityKey/i.test(msg);
|
|
545
|
+
}
|
|
546
|
+
async function signInWithBwalletX(opts) {
|
|
547
|
+
const { wallet, method } = await connectBwalletX(opts);
|
|
548
|
+
if (method !== "pairing" && !opts.skipWaitForAuthentication) await waitIfLocked(wallet);
|
|
549
|
+
const status = () => {
|
|
550
|
+
try {
|
|
551
|
+
opts.onStatus?.("signing");
|
|
552
|
+
} catch {
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
const sign = (c2) => wallet.createSignature({
|
|
556
|
+
protocolID: c2.protocolID ?? LOGIN_PROTOCOL,
|
|
557
|
+
keyID: c2.keyID,
|
|
558
|
+
counterparty: "anyone",
|
|
559
|
+
data: Array.from(new TextEncoder().encode(c2.message))
|
|
560
|
+
});
|
|
561
|
+
const keyFirst = async () => {
|
|
562
|
+
const { publicKey: identityKey2 } = await atStage("identity", () => wallet.getPublicKey({ identityKey: true }));
|
|
563
|
+
status();
|
|
564
|
+
const c2 = await atStage("challenge", () => opts.challenge(identityKey2));
|
|
565
|
+
const { signature: signature2 } = await atStage("signature", () => sign(c2));
|
|
566
|
+
return { identityKey: identityKey2, signature: Array.from(signature2), nonce: c2.nonce, method, wallet };
|
|
567
|
+
};
|
|
568
|
+
if (opts.challengeNeedsIdentityKey && !opts.identityKey) return keyFirst();
|
|
569
|
+
let c;
|
|
570
|
+
try {
|
|
571
|
+
c = await atStage("challenge", () => opts.challenge(opts.identityKey));
|
|
572
|
+
} catch (e) {
|
|
573
|
+
if (!opts.identityKey && challengeWantsIdentityKey(e)) return keyFirst();
|
|
574
|
+
throw e;
|
|
575
|
+
}
|
|
576
|
+
status();
|
|
577
|
+
const [{ publicKey: identityKey }, { signature }] = await Promise.all([
|
|
578
|
+
opts.identityKey ? Promise.resolve({ publicKey: opts.identityKey }) : atStage("identity", () => wallet.getPublicKey({ identityKey: true })),
|
|
579
|
+
atStage("signature", () => sign(c))
|
|
580
|
+
]);
|
|
581
|
+
return { identityKey, signature: Array.from(signature), nonce: c.nonce, method, wallet };
|
|
582
|
+
}
|
|
583
|
+
async function signInAndVerify(opts) {
|
|
584
|
+
const hinted = !opts.identityKey && !!opts.identityKeyHint;
|
|
585
|
+
const r = await signInWithBwalletX({ ...opts, identityKey: opts.identityKey ?? opts.identityKeyHint });
|
|
586
|
+
try {
|
|
587
|
+
return { result: r, verified: await atStage("verify", () => opts.verify(r)) };
|
|
588
|
+
} catch (e) {
|
|
589
|
+
const status = e?.status;
|
|
590
|
+
if (!hinted || status !== 401) throw e;
|
|
591
|
+
const again = await signInWithBwalletX({ ...opts, wallet: r.wallet, identityKey: void 0, skipWaitForAuthentication: true });
|
|
592
|
+
return { result: again, verified: await atStage("verify", () => opts.verify(again)) };
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async function atStage(stage, run) {
|
|
596
|
+
try {
|
|
597
|
+
return await run();
|
|
598
|
+
} catch (e) {
|
|
599
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
600
|
+
if (!err.stage) Object.assign(err, { stage });
|
|
601
|
+
throw err;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/wallets.ts
|
|
606
|
+
import { WalletClient as WalletClient2 } from "@bsv/sdk";
|
|
607
|
+
async function brc100Connected(wallet, o, c = {}) {
|
|
608
|
+
if (c.identity === false) return { wallet, payer: wallet, identityKey: null, optionId: o.id, kind: o.kind };
|
|
609
|
+
await waitIfLocked(wallet);
|
|
610
|
+
const { publicKey } = await wallet.getPublicKey({ identityKey: true });
|
|
611
|
+
return { wallet, payer: wallet, identityKey: publicKey, optionId: o.id, kind: o.kind };
|
|
612
|
+
}
|
|
613
|
+
function brc100Option(wallet, info) {
|
|
614
|
+
const opt = {
|
|
615
|
+
id: info.id,
|
|
616
|
+
name: info.name,
|
|
617
|
+
icon: info.icon,
|
|
618
|
+
rdns: info.rdns,
|
|
619
|
+
kind: "brc100",
|
|
620
|
+
isBwalletX: info.isBwalletX ?? false,
|
|
621
|
+
substrate: info.substrate,
|
|
622
|
+
wallet,
|
|
623
|
+
connect: (c) => brc100Connected(wallet, opt, c)
|
|
624
|
+
};
|
|
625
|
+
return opt;
|
|
626
|
+
}
|
|
627
|
+
function pairingOption(pairing) {
|
|
628
|
+
const opt = {
|
|
629
|
+
id: "bwalletx-pairing",
|
|
630
|
+
name: "bWalletX on your phone",
|
|
631
|
+
kind: "pairing",
|
|
632
|
+
isBwalletX: true,
|
|
633
|
+
substrate: "pairing",
|
|
634
|
+
async connect(c) {
|
|
635
|
+
const p = typeof pairing === "function" ? pairing() : pairing;
|
|
636
|
+
const w = await restorePairing(p) ?? await pairBwalletX(p);
|
|
637
|
+
return brc100Connected(w, opt, c);
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
return opt;
|
|
641
|
+
}
|
|
642
|
+
function handcashAdapter(o) {
|
|
643
|
+
const opt = {
|
|
644
|
+
id: "handcash",
|
|
645
|
+
name: "HandCash",
|
|
646
|
+
icon: o.icon,
|
|
647
|
+
kind: "handcash",
|
|
648
|
+
isBwalletX: false,
|
|
649
|
+
substrate: "adapter",
|
|
650
|
+
async connect() {
|
|
651
|
+
const p = await o.login();
|
|
652
|
+
return {
|
|
653
|
+
wallet: null,
|
|
654
|
+
payer: o.pay ? { pay: o.pay } : null,
|
|
655
|
+
identityKey: p.identityKey ?? null,
|
|
656
|
+
handle: p.handle,
|
|
657
|
+
paymail: p.paymail ?? `${p.handle.replace(/^\$/, "")}@handcash.io`,
|
|
658
|
+
optionId: opt.id,
|
|
659
|
+
kind: "handcash",
|
|
660
|
+
transitional: true
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
return opt;
|
|
665
|
+
}
|
|
666
|
+
var NAMES = { [RDNS_EXTENSION]: "bWalletX", "space.bwallet.mobile": "bWalletX" };
|
|
667
|
+
async function probe(substrate, ms) {
|
|
668
|
+
try {
|
|
669
|
+
const w = new WalletClient2(substrate);
|
|
670
|
+
const ok = await Promise.race([w.isAuthenticated({}).then(() => true), new Promise((r) => setTimeout(() => r(false), ms))]);
|
|
671
|
+
return ok ? w : null;
|
|
672
|
+
} catch {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function cwiIsAnnounced(cwi, announced) {
|
|
677
|
+
return announced.some((o) => o.wallet === cwi || o.rdns === RDNS_MOBILE);
|
|
678
|
+
}
|
|
679
|
+
async function discoverWallets(o = {}) {
|
|
680
|
+
const out = [];
|
|
681
|
+
const ms = o.ms ?? 300;
|
|
682
|
+
if (typeof window !== "undefined") {
|
|
683
|
+
const seen = /* @__PURE__ */ new Map();
|
|
684
|
+
const onAnnounce = (e) => {
|
|
685
|
+
const d = e.detail;
|
|
686
|
+
if (!d?.wallet) return;
|
|
687
|
+
const rdns = d.info?.rdns ?? d.info?.uuid ?? `wallet-${seen.size}`;
|
|
688
|
+
if (seen.has(rdns)) return;
|
|
689
|
+
const isB = BWALLETX_RDNS.includes(rdns);
|
|
690
|
+
const substrate = rdns === RDNS_EXTENSION ? "extension" : rdns === RDNS_MOBILE ? "in-app" : "injected";
|
|
691
|
+
seen.set(rdns, brc100Option(d.wallet, { id: rdns, rdns, name: NAMES[rdns] ?? d.info?.name ?? rdns, icon: d.info?.icon, isBwalletX: isB, substrate }));
|
|
692
|
+
};
|
|
693
|
+
window.addEventListener("brc100:announceWallet", onAnnounce);
|
|
694
|
+
window.dispatchEvent(new Event("brc100:requestWallet"));
|
|
695
|
+
const local = o.probeLocal ? Promise.all([probe("json-api", ms), probe("Cicada", ms)]) : Promise.resolve([null, null]);
|
|
696
|
+
await new Promise((r) => setTimeout(r, seen.size > 0 || window.CWI ? Math.min(ms, 50) : ms));
|
|
697
|
+
window.removeEventListener("brc100:announceWallet", onAnnounce);
|
|
698
|
+
out.push(...seen.values());
|
|
699
|
+
if (![...seen.values()].some((w) => w.isBwalletX) && isFramed()) {
|
|
700
|
+
const x = await probeXdm(Math.max(ms, 1200));
|
|
701
|
+
if (x) out.push(brc100Option(x, { id: "bwalletx-xdm", name: "bWalletX", isBwalletX: true, substrate: "xdm" }));
|
|
702
|
+
}
|
|
703
|
+
const cwi = window.CWI;
|
|
704
|
+
if (cwi && !cwiIsAnnounced(cwi, [...seen.values()])) {
|
|
705
|
+
out.push(brc100Option(cwi, { id: "window.CWI", name: seen.size ? "Another browser wallet" : "Browser wallet", isBwalletX: false, substrate: "window.CWI" }));
|
|
706
|
+
}
|
|
707
|
+
const [json, cicada] = await local;
|
|
708
|
+
if (json) out.push(brc100Option(json, { id: "localhost:3321", name: "Desktop wallet (localhost:3321)", substrate: "json-api" }));
|
|
709
|
+
if (cicada) out.push(brc100Option(cicada, { id: "localhost:3301", name: "Desktop wallet (localhost:3301)", substrate: "cicada" }));
|
|
710
|
+
}
|
|
711
|
+
if (o.pairing) out.push(pairingOption(o.pairing));
|
|
712
|
+
out.push(...o.extra ?? []);
|
|
713
|
+
const rank = (w) => w.isBwalletX ? 0 : w.kind === "handcash" ? 2 : 1;
|
|
714
|
+
return out.map((w, i) => ({ w, i })).sort((a, b) => rank(a.w) - rank(b.w) || a.i - b.i).map((x) => x.w);
|
|
715
|
+
}
|
|
716
|
+
var DESKTOP_OPTION_ID = "desktop";
|
|
717
|
+
function desktopOption(ms = 1500) {
|
|
718
|
+
const opt = {
|
|
719
|
+
id: DESKTOP_OPTION_ID,
|
|
720
|
+
name: "Desktop wallet",
|
|
721
|
+
kind: "brc100",
|
|
722
|
+
isBwalletX: false,
|
|
723
|
+
substrate: "json-api",
|
|
724
|
+
async connect(c) {
|
|
725
|
+
const [json, cicada] = await Promise.all([probe("json-api", ms), probe("Cicada", ms)]);
|
|
726
|
+
const w = json ?? cicada;
|
|
727
|
+
if (!w) throw Object.assign(new Error("No desktop wallet answered on this computer"), { code: "NOT_FOUND" });
|
|
728
|
+
opt.wallet = w;
|
|
729
|
+
opt.substrate = json ? "json-api" : "cicada";
|
|
730
|
+
return brc100Connected(w, opt, c);
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
return opt;
|
|
734
|
+
}
|
|
735
|
+
function autoChoice(list, force = false) {
|
|
736
|
+
const auto = list.filter((w) => !isLocalDesktop(w));
|
|
737
|
+
const detected = auto.filter((w) => w.kind === "brc100");
|
|
738
|
+
const others = auto.filter((w) => w.kind === "handcash" || w.kind === "custom");
|
|
739
|
+
const hasLocal = auto.length !== list.length;
|
|
740
|
+
if (force) return detected[0] ?? auto.find((w) => w.kind !== "pairing") ?? auto[0] ?? null;
|
|
741
|
+
if (!list.length) return null;
|
|
742
|
+
if (list.length === 1) return hasLocal ? void 0 : list[0];
|
|
743
|
+
if (detected.length === 1 && others.length === 0 && !hasLocal) return detected[0];
|
|
744
|
+
if (detected.length === 0 && others.length === 0) return auto[0] ?? void 0;
|
|
745
|
+
return void 0;
|
|
746
|
+
}
|
|
747
|
+
function isLocalDesktop(w) {
|
|
748
|
+
return w.substrate === "json-api" || w.substrate === "cicada" || w.id === "localhost:3321" || w.id === "localhost:3301" || w.id === DESKTOP_OPTION_ID;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/account.ts
|
|
752
|
+
var PAYMAIL_LOOKUP_URL = "https://pay.bwallet.space/api/paymail/lookup?key=";
|
|
753
|
+
var BAVATAR_URL = "https://chat.bwalletx.com/api/bavatar/";
|
|
754
|
+
function shortKey(key) {
|
|
755
|
+
return key.length > 12 ? `${key.slice(0, 6)}\u2026${key.slice(-4)}` : key;
|
|
756
|
+
}
|
|
757
|
+
function accountLabel(a) {
|
|
758
|
+
if (a.paymail && /@bwalletx\.com$/i.test(a.paymail)) return "$" + a.paymail.split("@")[0];
|
|
759
|
+
if (a.handle) return a.handle;
|
|
760
|
+
if (a.paymail) return a.paymail;
|
|
761
|
+
return a.identityKey ? shortKey(a.identityKey) : "";
|
|
762
|
+
}
|
|
763
|
+
function avatarUrlFor(identityKey) {
|
|
764
|
+
return identityKey && /^0[23][0-9a-fA-F]{64}$/.test(identityKey) ? `${BAVATAR_URL}${identityKey.toLowerCase()}.png` : null;
|
|
765
|
+
}
|
|
766
|
+
var cache = /* @__PURE__ */ new Map();
|
|
767
|
+
function lookupPaymail(identityKey, url = PAYMAIL_LOOKUP_URL) {
|
|
768
|
+
if (url === "off" || typeof fetch === "undefined") return Promise.resolve(null);
|
|
769
|
+
const k = `${url}|${identityKey}`;
|
|
770
|
+
let p = cache.get(k);
|
|
771
|
+
if (!p) {
|
|
772
|
+
p = fetch(url + encodeURIComponent(identityKey)).then((r) => r.ok ? r.json() : null).then((j) => j && typeof j.paymail === "string" ? j.paymail : null).catch(() => null);
|
|
773
|
+
cache.set(k, p);
|
|
774
|
+
}
|
|
775
|
+
return p;
|
|
776
|
+
}
|
|
777
|
+
function isUserRejection(e) {
|
|
778
|
+
const code = e?.code;
|
|
779
|
+
if (code === "ERR_PERMISSION_DENIED" || code === 4001) return true;
|
|
780
|
+
const msg = e instanceof Error ? e.message : String(e ?? "");
|
|
781
|
+
return /denied|reject|cancel|declin|refused|not now|user closed/i.test(msg);
|
|
782
|
+
}
|
|
783
|
+
function signInErrorText(e) {
|
|
784
|
+
if (isUserRejection(e)) return "Sign-in cancelled";
|
|
785
|
+
if (e?.name === "MetanetUnreachableError") return e.message;
|
|
786
|
+
const code = e?.code;
|
|
787
|
+
if (code === "NOT_FOUND") return "bWalletX was not found in this browser";
|
|
788
|
+
const msg = e instanceof Error ? e.message : String(e ?? "");
|
|
789
|
+
if (/lock/i.test(msg)) return "Your wallet is locked. Unlock it and try again";
|
|
790
|
+
if (/timeout|timed out|expired/i.test(msg)) return "The wallet did not answer in time";
|
|
791
|
+
if (/origin/i.test(msg)) return "This page is not on the site\u2019s configured origin";
|
|
792
|
+
return "Sign-in failed";
|
|
793
|
+
}
|
|
794
|
+
async function copyText(text) {
|
|
795
|
+
try {
|
|
796
|
+
await navigator.clipboard.writeText(text);
|
|
797
|
+
return true;
|
|
798
|
+
} catch {
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
var IDENTITY_HINT_KEY = "bwx:identityKey";
|
|
803
|
+
function rememberedIdentity() {
|
|
804
|
+
try {
|
|
805
|
+
const k = globalThis.localStorage?.getItem(IDENTITY_HINT_KEY) ?? "";
|
|
806
|
+
return /^0[23][0-9a-f]{64}$/i.test(k) ? k : void 0;
|
|
807
|
+
} catch {
|
|
808
|
+
return void 0;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function rememberIdentity(key) {
|
|
812
|
+
try {
|
|
813
|
+
if (key) globalThis.localStorage?.setItem(IDENTITY_HINT_KEY, key);
|
|
814
|
+
else globalThis.localStorage?.removeItem(IDENTITY_HINT_KEY);
|
|
815
|
+
} catch {
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/device.ts
|
|
820
|
+
function isTouchDevice(env = globalThis) {
|
|
821
|
+
const nav = env.navigator;
|
|
822
|
+
if (!nav) return false;
|
|
823
|
+
const ua = nav.userAgent ?? "";
|
|
824
|
+
if (/Android|iPhone|iPad|iPod|Mobile|Silk|Kindle/i.test(ua)) return true;
|
|
825
|
+
if (/Macintosh/.test(ua) && (nav.maxTouchPoints ?? 0) > 1) return true;
|
|
826
|
+
try {
|
|
827
|
+
return !!env.matchMedia?.("(pointer: coarse)").matches && !env.matchMedia?.("(hover: hover)").matches;
|
|
828
|
+
} catch {
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/metanet.ts
|
|
834
|
+
import { WalletClient as WalletClient3 } from "@bsv/sdk";
|
|
835
|
+
var METANET_TYPE = "metanet";
|
|
836
|
+
var METANET_OPTION_ID = METANET_TYPE;
|
|
837
|
+
var METANET_JSON_API = "http://localhost:3321";
|
|
838
|
+
var METANET_LABEL = "Use Metanet Desktop or another BRC-100 wallet";
|
|
839
|
+
var METANET_NOTE = "Metanet Desktop must be running on this computer";
|
|
840
|
+
var METANET_REMEMBERED_NOTE = "Signs in with Metanet Desktop on this computer.";
|
|
841
|
+
var NOT_YOU_LABEL = "Not you? Use another wallet";
|
|
842
|
+
var METANET_UNREACHABLE = "We couldn\u2019t reach Metanet Desktop. Open it, then try again.";
|
|
843
|
+
var METANET_ANSWER_MS = 4e3;
|
|
844
|
+
var METANET_SILENT_MS = 2e3;
|
|
845
|
+
var MetanetUnreachableError = class extends Error {
|
|
846
|
+
code = "NOT_FOUND";
|
|
847
|
+
constructor() {
|
|
848
|
+
super(METANET_UNREACHABLE);
|
|
849
|
+
this.name = "MetanetUnreachableError";
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
function inAnyFrame(win = typeof window === "undefined" ? void 0 : window) {
|
|
853
|
+
if (!win) return false;
|
|
854
|
+
try {
|
|
855
|
+
return win.top !== win.self;
|
|
856
|
+
} catch {
|
|
857
|
+
return true;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
function metanetOffered(env = {}) {
|
|
861
|
+
const win = env.window ?? (typeof window === "undefined" ? void 0 : window);
|
|
862
|
+
if (!win) return false;
|
|
863
|
+
const nav = env.navigator ?? (typeof navigator === "undefined" ? void 0 : navigator);
|
|
864
|
+
const mm = env.matchMedia ?? (typeof matchMedia === "undefined" ? void 0 : (q) => matchMedia(q));
|
|
865
|
+
return !isTouchDevice({ navigator: nav, matchMedia: mm }) && !inAnyFrame(win);
|
|
866
|
+
}
|
|
867
|
+
function client(o) {
|
|
868
|
+
if (o.make) return o.make();
|
|
869
|
+
const originator = o.originator ?? (typeof location !== "undefined" && location.host ? location.host : void 0);
|
|
870
|
+
return new WalletClient3("json-api", originator);
|
|
871
|
+
}
|
|
872
|
+
async function answers(wallet) {
|
|
873
|
+
return Promise.resolve().then(() => wallet.getVersion({})).then((v) => typeof v?.version === "string", () => false);
|
|
874
|
+
}
|
|
875
|
+
async function connectMetanetDesktop(o = {}) {
|
|
876
|
+
let wallet;
|
|
877
|
+
try {
|
|
878
|
+
wallet = client(o);
|
|
879
|
+
} catch {
|
|
880
|
+
throw new MetanetUnreachableError();
|
|
881
|
+
}
|
|
882
|
+
let timer;
|
|
883
|
+
const ok = await Promise.race([answers(wallet), new Promise((r) => timer = setTimeout(() => r(false), o.timeoutMs ?? METANET_ANSWER_MS))]);
|
|
884
|
+
clearTimeout(timer);
|
|
885
|
+
if (!ok) throw new MetanetUnreachableError();
|
|
886
|
+
return wallet;
|
|
887
|
+
}
|
|
888
|
+
function metanetOption(o = {}) {
|
|
889
|
+
const opt = {
|
|
890
|
+
id: METANET_OPTION_ID,
|
|
891
|
+
name: METANET_LABEL,
|
|
892
|
+
kind: "brc100",
|
|
893
|
+
isBwalletX: false,
|
|
894
|
+
substrate: "json-api",
|
|
895
|
+
async connect() {
|
|
896
|
+
const wallet = await connectMetanetDesktop(o);
|
|
897
|
+
opt.wallet = wallet;
|
|
898
|
+
await waitIfLocked(wallet);
|
|
899
|
+
const { publicKey } = await wallet.getPublicKey({ identityKey: true });
|
|
900
|
+
return { wallet, payer: wallet, identityKey: publicKey, optionId: opt.id, kind: "brc100" };
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
return opt;
|
|
904
|
+
}
|
|
905
|
+
var KNOWN_KEYS_KEY = "bwx:known-keys";
|
|
906
|
+
var KEY_RE = /^0[23][0-9a-f]{64}$/;
|
|
907
|
+
var MAX_KNOWN = 8;
|
|
908
|
+
function knownIdentityKeys() {
|
|
909
|
+
try {
|
|
910
|
+
const v = JSON.parse(globalThis.localStorage?.getItem(KNOWN_KEYS_KEY) || "[]");
|
|
911
|
+
return Array.isArray(v) ? v.filter((k) => typeof k === "string" && KEY_RE.test(k)) : [];
|
|
912
|
+
} catch {
|
|
913
|
+
return [];
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function isKnownIdentityKey(key) {
|
|
917
|
+
if (!key) return false;
|
|
918
|
+
const k = key.toLowerCase();
|
|
919
|
+
if (!KEY_RE.test(k)) return false;
|
|
920
|
+
return knownIdentityKeys().includes(k) || rememberedIdentity()?.toLowerCase() === k;
|
|
921
|
+
}
|
|
922
|
+
function rememberKnownIdentityKey(key) {
|
|
923
|
+
const k = (key ?? "").toLowerCase();
|
|
924
|
+
if (!KEY_RE.test(k)) return;
|
|
925
|
+
try {
|
|
926
|
+
const list = [k, ...knownIdentityKeys().filter((x) => x !== k)].slice(0, MAX_KNOWN);
|
|
927
|
+
globalThis.localStorage?.setItem(KNOWN_KEYS_KEY, JSON.stringify(list));
|
|
928
|
+
} catch {
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
var triedThisLoad = false;
|
|
932
|
+
function silentAttemptUsed() {
|
|
933
|
+
return triedThisLoad;
|
|
934
|
+
}
|
|
935
|
+
function claimSilentAttempt() {
|
|
936
|
+
if (triedThisLoad) return false;
|
|
937
|
+
triedThisLoad = true;
|
|
938
|
+
return true;
|
|
939
|
+
}
|
|
940
|
+
function resetSilentAttempt() {
|
|
941
|
+
triedThisLoad = false;
|
|
942
|
+
}
|
|
943
|
+
async function trySilentMetanet(o = {}) {
|
|
944
|
+
if (!metanetOffered()) return { ok: false, stop: "not-here" };
|
|
945
|
+
if (!claimSilentAttempt()) return { ok: false, stop: "used" };
|
|
946
|
+
const run = async () => {
|
|
947
|
+
let wallet;
|
|
948
|
+
try {
|
|
949
|
+
wallet = client(o);
|
|
950
|
+
} catch {
|
|
951
|
+
return { ok: false, stop: "unreachable" };
|
|
952
|
+
}
|
|
953
|
+
if (!await answers(wallet)) return { ok: false, stop: "unreachable" };
|
|
954
|
+
const unlocked = await Promise.resolve().then(() => wallet.isAuthenticated({})).then((r2) => r2?.authenticated === true, () => false);
|
|
955
|
+
if (!unlocked) return { ok: false, stop: "locked" };
|
|
956
|
+
const identityKey = await Promise.resolve().then(() => wallet.getPublicKey({ identityKey: true })).then((r2) => r2.publicKey, () => null);
|
|
957
|
+
if (!identityKey) return { ok: false, stop: "unreachable" };
|
|
958
|
+
if (!isKnownIdentityKey(identityKey)) return { ok: false, stop: "unknown-key" };
|
|
959
|
+
return { ok: true, wallet, identityKey };
|
|
960
|
+
};
|
|
961
|
+
let timer;
|
|
962
|
+
const r = await Promise.race([
|
|
963
|
+
run().catch(() => ({ ok: false, stop: "unreachable" })),
|
|
964
|
+
new Promise((res) => timer = setTimeout(() => res({ ok: false, stop: "slow" }), o.windowMs ?? METANET_SILENT_MS))
|
|
965
|
+
]);
|
|
966
|
+
clearTimeout(timer);
|
|
967
|
+
return r;
|
|
968
|
+
}
|
|
969
|
+
var warnedLogout = false;
|
|
970
|
+
var isProduction = () => {
|
|
971
|
+
try {
|
|
972
|
+
return globalThis.process?.env?.NODE_ENV === "production";
|
|
973
|
+
} catch {
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
function warnIfNoLogout(o) {
|
|
978
|
+
if (!o.verifyUrl || o.logoutUrl || warnedLogout || isProduction() || typeof console === "undefined") return false;
|
|
979
|
+
warnedLogout = true;
|
|
980
|
+
console.warn(
|
|
981
|
+
`@bwalletx/connect: ${o.where} has a verify URL but no logout URL. Set it: "Not you? Use another wallet" and Disconnect must be able to end a session the verify route created.`
|
|
982
|
+
);
|
|
983
|
+
return true;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/choice.ts
|
|
987
|
+
var BWALLETX_TYPE = "bwalletx";
|
|
988
|
+
function substrateOf(o) {
|
|
989
|
+
if (o.substrate) return o.substrate;
|
|
990
|
+
if (o.kind === "pairing") return "pairing";
|
|
991
|
+
if (o.isBwalletX && o.kind === "brc100") return o.rdns === RDNS_MOBILE ? "in-app" : o.id === "bwalletx-xdm" ? "xdm" : "extension";
|
|
992
|
+
if (o.kind === "handcash" || o.kind === "custom") return "adapter";
|
|
993
|
+
if (o.rdns === RDNS_EXTENSION) return "extension";
|
|
994
|
+
if (o.rdns === RDNS_MOBILE) return "in-app";
|
|
995
|
+
if (o.id === "window.CWI") return "window.CWI";
|
|
996
|
+
if (o.id === "localhost:3321" || o.id === DESKTOP_OPTION_ID) return "json-api";
|
|
997
|
+
if (o.id === "localhost:3301") return "cicada";
|
|
998
|
+
return "injected";
|
|
999
|
+
}
|
|
1000
|
+
function walletTypeOf(o) {
|
|
1001
|
+
return o.isBwalletX ? BWALLETX_TYPE : o.rdns ?? o.id;
|
|
1002
|
+
}
|
|
1003
|
+
var OWN_MARKERS = ["extension", "in-app", "xdm"];
|
|
1004
|
+
function isPresentBwalletX(o) {
|
|
1005
|
+
return o.isBwalletX && o.kind === "brc100" && !!o.wallet && OWN_MARKERS.includes(substrateOf(o));
|
|
1006
|
+
}
|
|
1007
|
+
function hasBwalletX(list) {
|
|
1008
|
+
return list.some(isPresentBwalletX);
|
|
1009
|
+
}
|
|
1010
|
+
function otherDetected(list) {
|
|
1011
|
+
return list.filter((o) => !o.isBwalletX && o.kind === "brc100" && !!o.wallet);
|
|
1012
|
+
}
|
|
1013
|
+
function chooserOrder(list) {
|
|
1014
|
+
const rank = (o) => isPresentBwalletX(o) ? 0 : o.isBwalletX ? 1 : o.kind === "brc100" ? 2 : o.kind === "custom" ? 3 : 4;
|
|
1015
|
+
return list.map((o, i) => ({ o, i })).sort((a, b) => rank(a.o) - rank(b.o) || a.i - b.i).map((x) => x.o);
|
|
1016
|
+
}
|
|
1017
|
+
function decideWallet(list, o = {}) {
|
|
1018
|
+
if (o.walletId) {
|
|
1019
|
+
const w = list.find((x) => x.id === o.walletId);
|
|
1020
|
+
if (w) return { option: w, reason: "requested" };
|
|
1021
|
+
}
|
|
1022
|
+
if (o.chooser) return { option: null, reason: list.length || o.picker !== false ? "chooser" : "none" };
|
|
1023
|
+
if (o.fastPath === false) {
|
|
1024
|
+
const a = autoChoice([...list], o.picker === false);
|
|
1025
|
+
return a === void 0 ? { option: null, reason: "chooser" } : a === null ? { option: null, reason: "none" } : { option: a, reason: "legacy" };
|
|
1026
|
+
}
|
|
1027
|
+
if (o.remembered && !o.preferBwalletX) {
|
|
1028
|
+
const m = matchRemembered(list, o.remembered);
|
|
1029
|
+
if (m) return { option: m, reason: "remembered" };
|
|
1030
|
+
}
|
|
1031
|
+
const b = list.find(isPresentBwalletX);
|
|
1032
|
+
if (b) return { option: b, reason: "bwalletx" };
|
|
1033
|
+
if (o.picker === false) {
|
|
1034
|
+
const phone = list.find((x) => x.isBwalletX);
|
|
1035
|
+
return phone ? { option: phone, reason: "bwalletx-phone" } : { option: null, reason: "none" };
|
|
1036
|
+
}
|
|
1037
|
+
return list.length ? { option: null, reason: "chooser" } : { option: null, reason: "none" };
|
|
1038
|
+
}
|
|
1039
|
+
function outcomeOf(e) {
|
|
1040
|
+
const err = e;
|
|
1041
|
+
if (err?.code === "NOT_FOUND") return "not-found";
|
|
1042
|
+
if (err?.name === "AbortError" || err?.message === "cancelled") return "cancelled";
|
|
1043
|
+
if (isUserRejection(e)) return "rejected";
|
|
1044
|
+
return "error";
|
|
1045
|
+
}
|
|
1046
|
+
function attemptFor(option, outcome, startedAt, now = Date.now()) {
|
|
1047
|
+
return {
|
|
1048
|
+
walletType: option ? walletTypeOf(option) : null,
|
|
1049
|
+
substrate: option ? substrateOf(option) : null,
|
|
1050
|
+
outcome,
|
|
1051
|
+
ms: Math.max(0, Math.round(now - startedAt))
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
function reportAttempt(hook, a) {
|
|
1055
|
+
if (!hook) return;
|
|
1056
|
+
try {
|
|
1057
|
+
hook(Object.freeze({ walletType: a.walletType, substrate: a.substrate, outcome: a.outcome, ms: a.ms }));
|
|
1058
|
+
} catch {
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
var WALLET_CHOICE_KEY = "bwx:wallet";
|
|
1062
|
+
var SUBSTRATES = ["extension", "in-app", "xdm", "injected", "window.CWI", "json-api", "cicada", "pairing", "adapter"];
|
|
1063
|
+
var TYPE_RE = /^[A-Za-z0-9._:-]{1,64}$/;
|
|
1064
|
+
var KEY_LIKE = /^(0[23][0-9a-f]{64}|04[0-9a-f]{128}|[13][1-9A-HJ-NP-Za-km-z]{25,34})$/i;
|
|
1065
|
+
function isRememberable(o) {
|
|
1066
|
+
const t = walletTypeOf(o);
|
|
1067
|
+
return substrateOf(o) !== "window.CWI" && TYPE_RE.test(t) && !KEY_LIKE.test(t);
|
|
1068
|
+
}
|
|
1069
|
+
function isExplicitPick(o, reason) {
|
|
1070
|
+
return o.isBwalletX || reason === "chooser" || reason === "requested" || reason === "remembered";
|
|
1071
|
+
}
|
|
1072
|
+
function rememberWalletChoice(o) {
|
|
1073
|
+
try {
|
|
1074
|
+
if (!o) return void globalThis.localStorage?.removeItem(WALLET_CHOICE_KEY);
|
|
1075
|
+
if (!isRememberable(o)) return;
|
|
1076
|
+
const v = { type: walletTypeOf(o), substrate: substrateOf(o), v: 1 };
|
|
1077
|
+
globalThis.localStorage?.setItem(WALLET_CHOICE_KEY, JSON.stringify(v));
|
|
1078
|
+
} catch {
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
function rememberedWalletChoice() {
|
|
1082
|
+
try {
|
|
1083
|
+
const raw = globalThis.localStorage?.getItem(WALLET_CHOICE_KEY);
|
|
1084
|
+
if (!raw) return null;
|
|
1085
|
+
const j = JSON.parse(raw);
|
|
1086
|
+
if (typeof j.type !== "string" || !TYPE_RE.test(j.type) || KEY_LIKE.test(j.type)) return null;
|
|
1087
|
+
if (!SUBSTRATES.includes(j.substrate) || j.substrate === "window.CWI") return null;
|
|
1088
|
+
return { type: j.type, substrate: j.substrate };
|
|
1089
|
+
} catch {
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
function forgetWalletChoice() {
|
|
1094
|
+
rememberWalletChoice(null);
|
|
1095
|
+
}
|
|
1096
|
+
function needsBwalletX(type) {
|
|
1097
|
+
return !!type && type !== BWALLETX_TYPE;
|
|
1098
|
+
}
|
|
1099
|
+
function tapGoesElsewhere(type) {
|
|
1100
|
+
return needsBwalletX(type) && type !== METANET_TYPE && type !== DESKTOP_OPTION_ID;
|
|
1101
|
+
}
|
|
1102
|
+
function isLocalDesktopType(r) {
|
|
1103
|
+
return r.type === METANET_TYPE || r.type === DESKTOP_OPTION_ID || r.substrate === "json-api" || r.substrate === "cicada";
|
|
1104
|
+
}
|
|
1105
|
+
function matchRemembered(list, r) {
|
|
1106
|
+
if (isLocalDesktopType(r)) return null;
|
|
1107
|
+
if (r.type === BWALLETX_TYPE) {
|
|
1108
|
+
return (r.substrate === "pairing" ? list.find((o) => o.kind === "pairing") : list.find(isPresentBwalletX)) ?? null;
|
|
1109
|
+
}
|
|
1110
|
+
return list.find((o) => !o.isBwalletX && !isLocalDesktop(o) && isRememberable(o) && walletTypeOf(o) === r.type) ?? null;
|
|
1111
|
+
}
|
|
1112
|
+
var ATTACHABLE = ["extension", "in-app", "xdm", "injected"];
|
|
1113
|
+
function planAutoConnect(o) {
|
|
1114
|
+
const r = o.remembered;
|
|
1115
|
+
if (!r || o.enabled === false) return { action: "none", remembered: r, needsBwalletX: false };
|
|
1116
|
+
if (r.type === METANET_TYPE) {
|
|
1117
|
+
return { action: o.metanetHere ?? metanetOffered() ? "silent-metanet" : "none", remembered: r, needsBwalletX: false };
|
|
1118
|
+
}
|
|
1119
|
+
if (isLocalDesktopType(r)) return { action: "none", remembered: r, needsBwalletX: false };
|
|
1120
|
+
const action = o.signedIn && ATTACHABLE.includes(r.substrate) ? "attach" : "wait-for-tap";
|
|
1121
|
+
return { action, remembered: r, needsBwalletX: needsBwalletX(r.type) };
|
|
1122
|
+
}
|
|
1123
|
+
async function attachRemembered(r, o = {}) {
|
|
1124
|
+
if (!ATTACHABLE.includes(r.substrate)) return null;
|
|
1125
|
+
const list = await discoverWallets({ ms: o.ms });
|
|
1126
|
+
const m = matchRemembered(list, r);
|
|
1127
|
+
return m && m.kind === "brc100" && m.wallet && ATTACHABLE.includes(substrateOf(m)) ? m : null;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// src/modal.ts
|
|
1131
|
+
import qrcode from "qrcode-generator";
|
|
1132
|
+
|
|
1133
|
+
// src/access-client.ts
|
|
1134
|
+
import { Hash, PublicKey as PublicKey2, Utils } from "@bsv/sdk";
|
|
1135
|
+
var ONESAT = [0, "onesat"];
|
|
1136
|
+
var LEGACY_ONESAT = [0, "p 1sat"];
|
|
1137
|
+
function toAccessError(e) {
|
|
1138
|
+
if (e instanceof AccessError) return e;
|
|
1139
|
+
const err = e;
|
|
1140
|
+
const msg = String(err?.message ?? e ?? "");
|
|
1141
|
+
if (err?.code === "NOT_FOUND") return new AccessError("not_installed", msg);
|
|
1142
|
+
if (/reject|denied|declin|cancel|user/i.test(msg) || err?.code === "ERR_USER_REJECTED" || err?.status === 4001) {
|
|
1143
|
+
return new AccessError("user_rejected", msg);
|
|
1144
|
+
}
|
|
1145
|
+
return new AccessError("bad_request", msg || "access failed");
|
|
1146
|
+
}
|
|
1147
|
+
function throwIfAccessError(body) {
|
|
1148
|
+
const b = body;
|
|
1149
|
+
if (b && typeof b.error === "string") throw new AccessError(b.error, b.message, { have: b.have, need: b.need });
|
|
1150
|
+
}
|
|
1151
|
+
function hash160FromP2pkh(script) {
|
|
1152
|
+
const m = /^76a914([0-9a-f]{40})88ac$/i.exec(script.slice(-50));
|
|
1153
|
+
return m ? m[1].toLowerCase() : null;
|
|
1154
|
+
}
|
|
1155
|
+
async function proveHoldings(wallet, challenge, tokenId, basket = "bsv21") {
|
|
1156
|
+
const bare = tokenId.replace(/^bsv21:/, "");
|
|
1157
|
+
const list = await wallet.listOutputs({ basket, include: "locking scripts", includeCustomInstructions: true, limit: 1e4 });
|
|
1158
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1159
|
+
const data = challengeBytes(challenge);
|
|
1160
|
+
for (const out of list.outputs) {
|
|
1161
|
+
if (byKey.size >= MAX_HOLDING_PROOFS) break;
|
|
1162
|
+
let ci = {};
|
|
1163
|
+
try {
|
|
1164
|
+
ci = JSON.parse(out.customInstructions ?? "{}");
|
|
1165
|
+
} catch {
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
if (ci.id !== bare || !ci.keyID) continue;
|
|
1169
|
+
const want = out.lockingScript ? hash160FromP2pkh(out.lockingScript) : null;
|
|
1170
|
+
const counterparty = ci.counterparty ?? "self";
|
|
1171
|
+
for (const protocolID of ci.protocolID ? [ci.protocolID] : [ONESAT, LEGACY_ONESAT]) {
|
|
1172
|
+
const { publicKey } = await wallet.getPublicKey({ protocolID, keyID: ci.keyID, counterparty });
|
|
1173
|
+
const h = Utils.toHex(Hash.hash160(PublicKey2.fromString(publicKey).encode(true)));
|
|
1174
|
+
if (want && h !== want) continue;
|
|
1175
|
+
const existing = byKey.get(publicKey);
|
|
1176
|
+
if (existing) {
|
|
1177
|
+
existing.outpoints.push(out.outpoint.replace(".", "_"));
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
1180
|
+
const { signature } = await wallet.createSignature({ protocolID, keyID: ci.keyID, counterparty, data });
|
|
1181
|
+
byKey.set(publicKey, { pubkey: publicKey, signature: Array.from(signature), outpoints: [out.outpoint.replace(".", "_")] });
|
|
1182
|
+
break;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return [...byKey.values()];
|
|
1186
|
+
}
|
|
1187
|
+
async function requestAccess(o) {
|
|
1188
|
+
let wallet;
|
|
1189
|
+
try {
|
|
1190
|
+
({ wallet } = await connectBwalletX({ wallet: o.wallet, pairing: o.pairing, discoverMs: o.discoverMs }));
|
|
1191
|
+
} catch (e) {
|
|
1192
|
+
throw e?.code === "NOT_FOUND" ? new AccessError("not_installed") : toAccessError(e);
|
|
1193
|
+
}
|
|
1194
|
+
try {
|
|
1195
|
+
const { publicKey: identityKey } = await wallet.getPublicKey({ identityKey: true });
|
|
1196
|
+
const { challenge } = await o.challenge(identityKey);
|
|
1197
|
+
const c = parseAccessChallenge(challenge);
|
|
1198
|
+
if (c.sub && c.sub !== identityKey.toLowerCase()) throw new AccessError("wrong_account");
|
|
1199
|
+
if (Date.now() / 1e3 >= c.exp) throw new AccessError("expired");
|
|
1200
|
+
const { signature } = await wallet.createSignature({
|
|
1201
|
+
protocolID: ACCESS_PROTOCOL,
|
|
1202
|
+
keyID: c.nonce,
|
|
1203
|
+
counterparty: "anyone",
|
|
1204
|
+
data: challengeBytes(challenge)
|
|
1205
|
+
});
|
|
1206
|
+
const response = { v: 1, challenge, identityKey, signature: Array.from(signature) };
|
|
1207
|
+
if (c.require.token) response.holdings = await proveHoldings(wallet, challenge, c.require.token.id, o.tokenBasket);
|
|
1208
|
+
let verified;
|
|
1209
|
+
if (o.verify) {
|
|
1210
|
+
verified = await o.verify(response);
|
|
1211
|
+
throwIfAccessError(verified);
|
|
1212
|
+
}
|
|
1213
|
+
return { response, verified, wallet };
|
|
1214
|
+
} catch (e) {
|
|
1215
|
+
throw toAccessError(e);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/connect-flow.ts
|
|
1220
|
+
async function runConnect(o) {
|
|
1221
|
+
const started = Date.now();
|
|
1222
|
+
const { list } = o;
|
|
1223
|
+
let choice = null;
|
|
1224
|
+
let reason = "none";
|
|
1225
|
+
try {
|
|
1226
|
+
if (o.metanet) {
|
|
1227
|
+
if (!metanetOffered()) throw Object.assign(new Error("Metanet Desktop runs on a computer, outside any frame."), { code: "NOT_SUPPORTED" });
|
|
1228
|
+
choice = metanetOption(typeof o.metanet === "object" ? o.metanet : {});
|
|
1229
|
+
reason = "requested";
|
|
1230
|
+
return await finish(o, choice, reason, started);
|
|
1231
|
+
}
|
|
1232
|
+
const d = decideWallet(list, {
|
|
1233
|
+
walletId: o.walletId,
|
|
1234
|
+
chooser: o.chooser,
|
|
1235
|
+
picker: o.picker,
|
|
1236
|
+
fastPath: o.fastPath,
|
|
1237
|
+
remembered: o.remembered,
|
|
1238
|
+
preferBwalletX: o.preferBwalletX
|
|
1239
|
+
});
|
|
1240
|
+
choice = d.option;
|
|
1241
|
+
reason = d.reason;
|
|
1242
|
+
if (!choice && d.reason === "chooser") choice = await o.pick(list);
|
|
1243
|
+
if (!choice) {
|
|
1244
|
+
if (d.reason === "none" || !list.length) throw Object.assign(new Error("No wallet found. Get bWalletX at bwalletx.com."), { code: "NOT_FOUND" });
|
|
1245
|
+
reportAttempt(o.onAttempt, attemptFor(null, "cancelled", started));
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
return await finish(o, choice, reason, started);
|
|
1249
|
+
} catch (e) {
|
|
1250
|
+
reportAttempt(o.onAttempt, attemptFor(choice, outcomeOf(e), started));
|
|
1251
|
+
throw e;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
async function finish(o, choice, reason, started) {
|
|
1255
|
+
const signIn = !!(o.challengeUrl && o.verifyUrl) && choice.kind !== "handcash";
|
|
1256
|
+
const cw = await choice.connect(signIn ? { identity: false } : void 0);
|
|
1257
|
+
let handle = cw.handle ?? null;
|
|
1258
|
+
let paymail = cw.paymail ?? null;
|
|
1259
|
+
let identityKey = cw.identityKey;
|
|
1260
|
+
let signedIn = false;
|
|
1261
|
+
if (cw.wallet && signIn) {
|
|
1262
|
+
const { result, verified } = await signInAndVerify({
|
|
1263
|
+
wallet: cw.wallet,
|
|
1264
|
+
identityKey: cw.identityKey ?? void 0,
|
|
1265
|
+
// Metanet's connect() just checked the lock: don't ask again (rule 7, the minimum).
|
|
1266
|
+
skipWaitForAuthentication: choice.id === METANET_OPTION_ID,
|
|
1267
|
+
// The key that signed in here last time: a repeat sign-in is then the signature alone.
|
|
1268
|
+
identityKeyHint: rememberedIdentity(),
|
|
1269
|
+
challengeNeedsIdentityKey: o.challengeNeedsIdentityKey,
|
|
1270
|
+
challenge: (key) => o.post(o.challengeUrl, key ? { identityKey: key } : {}),
|
|
1271
|
+
verify: (r) => o.post(o.verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature })
|
|
1272
|
+
});
|
|
1273
|
+
rememberIdentity(result.identityKey);
|
|
1274
|
+
identityKey = result.identityKey;
|
|
1275
|
+
handle = verified.handle ?? handle;
|
|
1276
|
+
paymail = verified.paymail ?? paymail;
|
|
1277
|
+
signedIn = true;
|
|
1278
|
+
} else if (cw.wallet && !identityKey) {
|
|
1279
|
+
identityKey = (await cw.wallet.getPublicKey({ identityKey: true })).publicKey;
|
|
1280
|
+
}
|
|
1281
|
+
if (o.remember && isExplicitPick(choice, reason)) rememberWalletChoice(choice);
|
|
1282
|
+
if (o.remember && signedIn) rememberKnownIdentityKey(identityKey);
|
|
1283
|
+
reportAttempt(o.onAttempt, attemptFor(choice, signedIn ? "signed-in" : "connected", started));
|
|
1284
|
+
return { connected: cw, identityKey, handle, paymail, signedIn, walletType: walletTypeOf(choice), substrate: substrateOf(choice), reason };
|
|
1285
|
+
}
|
|
1286
|
+
async function runSilentMetanet(o = {}) {
|
|
1287
|
+
const s = await trySilentMetanet(o);
|
|
1288
|
+
if (!s.ok || o.isAbandoned?.()) return null;
|
|
1289
|
+
const started = Date.now();
|
|
1290
|
+
const opt = metanetOption(o);
|
|
1291
|
+
opt.wallet = s.wallet;
|
|
1292
|
+
const cw = { wallet: s.wallet, payer: s.wallet, identityKey: s.identityKey, optionId: METANET_OPTION_ID, kind: "brc100" };
|
|
1293
|
+
const done = (signedIn, handle = null, paymail = null) => {
|
|
1294
|
+
rememberWalletChoice(opt);
|
|
1295
|
+
if (signedIn) rememberKnownIdentityKey(s.identityKey);
|
|
1296
|
+
reportAttempt(o.onAttempt, attemptFor(opt, signedIn ? "signed-in" : "connected", started));
|
|
1297
|
+
return { connected: cw, identityKey: s.identityKey, handle, paymail, signedIn, walletType: walletTypeOf(opt), substrate: "json-api", reason: "remembered" };
|
|
1298
|
+
};
|
|
1299
|
+
if (o.sessionKey !== void 0 && o.sessionKey !== null) {
|
|
1300
|
+
return o.sessionKey.toLowerCase() === s.identityKey.toLowerCase() ? done(true) : null;
|
|
1301
|
+
}
|
|
1302
|
+
if (!o.challengeUrl || !o.verifyUrl || !o.post) return done(false);
|
|
1303
|
+
if (!isKnownIdentityKey(s.identityKey)) return null;
|
|
1304
|
+
try {
|
|
1305
|
+
o.onSignIn?.();
|
|
1306
|
+
const post = o.post;
|
|
1307
|
+
const { result, verified } = await signInAndVerify({
|
|
1308
|
+
wallet: s.wallet,
|
|
1309
|
+
identityKey: s.identityKey,
|
|
1310
|
+
skipWaitForAuthentication: true,
|
|
1311
|
+
// trySilentMetanet just saw it unlocked
|
|
1312
|
+
challenge: (key) => post(o.challengeUrl, key ? { identityKey: key } : {}),
|
|
1313
|
+
verify: (r) => post(o.verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature })
|
|
1314
|
+
});
|
|
1315
|
+
if (o.isAbandoned?.()) {
|
|
1316
|
+
await Promise.resolve(o.logout?.()).catch(() => void 0);
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
rememberIdentity(result.identityKey);
|
|
1320
|
+
return done(true, verified.handle ?? null, verified.paymail ?? null);
|
|
1321
|
+
} catch (e) {
|
|
1322
|
+
reportAttempt(o.onAttempt, attemptFor(opt, outcomeOf(e), started));
|
|
1323
|
+
return null;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// src/element.ts
|
|
1328
|
+
var SIGNIN_TAG = "bwalletx-signin";
|
|
1329
|
+
var GET_URL = "https://bwalletx.com/get";
|
|
1330
|
+
var WEB_WALLET_URL = "https://web.bwalletx.com";
|
|
1331
|
+
var VERBS = { signin: "Sign in with", continue: "Continue with", connect: "Connect", get: "Get", play: "Play with" };
|
|
1332
|
+
function signinText(label = "signin", brand = "bWalletX") {
|
|
1333
|
+
return `${VERBS[label] ?? VERBS.signin} ${brand}`;
|
|
1334
|
+
}
|
|
1335
|
+
var uid = 0;
|
|
1336
|
+
function markSvg(px = 20) {
|
|
1337
|
+
const n = ++uid;
|
|
1338
|
+
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>`;
|
|
1339
|
+
}
|
|
1340
|
+
var SIGNIN_CSS = `
|
|
1341
|
+
:host{display:inline-block;vertical-align:middle;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
|
1342
|
+
:host([full-width]){display:block;width:100%}
|
|
1343
|
+
:host([hidden]){display:none}
|
|
1344
|
+
button{all:unset;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:12px;width:100%;cursor:pointer;
|
|
1345
|
+
padding:16px 20px;border-radius:12px;font-size:16px;font-weight:700;line-height:1.2;text-align:left;
|
|
1346
|
+
background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;
|
|
1347
|
+
box-shadow:0 10px 15px -3px rgba(245,158,11,.2),0 4px 6px -4px rgba(245,158,11,.2);
|
|
1348
|
+
transition:background .15s,transform .1s,box-shadow .15s,border-color .15s;-webkit-tap-highlight-color:transparent}
|
|
1349
|
+
button:hover{background:linear-gradient(180deg,#fde68a,#fbbf24)}
|
|
1350
|
+
button:active{transform:translateY(1px);background:linear-gradient(180deg,#fbbf24,#d97706)}
|
|
1351
|
+
button:focus-visible{outline:2px solid #b45309;outline-offset:3px}
|
|
1352
|
+
button:disabled{opacity:.6;cursor:not-allowed;transform:none}
|
|
1353
|
+
button[aria-busy=true]{cursor:wait}
|
|
1354
|
+
.mark{display:inline-flex;align-items:center;justify-content:center;flex:none;width:28px;height:28px;border-radius:50%;background:#000}
|
|
1355
|
+
.txt{display:flex;flex-direction:column;align-items:flex-start;min-width:0}
|
|
1356
|
+
.txt b{font-weight:700;white-space:nowrap}
|
|
1357
|
+
.txt small{font-size:11px;font-weight:500;color:rgba(0,0,0,.65);white-space:nowrap}
|
|
1358
|
+
.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}
|
|
1359
|
+
@keyframes r{to{transform:rotate(360deg)}}
|
|
1360
|
+
@media (prefers-reduced-motion:reduce){button{transition:none}button:active{transform:none}.spin{animation:none;border-color:#ffd24d;border-right-color:transparent}}
|
|
1361
|
+
/* sizes */
|
|
1362
|
+
:host([size=md]) button{padding:10px 16px;border-radius:10px;font-size:15px;gap:10px}
|
|
1363
|
+
:host([size=md]) .mark{width:24px;height:24px}
|
|
1364
|
+
:host([size=md]) .txt small{font-size:10.5px}
|
|
1365
|
+
:host([size=sm]) button{padding:6px 12px 6px 6px;border-radius:8px;font-size:13px;gap:8px;min-height:36px}
|
|
1366
|
+
:host([size=sm]) .mark{width:22px;height:22px}
|
|
1367
|
+
:host([size=icon]) button{width:48px;height:48px;padding:0;border-radius:50%;gap:0}
|
|
1368
|
+
:host([size=icon]) .mark{width:34px;height:34px}
|
|
1369
|
+
/* themes */
|
|
1370
|
+
:host([theme=dark]) button{background:#0a0a0a;color:#fafafa;border:1px solid #3f3f46;box-shadow:none}
|
|
1371
|
+
:host([theme=dark]) button:hover{background:#18181b;border-color:rgba(251,191,36,.6)}
|
|
1372
|
+
:host([theme=dark]) button:active{background:#000}
|
|
1373
|
+
:host([theme=dark]) button:focus-visible{outline-color:#fcd34d}
|
|
1374
|
+
:host([theme=dark]) .mark{box-shadow:inset 0 0 0 1px #3f3f46}
|
|
1375
|
+
:host([theme=dark]) .txt small{color:rgba(255,255,255,.65)}
|
|
1376
|
+
:host([theme=light]) button{background:#fff;color:#09090b;border:1px solid #d4d4d8;box-shadow:0 1px 2px rgba(0,0,0,.06)}
|
|
1377
|
+
:host([theme=light]) button:hover{background:#fafafa;border-color:#f59e0b}
|
|
1378
|
+
:host([theme=light]) button:active{background:#f4f4f5}
|
|
1379
|
+
:host([theme=light]) .txt small{color:#52525b}
|
|
1380
|
+
/* not found */
|
|
1381
|
+
.nf{margin-top:8px;font-size:12px;line-height:1.4;color:#71717a;text-align:center}
|
|
1382
|
+
.nf a{color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px}
|
|
1383
|
+
.nf a:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:2px}
|
|
1384
|
+
:host([theme=dark]) .nf,:host-context(.dark) .nf{color:#a1a1aa}
|
|
1385
|
+
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
1386
|
+
/* pending: one plain line under the button */
|
|
1387
|
+
.note{margin:8px 0 0;width:0;min-width:100%;font-size:12px;line-height:1.4;color:#71717a;text-align:center;overflow-wrap:break-word}
|
|
1388
|
+
:host([theme=dark]) .note,:host-context(.dark) .note{color:#a1a1aa}
|
|
1389
|
+
/* "Other wallets" (0.5.6): one small link under the button */
|
|
1390
|
+
.ow{margin:8px 0 0;width:0;min-width:100%;font-size:12px;line-height:1.4;color:#71717a;text-align:center}
|
|
1391
|
+
:host([theme=dark]) .ow,:host-context(.dark) .ow{color:#a1a1aa}
|
|
1392
|
+
.sw{margin:8px 0 0;width:0;min-width:100%;font-size:12px;line-height:1.4;color:#a16207;text-align:center}
|
|
1393
|
+
:host([theme=dark]) .sw,:host-context(.dark) .sw{color:#fcd34d}
|
|
1394
|
+
.mn{margin:8px 0 0;width:0;min-width:100%;font-size:12px;line-height:1.4;color:#71717a;text-align:center}
|
|
1395
|
+
.mn small{display:block;font-size:11px;opacity:.85}
|
|
1396
|
+
:host([theme=dark]) .mn,:host-context(.dark) .mn{color:#a1a1aa}
|
|
1397
|
+
.lnk{all:unset;cursor:pointer;color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px}
|
|
1398
|
+
.lnk:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:2px}
|
|
1399
|
+
/* error: one line plus Retry */
|
|
1400
|
+
.err{margin:0 0 8px;width:0;min-width:100%;font-size:13px;font-weight:600;line-height:1.4;color:#b91c1c;text-align:center;overflow-wrap:anywhere}
|
|
1401
|
+
:host([theme=dark]) .err,:host-context(.dark) .err{color:#fca5a5}
|
|
1402
|
+
/* signed in: the badge */
|
|
1403
|
+
button.chip{display:inline-flex;align-items:center;justify-content:flex-start;gap:10px;width:auto;max-width:100%;padding:5px 14px 5px 5px;border-radius:999px;
|
|
1404
|
+
background:#0b0b0c;color:#ffd24d;border:1px solid rgba(255,210,77,.55);box-shadow:none;font-size:15px}
|
|
1405
|
+
button.chip:hover{background:#151517;border-color:#ffd24d}
|
|
1406
|
+
button.chip:active{background:#000;transform:none}
|
|
1407
|
+
button.chip:focus-visible{outline:2px solid #ffd24d;outline-offset:3px}
|
|
1408
|
+
:host([theme=light]) button.chip{background:#fff;color:#09090b;border-color:#f59e0b}
|
|
1409
|
+
:host([theme=light]) button.chip:hover{background:#fffbeb}
|
|
1410
|
+
:host([full-width]) button.chip{width:100%}
|
|
1411
|
+
.av{position:relative;flex:none;display:inline-grid;place-items:center;width:30px;height:30px;border-radius:50%;overflow:hidden;
|
|
1412
|
+
background:radial-gradient(circle at 30% 30%,#ffe27a,#f5b800 55%,#b88600);color:#000;font-weight:800;font-size:14px}
|
|
1413
|
+
.av img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
|
1414
|
+
.name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:min(52vw,260px)}
|
|
1415
|
+
.dot{flex:none;width:8px;height:8px;border-radius:50%;background:#ffd24d;box-shadow:0 0 0 3px rgba(255,210,77,.22)}
|
|
1416
|
+
:host([size=sm]) button.chip{font-size:13px;padding:3px 12px 3px 3px;gap:8px}
|
|
1417
|
+
:host([size=sm]) .av{width:26px;height:26px}
|
|
1418
|
+
:host([size=icon]) button.chip{width:48px;height:48px;padding:0;justify-content:center}
|
|
1419
|
+
:host([size=icon]) .name,:host([size=icon]) .dot{display:none}
|
|
1420
|
+
:host([size=icon]) .av{width:40px;height:40px}
|
|
1421
|
+
`;
|
|
1422
|
+
var MENU_CSS = `
|
|
1423
|
+
:host{all:initial}
|
|
1424
|
+
.menu{position:fixed;z-index:2147483600;box-sizing:border-box;min-width:200px;padding:6px;border-radius:14px;background:#0b0b0c;color:#ffd24d;
|
|
1425
|
+
border:1px solid rgba(255,210,77,.55);box-shadow:0 12px 32px rgba(0,0,0,.45);font:600 14px/1.3 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
|
1426
|
+
.who{padding:8px 10px 10px;font-size:12px;font-weight:500;color:#e7d9a8;overflow-wrap:anywhere;border-bottom:1px solid rgba(255,210,77,.3);margin-bottom:4px}
|
|
1427
|
+
.item{all:unset;box-sizing:border-box;display:block;width:100%;padding:11px 10px;border-radius:10px;cursor:pointer;color:#ffd24d}
|
|
1428
|
+
.item:hover,.item:focus-visible{background:#ffd24d;color:#000;outline:none}
|
|
1429
|
+
.tip{position:fixed;z-index:2147483600;box-sizing:border-box;padding:8px 12px;border-radius:10px;background:#0b0b0c;color:#e7d9a8;
|
|
1430
|
+
border:1px solid rgba(255,210,77,.4);box-shadow:0 8px 24px rgba(0,0,0,.35);font:500 12px/1.4 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
|
1431
|
+
`;
|
|
1432
|
+
var PENDING_NOTE = "Sign in: shares only your public identity key, moves no money";
|
|
1433
|
+
var PENDING_LABEL = "Approve in bWalletX\u2026";
|
|
1434
|
+
var PENDING_LABEL_OTHER = "Approve in your wallet\u2026";
|
|
1435
|
+
var SWITCH_NOTE = "Some features need bWalletX";
|
|
1436
|
+
var CONTINUE_OTHER_LABEL = "Continue with your wallet";
|
|
1437
|
+
function walletGlyph(px) {
|
|
1438
|
+
return `<svg viewBox="0 0 24 24" width="${px}" height="${px}" aria-hidden="true" focusable="false" fill="none" stroke="#FFD24D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 7V5a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2"/><path d="M3 6v12a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-3"/></svg>`;
|
|
1439
|
+
}
|
|
1440
|
+
function b64url2(bytes) {
|
|
1441
|
+
let s = "";
|
|
1442
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
1443
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1444
|
+
}
|
|
1445
|
+
async function buildAuthorizeUrl(p) {
|
|
1446
|
+
const rnd = (n) => b64url2(crypto.getRandomValues(new Uint8Array(n)));
|
|
1447
|
+
const verifier = rnd(32);
|
|
1448
|
+
const state = rnd(16);
|
|
1449
|
+
const nonce = rnd(16);
|
|
1450
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)));
|
|
1451
|
+
const u = new URL(p.authorizeUrl, typeof location !== "undefined" ? location.href : void 0);
|
|
1452
|
+
const set = (k, v) => {
|
|
1453
|
+
if (v) u.searchParams.set(k, v);
|
|
1454
|
+
};
|
|
1455
|
+
set("response_type", "code");
|
|
1456
|
+
set("client_id", p.clientId);
|
|
1457
|
+
set("redirect_uri", p.redirectUri);
|
|
1458
|
+
set("scope", p.scope || "openid profile");
|
|
1459
|
+
set("state", state);
|
|
1460
|
+
set("nonce", nonce);
|
|
1461
|
+
set("code_challenge", b64url2(digest));
|
|
1462
|
+
set("code_challenge_method", "S256");
|
|
1463
|
+
try {
|
|
1464
|
+
sessionStorage.setItem("bwalletx:oauth", JSON.stringify({ verifier, state, nonce }));
|
|
1465
|
+
} catch {
|
|
1466
|
+
}
|
|
1467
|
+
return u.toString();
|
|
1468
|
+
}
|
|
1469
|
+
async function postJson(url, body) {
|
|
1470
|
+
const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
1471
|
+
const data = await res.json().catch(() => ({}));
|
|
1472
|
+
if (!res.ok) throw Object.assign(new Error(data.error ?? `HTTP ${res.status}`), { status: res.status });
|
|
1473
|
+
return data;
|
|
1474
|
+
}
|
|
1475
|
+
async function postAccess(url, body) {
|
|
1476
|
+
const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
1477
|
+
return res.json().catch(() => ({ error: "bad_request", message: `HTTP ${res.status}` }));
|
|
1478
|
+
}
|
|
1479
|
+
var OBSERVED = ["size", "theme", "label", "brand", "subtitle", "loading", "disabled", "full-width", "aria-label", "get-url", "web-url", "state", "identity-key", "paymail", "handle", "avatar-url", "error", "other-wallets", "challenge-url", "wallet-type", "switch-note", "remember", "metanet"];
|
|
1480
|
+
var STATES = ["signed-out", "restoring", "pending", "signed-in", "error"];
|
|
1481
|
+
function createClass() {
|
|
1482
|
+
return class BwalletxSigninElement extends HTMLElement {
|
|
1483
|
+
static get observedAttributes() {
|
|
1484
|
+
return OBSERVED;
|
|
1485
|
+
}
|
|
1486
|
+
#root;
|
|
1487
|
+
#notFound = false;
|
|
1488
|
+
#busy = false;
|
|
1489
|
+
#phase = "signed-out";
|
|
1490
|
+
#account = null;
|
|
1491
|
+
#error = "";
|
|
1492
|
+
#restored = false;
|
|
1493
|
+
#pendingLabel = PENDING_LABEL;
|
|
1494
|
+
/** The wallet type signed in with here (0.6.0), for the Switch note. */
|
|
1495
|
+
#signedType = null;
|
|
1496
|
+
#menu = null;
|
|
1497
|
+
/** (0.7.0) A Metanet sign-in (tapped or silent) is in flight. */
|
|
1498
|
+
#viaMetanet = false;
|
|
1499
|
+
/** (0.7.0) Bumped by "Not you?": a Metanet sign-in started before it lands nobody. */
|
|
1500
|
+
#run = 0;
|
|
1501
|
+
constructor() {
|
|
1502
|
+
super();
|
|
1503
|
+
this.#root = this.attachShadow({ mode: "open" });
|
|
1504
|
+
}
|
|
1505
|
+
connectedCallback() {
|
|
1506
|
+
if (!this.#controlled && this.#stateful) warnIfNoLogout({ verifyUrl: this.#attr("verify-url"), logoutUrl: this.#attr("logout-url"), where: "<bwalletx-signin>" });
|
|
1507
|
+
this.#render();
|
|
1508
|
+
void this.#restore().then(() => this.#silentMetanet());
|
|
1509
|
+
}
|
|
1510
|
+
disconnectedCallback() {
|
|
1511
|
+
this.#menu?.close(false);
|
|
1512
|
+
this.#hideTip();
|
|
1513
|
+
}
|
|
1514
|
+
attributeChangedCallback() {
|
|
1515
|
+
if (this.isConnected) this.#render();
|
|
1516
|
+
}
|
|
1517
|
+
/** The inner <button>, for focus() and tests. */
|
|
1518
|
+
get button() {
|
|
1519
|
+
return this.#root.querySelector("button");
|
|
1520
|
+
}
|
|
1521
|
+
get notFound() {
|
|
1522
|
+
return this.#notFound;
|
|
1523
|
+
}
|
|
1524
|
+
/** The state shown now (the `state` attribute when set, else the element's own). */
|
|
1525
|
+
get state() {
|
|
1526
|
+
const a = this.#attr("state");
|
|
1527
|
+
return a && STATES.includes(a) ? a : this.#phase;
|
|
1528
|
+
}
|
|
1529
|
+
/** The signed-in account, or null. */
|
|
1530
|
+
get account() {
|
|
1531
|
+
if (!this.#controlled) return this.#account;
|
|
1532
|
+
const identityKey = this.#attr("identity-key");
|
|
1533
|
+
return identityKey ? { identityKey, paymail: this.#attr("paymail"), handle: this.#attr("handle"), avatarUrl: this.#attr("avatar-url") } : null;
|
|
1534
|
+
}
|
|
1535
|
+
/** True while the menu is open (tests). */
|
|
1536
|
+
get menuOpen() {
|
|
1537
|
+
return !!this.#menu;
|
|
1538
|
+
}
|
|
1539
|
+
focus(opts) {
|
|
1540
|
+
this.button?.focus(opts);
|
|
1541
|
+
}
|
|
1542
|
+
get #controlled() {
|
|
1543
|
+
return this.hasAttribute("state");
|
|
1544
|
+
}
|
|
1545
|
+
get #stateful() {
|
|
1546
|
+
return !this.hasAttribute("stateless");
|
|
1547
|
+
}
|
|
1548
|
+
#attr(name) {
|
|
1549
|
+
return this.getAttribute(name);
|
|
1550
|
+
}
|
|
1551
|
+
/** Restore the badge from session-url once, with no wallet call. */
|
|
1552
|
+
async #restore() {
|
|
1553
|
+
const url = this.#attr("session-url");
|
|
1554
|
+
if (this.#restored || !url || this.#controlled || !this.#stateful) return;
|
|
1555
|
+
this.#restored = true;
|
|
1556
|
+
this.#set("restoring");
|
|
1557
|
+
try {
|
|
1558
|
+
const r = await fetch(url, { credentials: "include", headers: { accept: "application/json" } });
|
|
1559
|
+
const j = await r.json().catch(() => ({}));
|
|
1560
|
+
if (this.#phase !== "restoring") return;
|
|
1561
|
+
if (j.identityKey) this.#signedIn({ identityKey: j.identityKey, handle: j.handle ?? null, paymail: j.paymail ?? null });
|
|
1562
|
+
else this.#set("signed-out");
|
|
1563
|
+
} catch {
|
|
1564
|
+
if (this.#phase === "restoring") this.#set("signed-out");
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
#set(p) {
|
|
1568
|
+
this.#phase = p;
|
|
1569
|
+
if (p !== "signed-in") this.#menu?.close(false);
|
|
1570
|
+
this.#render();
|
|
1571
|
+
}
|
|
1572
|
+
#signedIn(a) {
|
|
1573
|
+
this.#account = a;
|
|
1574
|
+
this.#error = "";
|
|
1575
|
+
this.#set("signed-in");
|
|
1576
|
+
const lookup = this.#attr("lookup-url");
|
|
1577
|
+
if (!a.paymail && lookup !== "off") {
|
|
1578
|
+
void lookupPaymail(a.identityKey, lookup ?? void 0).then((pm) => {
|
|
1579
|
+
if (pm && this.#account?.identityKey === a.identityKey && !this.#account.paymail) {
|
|
1580
|
+
this.#account = { ...this.#account, paymail: pm };
|
|
1581
|
+
this.#render();
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
#render() {
|
|
1587
|
+
const size = this.#attr("size") ?? "lg";
|
|
1588
|
+
const brand = this.#attr("brand") === "bWallet" ? "bWallet" : "bWalletX";
|
|
1589
|
+
const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
1590
|
+
const state = this.state;
|
|
1591
|
+
const acct = this.account;
|
|
1592
|
+
if (state === "signed-in" && acct) {
|
|
1593
|
+
this.#hideTip();
|
|
1594
|
+
const name = accountLabel(acct);
|
|
1595
|
+
const avatar = acct.avatarUrl ?? avatarUrlFor(acct.identityKey);
|
|
1596
|
+
const initial = (name.replace(/^\$/, "").charAt(0) || "b").toUpperCase();
|
|
1597
|
+
this.#root.innerHTML = `<style>${SIGNIN_CSS}</style><button type="button" class="chip" part="button chip" aria-haspopup="menu" aria-expanded="${this.#menu ? "true" : "false"}" aria-label="Signed in with bWalletX as ${esc(name)}. Account menu" title="Signed in as ${esc(name)}"><span class="av" part="avatar" aria-hidden="true">${esc(initial)}${avatar ? `<img alt="" src="${esc(avatar)}" referrerpolicy="no-referrer">` : ""}</span><span class="name" part="label">${esc(name)}</span><span class="dot" part="dot" aria-hidden="true"></span></button>`;
|
|
1598
|
+
const img = this.#root.querySelector("img");
|
|
1599
|
+
if (img) img.addEventListener("error", () => img.remove(), { once: true });
|
|
1600
|
+
const chip = this.button;
|
|
1601
|
+
chip.addEventListener("click", () => this.#menu ? this.#menu.close(true) : this.#openMenu());
|
|
1602
|
+
chip.addEventListener("keydown", (e) => {
|
|
1603
|
+
if ((e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") && !this.#menu) {
|
|
1604
|
+
e.preventDefault();
|
|
1605
|
+
this.#openMenu();
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
const pending = state === "pending";
|
|
1611
|
+
const restoring = state === "restoring";
|
|
1612
|
+
const error = state === "error";
|
|
1613
|
+
const pendingLabel = this.#controlled ? PENDING_LABEL : this.#pendingLabel;
|
|
1614
|
+
const elsewhere = state === "signed-out" && tapGoesElsewhere(this.#walletType());
|
|
1615
|
+
const text = pending ? pendingLabel : error ? "Retry" : elsewhere ? CONTINUE_OTHER_LABEL : signinText(this.#attr("label") ?? "signin", brand);
|
|
1616
|
+
const subAttr = this.#attr("subtitle");
|
|
1617
|
+
const subDefault = brand === "bWalletX" && (size === "lg" || size === "md") ? "also works with bWallet" : null;
|
|
1618
|
+
const subtitle = pending || error || elsewhere ? null : subAttr === null || subAttr === "" || subAttr === "on" ? subAttr === "on" ? "also works with bWallet" : subDefault : subAttr === "off" ? null : subAttr;
|
|
1619
|
+
const loading = this.hasAttribute("loading") || this.#busy || pending || restoring;
|
|
1620
|
+
const disabled = this.hasAttribute("disabled");
|
|
1621
|
+
const markPx = size === "icon" ? 24 : size === "sm" ? 15 : size === "md" ? 17 : 20;
|
|
1622
|
+
const aria = pending ? pendingLabel : this.#attr("aria-label") ?? (subtitle && size !== "icon" ? `${text}, ${subtitle}` : text);
|
|
1623
|
+
const getUrl = this.#attr("get-url") ?? GET_URL;
|
|
1624
|
+
const webUrl = this.#attr("web-url") ?? WEB_WALLET_URL;
|
|
1625
|
+
const errText = error ? (this.#controlled ? this.#attr("error") : this.#error) || "Sign-in failed" : "";
|
|
1626
|
+
const label = size === "icon" ? "" : `<span class="txt" part="text"><b part="label">${esc(text)}</b>${subtitle ? `<small part="subtitle">${esc(subtitle)}</small>` : ""}</span>`;
|
|
1627
|
+
const mark = loading ? '<span class="spin" aria-hidden="true"></span>' : elsewhere ? walletGlyph(markPx - 4) : markSvg(markPx);
|
|
1628
|
+
const otherOff = this.#attr("other-wallets") === "off";
|
|
1629
|
+
const otherLink = '<button type="button" class="lnk" data-act="other" part="other-wallets-link">Other wallets</button>';
|
|
1630
|
+
const nf = this.#notFound ? `<p class="nf" part="not-found" role="status">bWalletX wasn't found in this browser. <a href="${esc(getUrl)}" target="_blank" rel="noopener">Get ${brand}</a> or <a href="${esc(webUrl)}" target="_blank" rel="noopener">use the web wallet</a>.${otherOff ? "" : ` ${otherLink}`}</p>` : "";
|
|
1631
|
+
const ow = !this.#notFound && !pending && !restoring && this.#otherWalletsShown(size) ? `<p class="ow" part="other-wallets">${otherLink}</p>` : "";
|
|
1632
|
+
const sw = !pending && !restoring && this.#switchNoteShown(size) ? `<p class="sw" part="switch-note">${esc(SWITCH_NOTE)} \xB7 <button type="button" class="lnk" data-act="switch" part="switch-link">Switch</button></p>` : "";
|
|
1633
|
+
const err = error ? `<p class="err" part="error" role="alert">${esc(errText)}</p>` : "";
|
|
1634
|
+
const mnHere = this.#metanetShown(size);
|
|
1635
|
+
const mnRemembered = mnHere && (pending ? this.#metanetPending() : !restoring && this.#walletType() === METANET_TYPE);
|
|
1636
|
+
const notYou = `<button type="button" class="lnk" data-act="not-you" part="not-you-link">${esc(NOT_YOU_LABEL)}</button>`;
|
|
1637
|
+
const mn = !mnHere ? "" : pending ? mnRemembered ? `<p class="mn" part="metanet">${esc(METANET_REMEMBERED_NOTE)} ${notYou}</p>` : "" : restoring ? "" : `<p class="mn" part="metanet"><button type="button" class="lnk" data-act="metanet" part="metanet-link" title="Connects to Metanet Desktop on this computer, never to a browser extension.">${esc(METANET_LABEL)}</button><small>${esc(METANET_NOTE)}</small>${mnRemembered ? `<small>${esc(METANET_REMEMBERED_NOTE)} ${notYou}</small>` : ""}</p>`;
|
|
1638
|
+
const compact = size === "sm" || size === "icon";
|
|
1639
|
+
const note = pending && !compact ? `<p class="note" part="note">${esc(PENDING_NOTE)}</p>` : "";
|
|
1640
|
+
const status = pending ? `<span class="sr" role="status">${esc(pendingLabel)}</span>` : loading ? '<span class="sr" role="status">Connecting to bWalletX\u2026</span>' : "";
|
|
1641
|
+
this.#root.innerHTML = `<style>${SIGNIN_CSS}</style>${err}<button type="button" part="button" aria-label="${esc(aria)}" title="${esc(subtitle ? `${text} (${subtitle})` : text)}"${disabled ? " disabled" : ""}${loading ? ' aria-busy="true"' : ""}><span class="mark" part="mark">${mark}</span>${label}</button>${status}${note}${sw}${mn}${ow}${nf}`;
|
|
1642
|
+
this.button.addEventListener("click", () => void this.#click());
|
|
1643
|
+
for (const l of Array.from(this.#root.querySelectorAll("[data-act=other]"))) l.addEventListener("click", () => void this.otherWallets());
|
|
1644
|
+
this.#root.querySelector("[data-act=switch]")?.addEventListener("click", () => void this.switchToBwalletX());
|
|
1645
|
+
this.#root.querySelector("[data-act=metanet]")?.addEventListener("click", () => void this.useMetanet());
|
|
1646
|
+
this.#root.querySelector("[data-act=not-you]")?.addEventListener("click", () => this.notYou());
|
|
1647
|
+
bindPrewarm(this.button);
|
|
1648
|
+
if (pending && compact) this.#showTip(PENDING_NOTE);
|
|
1649
|
+
else this.#hideTip();
|
|
1650
|
+
}
|
|
1651
|
+
#tip = null;
|
|
1652
|
+
#showTip(text) {
|
|
1653
|
+
const b = this.button;
|
|
1654
|
+
if (!b || typeof document === "undefined") return;
|
|
1655
|
+
if (!this.#tip) {
|
|
1656
|
+
const host = document.createElement("div");
|
|
1657
|
+
host.setAttribute("data-bwalletx-tip", "");
|
|
1658
|
+
const root = host.attachShadow({ mode: "open" });
|
|
1659
|
+
root.innerHTML = `<style>${MENU_CSS}</style><div class="tip" role="note"></div>`;
|
|
1660
|
+
document.body.appendChild(host);
|
|
1661
|
+
this.#tip = host;
|
|
1662
|
+
}
|
|
1663
|
+
const tip = this.#tip.shadowRoot.querySelector(".tip");
|
|
1664
|
+
tip.textContent = text;
|
|
1665
|
+
const r = b.getBoundingClientRect();
|
|
1666
|
+
const vw = window.innerWidth || document.documentElement.clientWidth || 360;
|
|
1667
|
+
const width = Math.min(280, vw - 32);
|
|
1668
|
+
tip.style.width = `${width}px`;
|
|
1669
|
+
tip.style.left = `${Math.max(16, Math.min(r.right - width, vw - width - 16))}px`;
|
|
1670
|
+
tip.style.top = `${r.bottom + 8}px`;
|
|
1671
|
+
}
|
|
1672
|
+
#hideTip() {
|
|
1673
|
+
this.#tip?.remove();
|
|
1674
|
+
this.#tip = null;
|
|
1675
|
+
}
|
|
1676
|
+
#openMenu() {
|
|
1677
|
+
const acct = this.account;
|
|
1678
|
+
const chip = this.button;
|
|
1679
|
+
if (!acct || !chip || typeof document === "undefined") return;
|
|
1680
|
+
const name = accountLabel(acct);
|
|
1681
|
+
const host = document.createElement("div");
|
|
1682
|
+
host.setAttribute("data-bwalletx-menu", "");
|
|
1683
|
+
const root = host.attachShadow({ mode: "open" });
|
|
1684
|
+
const style = document.createElement("style");
|
|
1685
|
+
style.textContent = MENU_CSS;
|
|
1686
|
+
const menu = document.createElement("div");
|
|
1687
|
+
menu.className = "menu";
|
|
1688
|
+
menu.setAttribute("role", "menu");
|
|
1689
|
+
menu.setAttribute("aria-label", "bWalletX account");
|
|
1690
|
+
const who = document.createElement("div");
|
|
1691
|
+
who.className = "who";
|
|
1692
|
+
who.textContent = `Signed in as ${acct.paymail || name}`;
|
|
1693
|
+
const item = (text, run) => {
|
|
1694
|
+
const b = document.createElement("button");
|
|
1695
|
+
b.type = "button";
|
|
1696
|
+
b.className = "item";
|
|
1697
|
+
b.setAttribute("role", "menuitem");
|
|
1698
|
+
b.tabIndex = -1;
|
|
1699
|
+
b.textContent = text;
|
|
1700
|
+
b.addEventListener("click", () => run(b));
|
|
1701
|
+
return b;
|
|
1702
|
+
};
|
|
1703
|
+
const copyWhat = acct.paymail ? "paymail" : "key";
|
|
1704
|
+
const copy = item(`Copy ${copyWhat}`, (b) => {
|
|
1705
|
+
void copyText(acct.paymail || acct.identityKey).then((ok) => {
|
|
1706
|
+
b.textContent = ok ? "Copied" : `Copy ${copyWhat}`;
|
|
1707
|
+
setTimeout(() => close(true), ok ? 600 : 0);
|
|
1708
|
+
});
|
|
1709
|
+
});
|
|
1710
|
+
const out = item("Disconnect", () => {
|
|
1711
|
+
close(false);
|
|
1712
|
+
void this.signOut();
|
|
1713
|
+
});
|
|
1714
|
+
const items = [copy, out];
|
|
1715
|
+
menu.append(who);
|
|
1716
|
+
if (needsBwalletX(this.#walletType())) {
|
|
1717
|
+
const sw = item("Switch to bWalletX", () => {
|
|
1718
|
+
close(false);
|
|
1719
|
+
void this.switchToBwalletX();
|
|
1720
|
+
});
|
|
1721
|
+
const line = document.createElement("div");
|
|
1722
|
+
line.className = "who";
|
|
1723
|
+
line.textContent = SWITCH_NOTE;
|
|
1724
|
+
menu.append(line, sw);
|
|
1725
|
+
items.unshift(sw);
|
|
1726
|
+
}
|
|
1727
|
+
menu.append(copy, out);
|
|
1728
|
+
root.append(style, menu);
|
|
1729
|
+
document.body.appendChild(host);
|
|
1730
|
+
const place = () => {
|
|
1731
|
+
const r = chip.getBoundingClientRect();
|
|
1732
|
+
const vw = window.innerWidth || document.documentElement.clientWidth || 360;
|
|
1733
|
+
const vh = window.innerHeight || document.documentElement.clientHeight || 640;
|
|
1734
|
+
const width = Math.min(260, vw - 32);
|
|
1735
|
+
menu.style.width = `${width}px`;
|
|
1736
|
+
const left = Math.max(16, Math.min(r.right - width, vw - width - 16));
|
|
1737
|
+
menu.style.left = `${left}px`;
|
|
1738
|
+
const h = menu.offsetHeight || 150;
|
|
1739
|
+
const below = r.bottom + 8;
|
|
1740
|
+
menu.style.top = `${below + h > vh && r.top - 8 - h > 0 ? r.top - 8 - h : below}px`;
|
|
1741
|
+
};
|
|
1742
|
+
place();
|
|
1743
|
+
const focusAt = (i) => items[(i + items.length) % items.length].focus();
|
|
1744
|
+
const onKey = (e) => {
|
|
1745
|
+
const i = items.indexOf(root.activeElement);
|
|
1746
|
+
if (e.key === "Escape") {
|
|
1747
|
+
e.preventDefault();
|
|
1748
|
+
close(true);
|
|
1749
|
+
} else if (e.key === "ArrowDown") {
|
|
1750
|
+
e.preventDefault();
|
|
1751
|
+
focusAt(i + 1);
|
|
1752
|
+
} else if (e.key === "ArrowUp") {
|
|
1753
|
+
e.preventDefault();
|
|
1754
|
+
focusAt(i - 1);
|
|
1755
|
+
} else if (e.key === "Home") {
|
|
1756
|
+
e.preventDefault();
|
|
1757
|
+
focusAt(0);
|
|
1758
|
+
} else if (e.key === "End") {
|
|
1759
|
+
e.preventDefault();
|
|
1760
|
+
focusAt(items.length - 1);
|
|
1761
|
+
} else if (e.key === "Tab") close(false);
|
|
1762
|
+
};
|
|
1763
|
+
const onDown = (e) => {
|
|
1764
|
+
const path = e.composedPath?.() ?? [];
|
|
1765
|
+
if (!path.includes(host) && !path.includes(this)) close(false);
|
|
1766
|
+
};
|
|
1767
|
+
const onMove = () => close(false);
|
|
1768
|
+
menu.addEventListener("keydown", onKey);
|
|
1769
|
+
document.addEventListener("pointerdown", onDown, true);
|
|
1770
|
+
window.addEventListener("resize", onMove);
|
|
1771
|
+
window.addEventListener("scroll", place, true);
|
|
1772
|
+
const close = (focusChip = false) => {
|
|
1773
|
+
if (this.#menu?.host !== host) return;
|
|
1774
|
+
this.#menu = null;
|
|
1775
|
+
menu.removeEventListener("keydown", onKey);
|
|
1776
|
+
document.removeEventListener("pointerdown", onDown, true);
|
|
1777
|
+
window.removeEventListener("resize", onMove);
|
|
1778
|
+
window.removeEventListener("scroll", place, true);
|
|
1779
|
+
host.remove();
|
|
1780
|
+
this.button?.setAttribute("aria-expanded", "false");
|
|
1781
|
+
if (focusChip) this.button?.focus();
|
|
1782
|
+
};
|
|
1783
|
+
this.#menu = { host, close };
|
|
1784
|
+
chip.setAttribute("aria-expanded", "true");
|
|
1785
|
+
focusAt(0);
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Disconnect: emits bwalletx-disconnect (cancelable). Unless a listener cancels it (BwalletXProvider signs out
|
|
1789
|
+
* itself) or the element is driven by `state`, posts logout-url and shows signed out.
|
|
1790
|
+
*/
|
|
1791
|
+
async signOut() {
|
|
1792
|
+
this.#menu?.close(false);
|
|
1793
|
+
if (!this.#emit("bwalletx-disconnect", { element: this, account: this.account }, true)) return;
|
|
1794
|
+
if (this.#controlled) return;
|
|
1795
|
+
const url = this.#attr("logout-url");
|
|
1796
|
+
if (url) await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: "{}" }).catch(() => void 0);
|
|
1797
|
+
if (this.#remember) forgetWalletChoice();
|
|
1798
|
+
this.#signedType = null;
|
|
1799
|
+
this.#account = null;
|
|
1800
|
+
this.#set("signed-out");
|
|
1801
|
+
this.#emit("bwalletx-signed-out");
|
|
1802
|
+
this.button?.focus();
|
|
1803
|
+
}
|
|
1804
|
+
#emit(type, detail, cancelable = false) {
|
|
1805
|
+
return this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable }));
|
|
1806
|
+
}
|
|
1807
|
+
/** "Other wallets" is offered under the button: other-wallets on, or auto at lg/md when this element signs in. */
|
|
1808
|
+
#otherWalletsShown(size) {
|
|
1809
|
+
const a = this.#attr("other-wallets");
|
|
1810
|
+
if (a === "off") return false;
|
|
1811
|
+
if (a === "on") return true;
|
|
1812
|
+
return (size === "lg" || size === "md") && (this.#controlled || !!this.#attr("challenge-url"));
|
|
1813
|
+
}
|
|
1814
|
+
/** Open the wallet chooser (the "Other wallets" link): phone QR, other browser wallets, desktop wallets. */
|
|
1815
|
+
otherWallets() {
|
|
1816
|
+
return this.#click("chooser");
|
|
1817
|
+
}
|
|
1818
|
+
/** (0.6.0) "Switch": sign in with bWalletX (its sheet when here, else the chooser with Get bWalletX first). */
|
|
1819
|
+
switchToBwalletX() {
|
|
1820
|
+
return this.#click("bwalletx");
|
|
1821
|
+
}
|
|
1822
|
+
get #remember() {
|
|
1823
|
+
return this.#attr("remember") !== "off";
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* (0.7.0) "Use Metanet Desktop or another BRC-100 wallet" is offered: metanet on, or auto at lg/md when this
|
|
1827
|
+
* element signs in or follows a provider. Never on a phone or inside a frame.
|
|
1828
|
+
*/
|
|
1829
|
+
#metanetShown(size) {
|
|
1830
|
+
const a = this.#attr("metanet");
|
|
1831
|
+
if (a === "off" || !metanetOffered()) return false;
|
|
1832
|
+
if (a === "on") return true;
|
|
1833
|
+
return (size === "lg" || size === "md") && (this.#controlled || !!this.#attr("challenge-url"));
|
|
1834
|
+
}
|
|
1835
|
+
/** A Metanet sign-in is on screen: our own, or (driven) the provider's with Metanet remembered. */
|
|
1836
|
+
#metanetPending() {
|
|
1837
|
+
return this.#controlled ? this.#attr("wallet-type") === METANET_TYPE : this.#viaMetanet;
|
|
1838
|
+
}
|
|
1839
|
+
/** (0.7.0) The Metanet link: connect to Metanet Desktop on localhost:3321 (after this tap, never before). */
|
|
1840
|
+
useMetanet() {
|
|
1841
|
+
return this.#click("metanet");
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* (0.7.0) "Not you? Use another wallet": forget the remembered wallet here, and drop a Metanet sign-in in flight
|
|
1845
|
+
* (if its verify already set a session, logout-url ends it). Emits bwalletx-not-you (cancelable).
|
|
1846
|
+
*/
|
|
1847
|
+
notYou() {
|
|
1848
|
+
if (!this.#emit("bwalletx-not-you", { element: this }, true)) return;
|
|
1849
|
+
this.#run++;
|
|
1850
|
+
if (this.#remember) forgetWalletChoice();
|
|
1851
|
+
this.#signedType = null;
|
|
1852
|
+
if (this.#viaMetanet && !this.#controlled) {
|
|
1853
|
+
this.#viaMetanet = false;
|
|
1854
|
+
this.#busy = false;
|
|
1855
|
+
if (this.#phase === "pending") this.#phase = "signed-out";
|
|
1856
|
+
}
|
|
1857
|
+
this.#render();
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* (0.7.0) Rule 4: back with Metanet remembered and no session, ONE silent attempt per page load (2 s, a known
|
|
1861
|
+
* identity key only), then the signature alone. Any stop or failure is silent: the normal button stays.
|
|
1862
|
+
*/
|
|
1863
|
+
async #silentMetanet() {
|
|
1864
|
+
if (this.#controlled || !this.#stateful || !this.#remember || this.#attr("require-token")) return;
|
|
1865
|
+
if (!this.#metanetShown(this.#attr("size") ?? "lg")) return;
|
|
1866
|
+
const challengeUrl = this.#attr("challenge-url");
|
|
1867
|
+
const verifyUrl = this.#attr("verify-url");
|
|
1868
|
+
if (!challengeUrl || !verifyUrl || this.#phase !== "signed-out" || this.#busy || silentAttemptUsed()) return;
|
|
1869
|
+
if (rememberedWalletChoice()?.type !== METANET_TYPE || !metanetOffered()) return;
|
|
1870
|
+
const run = ++this.#run;
|
|
1871
|
+
const live = () => run === this.#run;
|
|
1872
|
+
const started = Date.now();
|
|
1873
|
+
const r = await runSilentMetanet({
|
|
1874
|
+
challengeUrl,
|
|
1875
|
+
verifyUrl,
|
|
1876
|
+
post: (url, body) => postJson(url, body),
|
|
1877
|
+
isAbandoned: () => !live(),
|
|
1878
|
+
logout: async () => {
|
|
1879
|
+
const out = this.#attr("logout-url");
|
|
1880
|
+
if (out) await fetch(out, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: "{}" }).catch(() => void 0);
|
|
1881
|
+
},
|
|
1882
|
+
onSignIn: () => {
|
|
1883
|
+
if (!live() || this.#phase !== "signed-out" || this.#busy) return;
|
|
1884
|
+
this.#viaMetanet = true;
|
|
1885
|
+
this.#busy = true;
|
|
1886
|
+
this.#pendingLabel = PENDING_LABEL_OTHER;
|
|
1887
|
+
this.#set("pending");
|
|
1888
|
+
},
|
|
1889
|
+
onAttempt: (a) => this.#emit("bwalletx-attempt", a)
|
|
1890
|
+
});
|
|
1891
|
+
if (!live()) return;
|
|
1892
|
+
this.#viaMetanet = false;
|
|
1893
|
+
this.#busy = false;
|
|
1894
|
+
if (!r) {
|
|
1895
|
+
if (this.#phase === "pending") this.#set("signed-out");
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
this.#signedType = r.walletType;
|
|
1899
|
+
this.#signedIn({ identityKey: r.identityKey, handle: r.handle, paymail: r.paymail });
|
|
1900
|
+
this.#emit("bwalletx-signed-in", { identityKey: r.identityKey, method: "wallet", nonce: null, signature: null, response: { handle: r.handle, paymail: r.paymail }, silent: true, ms: Date.now() - started });
|
|
1901
|
+
}
|
|
1902
|
+
/** The wallet type the Switch note judges: the provider's, else what signed in here, else the remembered one. */
|
|
1903
|
+
#walletType() {
|
|
1904
|
+
if (this.#controlled) return this.#attr("wallet-type");
|
|
1905
|
+
return this.#signedType ?? (this.#remember ? rememberedWalletChoice()?.type ?? null : null);
|
|
1906
|
+
}
|
|
1907
|
+
#switchNoteShown(size) {
|
|
1908
|
+
const a = this.#attr("switch-note");
|
|
1909
|
+
if (a === "off" || !tapGoesElsewhere(this.#walletType())) return false;
|
|
1910
|
+
return a === "on" || size === "lg" || size === "md";
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Signed in: note the wallet type, and remember it for this origin when the user picked it (0.6.0), plus the
|
|
1914
|
+
* identity key as known here (0.7.0, public keys only: Metanet's silent attempt goes on only for these). Pass a key
|
|
1915
|
+
* only after the site's verify route accepted it.
|
|
1916
|
+
*/
|
|
1917
|
+
#picked(option, reason, identityKey) {
|
|
1918
|
+
this.#signedType = walletTypeOf(option);
|
|
1919
|
+
if (this.#remember && isExplicitPick(option, reason)) rememberWalletChoice(option);
|
|
1920
|
+
if (this.#remember) rememberKnownIdentityKey(identityKey);
|
|
1921
|
+
}
|
|
1922
|
+
#attempt(option, outcome, started) {
|
|
1923
|
+
this.#emit("bwalletx-attempt", attemptFor(option, outcome, started));
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Which wallet: bWalletX by its own marker goes straight to its sheet; otherwise the chooser ("Get bWalletX" first)
|
|
1927
|
+
* when another wallet is here or the user asked; otherwise 'not-found'. null: the chooser was closed.
|
|
1928
|
+
*/
|
|
1929
|
+
async #choose(mode, ms, pairing) {
|
|
1930
|
+
if (mode === "metanet") return { option: metanetOption(), reason: "requested" };
|
|
1931
|
+
const r = mode === "signin" && this.#remember ? rememberedWalletChoice() : null;
|
|
1932
|
+
if (r && r.type !== METANET_TYPE && !(r.type === BWALLETX_TYPE && r.substrate !== "pairing")) {
|
|
1933
|
+
const m = matchRemembered(await discoverWallets({ ms, pairing: () => pairing }), r);
|
|
1934
|
+
if (m) return { option: m, reason: "remembered" };
|
|
1935
|
+
}
|
|
1936
|
+
if (mode !== "chooser") {
|
|
1937
|
+
const f = await discoverBwalletX(ms);
|
|
1938
|
+
if (f) {
|
|
1939
|
+
const substrate = f.rdns === RDNS_EXTENSION ? "extension" : f.rdns === RDNS_MOBILE ? "in-app" : "xdm";
|
|
1940
|
+
const option = brc100Option(f.wallet, { id: f.rdns ?? "bwalletx-xdm", rdns: f.rdns ?? void 0, name: "bWalletX", isBwalletX: true, substrate });
|
|
1941
|
+
return { option, reason: "bwalletx" };
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
const list = await discoverWallets({ ms: 50, pairing: () => pairing });
|
|
1945
|
+
if (mode !== "chooser" && !hasBwalletX(list) && !otherDetected(list).length) return "not-found";
|
|
1946
|
+
const picked = await chooseWallet(list, { getUrl: this.#attr("get-url") ?? void 0 });
|
|
1947
|
+
return picked ? { option: picked, reason: "chooser" } : null;
|
|
1948
|
+
}
|
|
1949
|
+
async #click(mode = "signin") {
|
|
1950
|
+
if (this.hasAttribute("disabled") || this.hasAttribute("loading") || this.#busy || this.state === "pending") return;
|
|
1951
|
+
if (!this.#emit("bwalletx-signin", { element: this, mode }, true)) return;
|
|
1952
|
+
if (mode === "signin") {
|
|
1953
|
+
const href = this.#attr("href");
|
|
1954
|
+
const authorizeUrl = this.#attr("authorize-url");
|
|
1955
|
+
if (authorizeUrl) {
|
|
1956
|
+
location.assign(await buildAuthorizeUrl({ authorizeUrl, clientId: this.#attr("client-id"), redirectUri: this.#attr("redirect-uri"), scope: this.#attr("scope") }));
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
if (href) {
|
|
1960
|
+
location.assign(href);
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
const challengeUrl = this.#attr("challenge-url");
|
|
1965
|
+
if (!challengeUrl) return;
|
|
1966
|
+
const own = this.#stateful && !this.#controlled;
|
|
1967
|
+
const prev = this.#phase === "signed-in" ? "signed-in" : "signed-out";
|
|
1968
|
+
const started = Date.now();
|
|
1969
|
+
const pairing = pairingModal();
|
|
1970
|
+
let option = null;
|
|
1971
|
+
const run = ++this.#run;
|
|
1972
|
+
const live = () => run === this.#run;
|
|
1973
|
+
this.#notFound = false;
|
|
1974
|
+
this.#busy = true;
|
|
1975
|
+
this.#viaMetanet = mode === "metanet";
|
|
1976
|
+
this.#pendingLabel = mode === "metanet" ? PENDING_LABEL_OTHER : PENDING_LABEL;
|
|
1977
|
+
if (own) this.#set("pending");
|
|
1978
|
+
else this.#render();
|
|
1979
|
+
try {
|
|
1980
|
+
const ms = Number(this.#attr("discover-ms")) || DEFAULT_DISCOVER_MS;
|
|
1981
|
+
const picked = await this.#choose(mode, ms, pairing);
|
|
1982
|
+
if (picked === "not-found") {
|
|
1983
|
+
this.#notFound = true;
|
|
1984
|
+
if (own) this.#phase = "signed-out";
|
|
1985
|
+
this.#attempt(null, "not-found", started);
|
|
1986
|
+
this.#emit("bwalletx-not-found");
|
|
1987
|
+
if (this.#attr("require-token")) this.#emit("bwalletx-access-denied", { code: "not_installed", message: "No wallet found" });
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
if (!picked) {
|
|
1991
|
+
if (own) this.#phase = prev;
|
|
1992
|
+
this.#attempt(null, "cancelled", started);
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
option = picked.option;
|
|
1996
|
+
const reason = picked.reason;
|
|
1997
|
+
if (!option.isBwalletX) {
|
|
1998
|
+
this.#pendingLabel = PENDING_LABEL_OTHER;
|
|
1999
|
+
if (own) this.#render();
|
|
2000
|
+
}
|
|
2001
|
+
const cw = await option.connect({ identity: false });
|
|
2002
|
+
if (!live()) return;
|
|
2003
|
+
const wallet = cw.wallet;
|
|
2004
|
+
if (!wallet) throw Object.assign(new Error("This wallet cannot sign in here"), { code: "NOT_SUPPORTED" });
|
|
2005
|
+
const sub = substrateOf(option);
|
|
2006
|
+
const method = sub === "extension" ? "extension" : sub === "in-app" || sub === "xdm" ? "in-app" : sub === "pairing" ? "pairing" : "wallet";
|
|
2007
|
+
const requireToken = this.#attr("require-token");
|
|
2008
|
+
if (requireToken) {
|
|
2009
|
+
if (own) this.#phase = "signed-out";
|
|
2010
|
+
const verifyUrl2 = this.#attr("verify-url");
|
|
2011
|
+
const min = this.#attr("min");
|
|
2012
|
+
const minBps = this.#attr("min-bps");
|
|
2013
|
+
const hint = { token: { id: requireToken, ...minBps ? { minBps: Number(minBps) } : { min: min ?? "1" } } };
|
|
2014
|
+
try {
|
|
2015
|
+
const a = await requestAccess({
|
|
2016
|
+
wallet,
|
|
2017
|
+
challenge: (identityKey) => postJson(challengeUrl, { identityKey, require: hint }),
|
|
2018
|
+
verify: verifyUrl2 ? (response2) => postAccess(verifyUrl2, response2) : void 0
|
|
2019
|
+
});
|
|
2020
|
+
this.#picked(option, reason, verifyUrl2 ? a.response.identityKey : null);
|
|
2021
|
+
this.#attempt(option, "signed-in", started);
|
|
2022
|
+
this.#emit("bwalletx-access-granted", { identityKey: a.response.identityKey, response: a.verified });
|
|
2023
|
+
} catch (e) {
|
|
2024
|
+
const err = e instanceof AccessError ? e : new AccessError("bad_request", String(e));
|
|
2025
|
+
this.#attempt(option, outcomeOf(e), started);
|
|
2026
|
+
this.#emit("bwalletx-access-denied", { code: err.code, message: err.message, have: err.have, need: err.need });
|
|
2027
|
+
}
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
const verifyUrl = this.#attr("verify-url");
|
|
2031
|
+
const { result: r, verified } = await signInAndVerify({
|
|
2032
|
+
wallet,
|
|
2033
|
+
// Metanet hands over its key on connect (one request at a time), so its sign-in is the signature alone.
|
|
2034
|
+
identityKey: cw.identityKey ?? void 0,
|
|
2035
|
+
skipWaitForAuthentication: option.id === METANET_OPTION_ID,
|
|
2036
|
+
// its connect() just checked the lock
|
|
2037
|
+
identityKeyHint: verifyUrl ? rememberedIdentity() : void 0,
|
|
2038
|
+
challenge: (identityKey) => postJson(challengeUrl, identityKey ? { identityKey } : {}),
|
|
2039
|
+
verify: async (res) => verifyUrl ? await postJson(verifyUrl, { identityKey: res.identityKey, nonce: res.nonce, signature: res.signature }) : {}
|
|
2040
|
+
});
|
|
2041
|
+
if (!live()) {
|
|
2042
|
+
const out = this.#attr("logout-url");
|
|
2043
|
+
if (verifyUrl && out) await fetch(out, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: "{}" }).catch(() => void 0);
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
if (verifyUrl) rememberIdentity(r.identityKey);
|
|
2047
|
+
const response = verifyUrl ? verified : null;
|
|
2048
|
+
this.#picked(option, reason, verifyUrl ? r.identityKey : null);
|
|
2049
|
+
if (own) this.#signedIn({ identityKey: r.identityKey, handle: response?.handle ?? null, paymail: response?.paymail ?? null });
|
|
2050
|
+
this.#attempt(option, "signed-in", started);
|
|
2051
|
+
this.#emit("bwalletx-signed-in", { identityKey: r.identityKey, method, nonce: r.nonce, signature: r.signature, response });
|
|
2052
|
+
} catch (error) {
|
|
2053
|
+
const outcome = outcomeOf(error);
|
|
2054
|
+
this.#attempt(option, outcome, started);
|
|
2055
|
+
if (!live()) return;
|
|
2056
|
+
const closedQr = outcome === "cancelled" && option?.kind === "pairing";
|
|
2057
|
+
if (own) {
|
|
2058
|
+
if (closedQr) this.#phase = prev;
|
|
2059
|
+
else {
|
|
2060
|
+
this.#error = signInErrorText(error);
|
|
2061
|
+
this.#phase = "error";
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
if (!closedQr) this.#emit("bwalletx-error", { error });
|
|
2065
|
+
} finally {
|
|
2066
|
+
try {
|
|
2067
|
+
pairing.close();
|
|
2068
|
+
} catch {
|
|
2069
|
+
}
|
|
2070
|
+
if (live()) {
|
|
2071
|
+
this.#busy = false;
|
|
2072
|
+
this.#viaMetanet = false;
|
|
2073
|
+
this.#render();
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
function defineBwalletxSignin(tag = SIGNIN_TAG) {
|
|
2080
|
+
if (typeof window === "undefined" || typeof customElements === "undefined") return void 0;
|
|
2081
|
+
const existing = customElements.get(tag);
|
|
2082
|
+
if (existing) return existing;
|
|
2083
|
+
const C = createClass();
|
|
2084
|
+
customElements.define(tag, C);
|
|
2085
|
+
return C;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// src/modal.ts
|
|
2089
|
+
var CSS = `
|
|
2090
|
+
.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}
|
|
2091
|
+
.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)}
|
|
2092
|
+
.bwx-box h2{margin:0 0 6px;font-size:18px;color:#e8c766}
|
|
2093
|
+
.bwx-box p{margin:0 0 14px;color:#bdb6a3;font-size:14px}
|
|
2094
|
+
.bwx-qr{background:#fff;border-radius:12px;padding:12px;display:flex;justify-content:center}
|
|
2095
|
+
.bwx-qr svg{width:100%;height:auto;max-width:260px}
|
|
2096
|
+
.bwx-code{margin:14px 0 0;text-align:center;font-size:28px;letter-spacing:.3em;color:#e8c766;font-variant-numeric:tabular-nums}
|
|
2097
|
+
.bwx-row{display:flex;gap:8px;margin-top:14px}
|
|
2098
|
+
.bwx-btn{flex:1;border:1px solid #3a3220;background:#15130e;color:#f4f1e8;border-radius:10px;padding:10px 12px;font:inherit;cursor:pointer}
|
|
2099
|
+
.bwx-btn:hover{border-color:#e8c766}
|
|
2100
|
+
a.bwx-btn{display:block;text-align:center;text-decoration:none;box-sizing:border-box}
|
|
2101
|
+
.bwx-primary{background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;border:0;font-weight:700;min-height:48px;padding:13px 12px}
|
|
2102
|
+
.bwx-stack{display:flex;flex-direction:column;gap:8px}
|
|
2103
|
+
.bwx-list{display:flex;flex-direction:column;gap:8px}
|
|
2104
|
+
.bwx-opt{display:flex;align-items:center;gap:10px;text-align:left}
|
|
2105
|
+
.bwx-opt img{width:24px;height:24px;border-radius:6px}
|
|
2106
|
+
.bwx-tag{margin-left:auto;font-size:11px;color:#8d866f;white-space:nowrap}
|
|
2107
|
+
.bwx-opt{min-height:48px;box-sizing:border-box}
|
|
2108
|
+
a.bwx-btn.bwx-opt{display:flex;text-align:left}
|
|
2109
|
+
.bwx-btn:focus{outline:none}
|
|
2110
|
+
.bwx-btn:focus-visible{outline:2px solid #e8c766;outline-offset:2px}
|
|
2111
|
+
.bwx-mk{display:inline-flex;align-items:center;justify-content:center;flex:none;width:28px;height:28px;border-radius:50%;background:#000}
|
|
2112
|
+
.bwx-ico{display:inline-grid;place-items:center;flex:none;width:28px;height:28px;border-radius:8px;background:#2a2518;color:#e8c766;font-weight:700;font-size:13px}
|
|
2113
|
+
.bwx-opt img{width:28px;height:28px;border-radius:8px;flex:none}
|
|
2114
|
+
.bwx-txt{display:flex;flex-direction:column;min-width:0;gap:1px}
|
|
2115
|
+
.bwx-txt b{font-weight:600}
|
|
2116
|
+
.bwx-txt small{font-size:12px;color:#8d866f;font-weight:400}
|
|
2117
|
+
.bwx-primary .bwx-txt small{color:rgba(0,0,0,.68)}
|
|
2118
|
+
.bwx-primary .bwx-tag{color:rgba(0,0,0,.6)}
|
|
2119
|
+
.bwx-sep{font-size:12px;color:#8d866f;margin:6px 2px 0}
|
|
2120
|
+
.bwx-foot{font-size:12px;color:#8d866f;margin:12px 0 0}
|
|
2121
|
+
`;
|
|
2122
|
+
function mount() {
|
|
2123
|
+
const root = document.createElement("div");
|
|
2124
|
+
root.className = "bwx-ov";
|
|
2125
|
+
root.setAttribute("role", "dialog");
|
|
2126
|
+
root.setAttribute("aria-modal", "true");
|
|
2127
|
+
const style = document.createElement("style");
|
|
2128
|
+
style.textContent = CSS;
|
|
2129
|
+
const box = document.createElement("div");
|
|
2130
|
+
box.className = "bwx-box";
|
|
2131
|
+
root.append(style, box);
|
|
2132
|
+
document.body.appendChild(root);
|
|
2133
|
+
return { root, box, close: () => root.remove() };
|
|
2134
|
+
}
|
|
2135
|
+
function el(tag, cls, text) {
|
|
2136
|
+
const e = document.createElement(tag);
|
|
2137
|
+
if (cls) e.className = cls;
|
|
2138
|
+
if (text != null) e.textContent = text;
|
|
2139
|
+
return e;
|
|
2140
|
+
}
|
|
2141
|
+
function qrSvg(text) {
|
|
2142
|
+
const q = qrcode(0, "M");
|
|
2143
|
+
q.addData(text);
|
|
2144
|
+
q.make();
|
|
2145
|
+
return q.createSvgTag({ cellSize: 4, margin: 2, scalable: true });
|
|
2146
|
+
}
|
|
2147
|
+
function pairingModal(o = {}) {
|
|
2148
|
+
const ac = new AbortController();
|
|
2149
|
+
let ui = null;
|
|
2150
|
+
let codeEl = null;
|
|
2151
|
+
const close = () => {
|
|
2152
|
+
ui?.close();
|
|
2153
|
+
ui = null;
|
|
2154
|
+
};
|
|
2155
|
+
ac.signal.addEventListener("abort", close);
|
|
2156
|
+
return {
|
|
2157
|
+
signal: ac.signal,
|
|
2158
|
+
close,
|
|
2159
|
+
onLink(link) {
|
|
2160
|
+
if (typeof document === "undefined") return;
|
|
2161
|
+
close();
|
|
2162
|
+
ui = mount();
|
|
2163
|
+
const app = o.mode === "app" || (o.mode ?? "auto") === "auto" && isTouchDevice();
|
|
2164
|
+
const h = el("h2", "", o.title ?? "Connect bWalletX");
|
|
2165
|
+
const cancel = el("button", "bwx-btn", "Cancel");
|
|
2166
|
+
cancel.onclick = () => {
|
|
2167
|
+
o.onCancel?.();
|
|
2168
|
+
ac.abort();
|
|
2169
|
+
};
|
|
2170
|
+
codeEl = el("div", "bwx-code", "");
|
|
2171
|
+
codeEl.setAttribute("aria-live", "polite");
|
|
2172
|
+
if (app) {
|
|
2173
|
+
const p2 = el("p", "", "Open bWalletX, approve, then come back to this page.");
|
|
2174
|
+
const stack = el("div", "bwx-stack");
|
|
2175
|
+
const open = el("a", "bwx-btn bwx-primary", "Open bWalletX");
|
|
2176
|
+
open.href = link;
|
|
2177
|
+
open.rel = "noopener";
|
|
2178
|
+
const web = el("button", "bwx-btn", "Use web.bwalletx.com instead");
|
|
2179
|
+
web.onclick = () => {
|
|
2180
|
+
void navigator.clipboard?.writeText(link).catch(() => void 0);
|
|
2181
|
+
window.open(WEB_WALLET_URL, "_blank", "noopener");
|
|
2182
|
+
p2.textContent = "Link copied. In web.bwalletx.com tap Connect and paste it, then come back here.";
|
|
2183
|
+
};
|
|
2184
|
+
stack.append(open, web, cancel);
|
|
2185
|
+
ui.box.append(h, p2, codeEl, stack);
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
const p = el("p", "", "Scan with bWalletX on your phone, or paste the link into web.bwalletx.com.");
|
|
2189
|
+
const qr = el("div", "bwx-qr");
|
|
2190
|
+
qr.innerHTML = qrSvg(link);
|
|
2191
|
+
const row = el("div", "bwx-row");
|
|
2192
|
+
const copy = el("button", "bwx-btn", "Copy link");
|
|
2193
|
+
copy.onclick = () => void navigator.clipboard?.writeText(link).then(() => copy.textContent = "Copied");
|
|
2194
|
+
row.append(copy, cancel);
|
|
2195
|
+
ui.box.append(h, p, qr, codeEl, row);
|
|
2196
|
+
},
|
|
2197
|
+
onCode(code) {
|
|
2198
|
+
if (codeEl) {
|
|
2199
|
+
codeEl.textContent = code;
|
|
2200
|
+
const p = ui?.box.querySelector("p");
|
|
2201
|
+
if (p) p.textContent = "Check bWalletX shows the same code, then tap Connect.";
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
function pickWallet(options, o = {}) {
|
|
2207
|
+
if (typeof document === "undefined") return Promise.resolve(null);
|
|
2208
|
+
return new Promise((resolve) => {
|
|
2209
|
+
const ui = mount();
|
|
2210
|
+
const done = (w) => {
|
|
2211
|
+
ui.close();
|
|
2212
|
+
resolve(w);
|
|
2213
|
+
};
|
|
2214
|
+
ui.box.append(el("h2", "", o.title ?? "Choose a wallet"));
|
|
2215
|
+
if (!options.length) ui.box.append(el("p", "", "No wallet found. Get bWalletX at bwalletx.com."));
|
|
2216
|
+
const list = el("div", "bwx-list");
|
|
2217
|
+
for (const w of options) {
|
|
2218
|
+
const b = el("button", "bwx-btn bwx-opt");
|
|
2219
|
+
if (w.icon) {
|
|
2220
|
+
const img = el("img");
|
|
2221
|
+
img.src = w.icon;
|
|
2222
|
+
img.alt = "";
|
|
2223
|
+
b.append(img);
|
|
2224
|
+
}
|
|
2225
|
+
b.append(el("span", "", w.name));
|
|
2226
|
+
if (w.kind === "handcash") b.append(el("span", "bwx-tag", "until BRC-100"));
|
|
2227
|
+
b.onclick = () => done(w);
|
|
2228
|
+
list.append(b);
|
|
2229
|
+
}
|
|
2230
|
+
const cancel = el("button", "bwx-btn", "Cancel");
|
|
2231
|
+
cancel.onclick = () => done(null);
|
|
2232
|
+
ui.root.onclick = (e) => e.target === ui.root && done(null);
|
|
2233
|
+
ui.box.append(list, el("div", "bwx-row"));
|
|
2234
|
+
ui.box.lastChild.append(cancel);
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
function optionRow(w, primary, sub, tag) {
|
|
2238
|
+
const b = el("button", `bwx-btn bwx-opt${primary ? " bwx-primary" : ""}`);
|
|
2239
|
+
b.type = "button";
|
|
2240
|
+
b.dataset.wallet = w.id;
|
|
2241
|
+
if (w.isBwalletX) {
|
|
2242
|
+
const mk = el("span", "bwx-mk");
|
|
2243
|
+
mk.innerHTML = markSvg(18);
|
|
2244
|
+
b.append(mk);
|
|
2245
|
+
} else if (w.icon) {
|
|
2246
|
+
const img = el("img");
|
|
2247
|
+
img.src = w.icon;
|
|
2248
|
+
img.alt = "";
|
|
2249
|
+
b.append(img);
|
|
2250
|
+
} else {
|
|
2251
|
+
b.append(el("span", "bwx-ico", (w.name.trim().charAt(0) || "W").toUpperCase()));
|
|
2252
|
+
}
|
|
2253
|
+
const t = el("span", "bwx-txt");
|
|
2254
|
+
t.append(el("b", "", w.name));
|
|
2255
|
+
if (sub) t.append(el("small", "", sub));
|
|
2256
|
+
b.append(t);
|
|
2257
|
+
if (tag) b.append(el("span", "bwx-tag", tag));
|
|
2258
|
+
return b;
|
|
2259
|
+
}
|
|
2260
|
+
function chooseWallet(options, o = {}) {
|
|
2261
|
+
if (typeof document === "undefined") return Promise.resolve(null);
|
|
2262
|
+
return new Promise((resolve) => {
|
|
2263
|
+
const ui = mount();
|
|
2264
|
+
ui.root.dataset.bwxChooser = "";
|
|
2265
|
+
const done = (w) => {
|
|
2266
|
+
document.removeEventListener("keydown", onKey, true);
|
|
2267
|
+
ui.close();
|
|
2268
|
+
resolve(w);
|
|
2269
|
+
};
|
|
2270
|
+
const onKey = (e) => {
|
|
2271
|
+
if (e.key === "Escape") {
|
|
2272
|
+
e.preventDefault();
|
|
2273
|
+
done(null);
|
|
2274
|
+
}
|
|
2275
|
+
};
|
|
2276
|
+
document.addEventListener("keydown", onKey, true);
|
|
2277
|
+
ui.box.append(el("h2", "", o.title ?? "Choose a wallet"));
|
|
2278
|
+
const list = el("div", "bwx-list");
|
|
2279
|
+
const here = hasBwalletX(options);
|
|
2280
|
+
if (!here) {
|
|
2281
|
+
const get = el("a", "bwx-btn bwx-opt bwx-primary");
|
|
2282
|
+
get.href = o.getUrl ?? GET_URL;
|
|
2283
|
+
get.target = "_blank";
|
|
2284
|
+
get.rel = "noopener";
|
|
2285
|
+
get.dataset.wallet = "get-bwalletx";
|
|
2286
|
+
const mk = el("span", "bwx-mk");
|
|
2287
|
+
mk.innerHTML = markSvg(18);
|
|
2288
|
+
const t = el("span", "bwx-txt");
|
|
2289
|
+
t.append(el("b", "", "Get bWalletX"), el("small", "", "Free. Every feature on this site"));
|
|
2290
|
+
get.append(mk, t);
|
|
2291
|
+
list.append(get);
|
|
2292
|
+
}
|
|
2293
|
+
const ordered = chooserOrder(options);
|
|
2294
|
+
const touch = isTouchDevice();
|
|
2295
|
+
let sep = false;
|
|
2296
|
+
for (const w of ordered) {
|
|
2297
|
+
if (!here && !sep) {
|
|
2298
|
+
list.append(el("p", "bwx-sep", "Or use"));
|
|
2299
|
+
sep = true;
|
|
2300
|
+
}
|
|
2301
|
+
const sub = isPresentBwalletX(w) ? "Recommended" : w.kind === "pairing" ? touch ? "Open the bWalletX app" : "Scan a QR with your phone" : void 0;
|
|
2302
|
+
const tag = w.kind === "handcash" ? "until BRC-100" : !w.isBwalletX && w.kind === "brc100" ? "Fewer features" : void 0;
|
|
2303
|
+
const row2 = optionRow(w, isPresentBwalletX(w), sub, tag);
|
|
2304
|
+
row2.onclick = () => done(w);
|
|
2305
|
+
list.append(row2);
|
|
2306
|
+
}
|
|
2307
|
+
if (o.desktop !== false && metanetOffered() && !options.some((w) => w.substrate === "json-api" || w.substrate === "cicada")) {
|
|
2308
|
+
if (!here && !sep) list.append(el("p", "bwx-sep", "Or use"));
|
|
2309
|
+
const d = metanetOption();
|
|
2310
|
+
const row2 = optionRow(d, false, METANET_NOTE);
|
|
2311
|
+
row2.title = "Connects to Metanet Desktop on this computer, never to a browser extension.";
|
|
2312
|
+
row2.onclick = () => done(d);
|
|
2313
|
+
list.append(row2);
|
|
2314
|
+
}
|
|
2315
|
+
ui.box.append(list);
|
|
2316
|
+
if (otherDetected(options).length || !here) ui.box.append(el("p", "bwx-foot", "Some features need bWalletX."));
|
|
2317
|
+
const row = el("div", "bwx-row");
|
|
2318
|
+
const cancel = el("button", "bwx-btn", "Cancel");
|
|
2319
|
+
cancel.type = "button";
|
|
2320
|
+
cancel.onclick = () => done(null);
|
|
2321
|
+
row.append(cancel);
|
|
2322
|
+
ui.box.append(row);
|
|
2323
|
+
ui.root.onclick = (e) => e.target === ui.root && done(null);
|
|
2324
|
+
list.querySelector("a,button")?.focus();
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
export {
|
|
2329
|
+
RDNS_EXTENSION,
|
|
2330
|
+
RDNS_MOBILE,
|
|
2331
|
+
BWALLETX_RDNS,
|
|
2332
|
+
DEFAULT_DISCOVER_MS,
|
|
2333
|
+
discoverBwalletX,
|
|
2334
|
+
findBwalletX,
|
|
2335
|
+
isFramed,
|
|
2336
|
+
prewarmBwalletX,
|
|
2337
|
+
bindPrewarm,
|
|
2338
|
+
inOrder,
|
|
2339
|
+
pairBwalletX,
|
|
2340
|
+
restorePairing,
|
|
2341
|
+
clearPairing,
|
|
2342
|
+
LOGIN_PROTOCOL,
|
|
2343
|
+
connectBwalletX,
|
|
2344
|
+
waitIfLocked,
|
|
2345
|
+
challengeWantsIdentityKey,
|
|
2346
|
+
signInWithBwalletX,
|
|
2347
|
+
signInAndVerify,
|
|
2348
|
+
brc100Option,
|
|
2349
|
+
pairingOption,
|
|
2350
|
+
handcashAdapter,
|
|
2351
|
+
cwiIsAnnounced,
|
|
2352
|
+
discoverWallets,
|
|
2353
|
+
DESKTOP_OPTION_ID,
|
|
2354
|
+
desktopOption,
|
|
2355
|
+
autoChoice,
|
|
2356
|
+
isLocalDesktop,
|
|
2357
|
+
PAYMAIL_LOOKUP_URL,
|
|
2358
|
+
BAVATAR_URL,
|
|
2359
|
+
shortKey,
|
|
2360
|
+
accountLabel,
|
|
2361
|
+
avatarUrlFor,
|
|
2362
|
+
lookupPaymail,
|
|
2363
|
+
isUserRejection,
|
|
2364
|
+
signInErrorText,
|
|
2365
|
+
IDENTITY_HINT_KEY,
|
|
2366
|
+
rememberedIdentity,
|
|
2367
|
+
rememberIdentity,
|
|
2368
|
+
isTouchDevice,
|
|
2369
|
+
METANET_TYPE,
|
|
2370
|
+
METANET_OPTION_ID,
|
|
2371
|
+
METANET_JSON_API,
|
|
2372
|
+
METANET_LABEL,
|
|
2373
|
+
METANET_NOTE,
|
|
2374
|
+
METANET_REMEMBERED_NOTE,
|
|
2375
|
+
NOT_YOU_LABEL,
|
|
2376
|
+
METANET_UNREACHABLE,
|
|
2377
|
+
METANET_ANSWER_MS,
|
|
2378
|
+
METANET_SILENT_MS,
|
|
2379
|
+
MetanetUnreachableError,
|
|
2380
|
+
inAnyFrame,
|
|
2381
|
+
metanetOffered,
|
|
2382
|
+
connectMetanetDesktop,
|
|
2383
|
+
metanetOption,
|
|
2384
|
+
KNOWN_KEYS_KEY,
|
|
2385
|
+
knownIdentityKeys,
|
|
2386
|
+
isKnownIdentityKey,
|
|
2387
|
+
rememberKnownIdentityKey,
|
|
2388
|
+
silentAttemptUsed,
|
|
2389
|
+
claimSilentAttempt,
|
|
2390
|
+
resetSilentAttempt,
|
|
2391
|
+
trySilentMetanet,
|
|
2392
|
+
warnIfNoLogout,
|
|
2393
|
+
BWALLETX_TYPE,
|
|
2394
|
+
substrateOf,
|
|
2395
|
+
walletTypeOf,
|
|
2396
|
+
isPresentBwalletX,
|
|
2397
|
+
hasBwalletX,
|
|
2398
|
+
otherDetected,
|
|
2399
|
+
chooserOrder,
|
|
2400
|
+
decideWallet,
|
|
2401
|
+
outcomeOf,
|
|
2402
|
+
attemptFor,
|
|
2403
|
+
reportAttempt,
|
|
2404
|
+
WALLET_CHOICE_KEY,
|
|
2405
|
+
isRememberable,
|
|
2406
|
+
isExplicitPick,
|
|
2407
|
+
rememberWalletChoice,
|
|
2408
|
+
rememberedWalletChoice,
|
|
2409
|
+
forgetWalletChoice,
|
|
2410
|
+
needsBwalletX,
|
|
2411
|
+
tapGoesElsewhere,
|
|
2412
|
+
isLocalDesktopType,
|
|
2413
|
+
matchRemembered,
|
|
2414
|
+
planAutoConnect,
|
|
2415
|
+
attachRemembered,
|
|
2416
|
+
qrSvg,
|
|
2417
|
+
pairingModal,
|
|
2418
|
+
pickWallet,
|
|
2419
|
+
chooseWallet,
|
|
2420
|
+
toAccessError,
|
|
2421
|
+
throwIfAccessError,
|
|
2422
|
+
proveHoldings,
|
|
2423
|
+
requestAccess,
|
|
2424
|
+
runConnect,
|
|
2425
|
+
runSilentMetanet,
|
|
2426
|
+
SIGNIN_TAG,
|
|
2427
|
+
GET_URL,
|
|
2428
|
+
WEB_WALLET_URL,
|
|
2429
|
+
signinText,
|
|
2430
|
+
markSvg,
|
|
2431
|
+
SIGNIN_CSS,
|
|
2432
|
+
MENU_CSS,
|
|
2433
|
+
PENDING_NOTE,
|
|
2434
|
+
PENDING_LABEL,
|
|
2435
|
+
PENDING_LABEL_OTHER,
|
|
2436
|
+
SWITCH_NOTE,
|
|
2437
|
+
CONTINUE_OTHER_LABEL,
|
|
2438
|
+
buildAuthorizeUrl,
|
|
2439
|
+
defineBwalletxSignin
|
|
2440
|
+
};
|
|
2441
|
+
//# sourceMappingURL=chunk-6EFFOIHK.js.map
|