@three-ws/x402-modal 0.2.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.
@@ -0,0 +1,1371 @@
1
+ // src/util.js
2
+ var EVM_NETWORKS = {
3
+ "eip155:8453": { chainId: 8453, name: "Base", explorer: "https://basescan.org/tx/" },
4
+ "eip155:84532": { chainId: 84532, name: "Base Sepolia", explorer: "https://sepolia.basescan.org/tx/" },
5
+ "eip155:42161": { chainId: 42161, name: "Arbitrum", explorer: "https://arbiscan.io/tx/" },
6
+ "eip155:10": { chainId: 10, name: "Optimism", explorer: "https://optimistic.etherscan.io/tx/" }
7
+ };
8
+ var STABLE_NAMES = /* @__PURE__ */ new Set([
9
+ "usdc",
10
+ "usd coin",
11
+ "usdt",
12
+ "tether",
13
+ "binance-peg usd coin",
14
+ "dai"
15
+ ]);
16
+ function normalizeAccept(accept) {
17
+ if (!accept || typeof accept !== "object") return accept;
18
+ const amount = accept.amount ?? accept.maxAmountRequired;
19
+ return amount != null && accept.amount == null ? { ...accept, amount: String(amount) } : accept;
20
+ }
21
+ function isSolanaNetwork(net) {
22
+ return typeof net === "string" && (net === "solana" || net.startsWith("solana:"));
23
+ }
24
+ function isEvmNetwork(net) {
25
+ return typeof net === "string" && net.startsWith("eip155:");
26
+ }
27
+ function isEip3009Accept(accept) {
28
+ if (!isEvmNetwork(accept?.network)) return false;
29
+ const method = accept?.extra?.assetTransferMethod;
30
+ return !method || method === "eip3009";
31
+ }
32
+ function networkLabel(net, accept) {
33
+ if (isSolanaNetwork(net)) return "Solana";
34
+ const meta = EVM_NETWORKS[net];
35
+ return meta?.name || accept?.extra?.name || net;
36
+ }
37
+ function explorerUrl(net, tx) {
38
+ if (!tx) return null;
39
+ if (isSolanaNetwork(net)) return `https://solscan.io/tx/${tx}`;
40
+ const meta = EVM_NETWORKS[net];
41
+ return meta ? `${meta.explorer}${tx}` : null;
42
+ }
43
+ function formatAmount(rawAtomics, decimals = 6) {
44
+ const n = Number(rawAtomics) / 10 ** decimals;
45
+ if (n < 0.01) return n.toFixed(6).replace(/0+$/, "").replace(/\.$/, "");
46
+ if (n < 1) return n.toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
47
+ return n.toFixed(2);
48
+ }
49
+ function b64encode(obj) {
50
+ const json = JSON.stringify(obj);
51
+ if (typeof Buffer !== "undefined") return Buffer.from(json, "utf8").toString("base64");
52
+ return btoa(unescape(encodeURIComponent(json)));
53
+ }
54
+ function b64decode(str) {
55
+ if (!str) return null;
56
+ try {
57
+ const bin = typeof Buffer !== "undefined" ? Buffer.from(str, "base64").toString("utf8") : decodeURIComponent(escape(atob(str)));
58
+ return JSON.parse(bin);
59
+ } catch (_) {
60
+ return null;
61
+ }
62
+ }
63
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
64
+ function base58encode(bytes) {
65
+ if (!bytes || bytes.length === 0) return "";
66
+ let leadingZeros = 0;
67
+ while (leadingZeros < bytes.length && bytes[leadingZeros] === 0) leadingZeros++;
68
+ let n = 0n;
69
+ for (let i = 0; i < bytes.length; i++) n = n << 8n | BigInt(bytes[i]);
70
+ let out = "";
71
+ while (n > 0n) {
72
+ out = BASE58_ALPHABET[Number(n % 58n)] + out;
73
+ n /= 58n;
74
+ }
75
+ for (let i = 0; i < leadingZeros; i++) out = BASE58_ALPHABET[0] + out;
76
+ return out;
77
+ }
78
+ function toMicroUsd(amount, accept) {
79
+ const atomic = BigInt(amount);
80
+ const decimals = Number(accept?.extra?.decimals ?? 6);
81
+ const name = String(accept?.extra?.name || "").toLowerCase();
82
+ if (STABLE_NAMES.has(name)) {
83
+ if (decimals === 6) return atomic;
84
+ if (decimals > 6) return atomic / 10n ** BigInt(decimals - 6);
85
+ return atomic * 10n ** BigInt(6 - decimals);
86
+ }
87
+ return atomic;
88
+ }
89
+ function spendBuckets(timestamp = Date.now()) {
90
+ const hour = Math.floor(timestamp / 36e5);
91
+ const day = Math.floor(timestamp / 864e5);
92
+ return { hour, day };
93
+ }
94
+ function buildSiwxMessage(info, chain, address) {
95
+ const isEvm = chain.type === "eip191";
96
+ const accountHeader = isEvm ? `${info.domain} wants you to sign in with your Ethereum account:` : `${info.domain} wants you to sign in with your Solana account:`;
97
+ const [, chainTail = ""] = String(chain.chainId).split(":");
98
+ const chainRef = isEvm ? String(parseInt(chainTail, 10)) : chainTail;
99
+ const lines = [accountHeader, address, ""];
100
+ if (info.statement) {
101
+ lines.push(info.statement, "");
102
+ } else if (isEvm) {
103
+ lines.push("");
104
+ }
105
+ lines.push(`URI: ${info.uri}`);
106
+ lines.push(`Version: ${info.version || "1"}`);
107
+ lines.push(`Chain ID: ${chainRef}`);
108
+ lines.push(`Nonce: ${info.nonce}`);
109
+ lines.push(`Issued At: ${info.issuedAt}`);
110
+ if (info.expirationTime) lines.push(`Expiration Time: ${info.expirationTime}`);
111
+ if (info.notBefore) lines.push(`Not Before: ${info.notBefore}`);
112
+ if (info.requestId !== void 0 && info.requestId !== null) lines.push(`Request ID: ${info.requestId}`);
113
+ if (Array.isArray(info.resources) && info.resources.length) {
114
+ lines.push("Resources:");
115
+ for (const r of info.resources) lines.push(`- ${r}`);
116
+ }
117
+ return lines.join("\n");
118
+ }
119
+
120
+ // src/x402-modal.js
121
+ var VERSION = "0.2.0";
122
+ var DEFAULTS = {
123
+ // Origin that serves the Solana `prepare` / `encode` checkout helpers
124
+ // (POST {origin}/api/x402-checkout?action=prepare|encode). Only the Solana
125
+ // payment path uses these — the EVM/EIP-3009 path is fully client-side and
126
+ // needs no backend. `null` ⇒ resolve from the script's own origin at runtime.
127
+ apiOrigin: null,
128
+ // Footer attribution shown at the bottom of the modal.
129
+ brand: { label: "Powered by three.ws", href: "https://three.ws" },
130
+ // ERC-8021 builder-code self-attribution echoed back when the 402 challenge
131
+ // declares a builder code. `wallet` = your wallet code, `service` = your
132
+ // integration code. Set to null to disable the echo entirely.
133
+ builderCode: { wallet: "3d_agent", service: "3d_agent_modal" },
134
+ // CDN modules dynamic-imported on demand. Override to self-host / satisfy a
135
+ // strict Content-Security-Policy.
136
+ solanaWeb3Url: "https://esm.sh/@solana/web3.js@1.95.3?bundle",
137
+ nobleHashesUrl: "https://esm.sh/@noble/hashes@1.4.0/sha3?bundle"
138
+ };
139
+ var config = {
140
+ apiOrigin: DEFAULTS.apiOrigin,
141
+ brand: { ...DEFAULTS.brand },
142
+ builderCode: DEFAULTS.builderCode ? { ...DEFAULTS.builderCode } : null,
143
+ solanaWeb3Url: DEFAULTS.solanaWeb3Url,
144
+ nobleHashesUrl: DEFAULTS.nobleHashesUrl
145
+ };
146
+ function resolveScriptOrigin() {
147
+ try {
148
+ if (typeof document !== "undefined") {
149
+ const current = document.currentScript;
150
+ if (current?.src) return new URL(current.src).origin;
151
+ const found = document.querySelector('script[src*="x402"]');
152
+ if (found?.src) return new URL(found.src).origin;
153
+ }
154
+ } catch (_) {
155
+ }
156
+ return typeof location !== "undefined" ? location.origin : "";
157
+ }
158
+ function configure(opts = {}) {
159
+ if (!opts || typeof opts !== "object") return getConfig();
160
+ if (opts.apiOrigin !== void 0) config.apiOrigin = opts.apiOrigin;
161
+ if (opts.brand) config.brand = { ...config.brand, ...opts.brand };
162
+ if (opts.builderCode === null) config.builderCode = null;
163
+ else if (opts.builderCode) config.builderCode = { ...config.builderCode || {}, ...opts.builderCode };
164
+ if (opts.solanaWeb3Url) config.solanaWeb3Url = opts.solanaWeb3Url;
165
+ if (opts.nobleHashesUrl) config.nobleHashesUrl = opts.nobleHashesUrl;
166
+ return getConfig();
167
+ }
168
+ function getConfig() {
169
+ return {
170
+ apiOrigin: config.apiOrigin,
171
+ brand: { ...config.brand },
172
+ builderCode: config.builderCode ? { ...config.builderCode } : null,
173
+ solanaWeb3Url: config.solanaWeb3Url,
174
+ nobleHashesUrl: config.nobleHashesUrl
175
+ };
176
+ }
177
+ function apiOriginFor(opts) {
178
+ if (opts && opts.apiOrigin !== void 0 && opts.apiOrigin !== null) return opts.apiOrigin;
179
+ if (config.apiOrigin !== null && config.apiOrigin !== void 0) return config.apiOrigin;
180
+ config.apiOrigin = resolveScriptOrigin();
181
+ return config.apiOrigin;
182
+ }
183
+ var SIWX_HEADER = "SIGN-IN-WITH-X";
184
+ var SIWX_EXTENSION_KEY = "sign-in-with-x";
185
+ var SPEND_LS_PREFIX = "x402.spend.";
186
+ function spendKey(address, kind, bucket) {
187
+ return `${SPEND_LS_PREFIX}${kind}.${address.toLowerCase()}.${bucket}`;
188
+ }
189
+ function readSpend(address, kind, bucket) {
190
+ try {
191
+ const raw = localStorage.getItem(spendKey(address, kind, bucket));
192
+ if (!raw) return 0n;
193
+ return BigInt(raw);
194
+ } catch {
195
+ return 0n;
196
+ }
197
+ }
198
+ function writeSpend(address, kind, bucket, value) {
199
+ try {
200
+ localStorage.setItem(spendKey(address, kind, bucket), value.toString());
201
+ } catch {
202
+ }
203
+ }
204
+ function browserEnforceCap({ accept, caps, address }) {
205
+ if (!caps || !address) return { abort: false };
206
+ const microUsd = toMicroUsd(accept.amount, accept);
207
+ const maxPerCall = caps.maxPerCall != null ? BigInt(caps.maxPerCall) : null;
208
+ const maxPerHour = caps.maxPerHour != null ? BigInt(caps.maxPerHour) : null;
209
+ const maxPerDay = caps.maxPerDay != null ? BigInt(caps.maxPerDay) : null;
210
+ if (maxPerCall != null && microUsd > maxPerCall) {
211
+ return { abort: true, reason: `Per-call cap exceeded (${microUsd} > ${maxPerCall} \xB5USD)` };
212
+ }
213
+ const buckets = spendBuckets();
214
+ const hourTotal = readSpend(address, "hr", buckets.hour) + microUsd;
215
+ const dayTotal = readSpend(address, "day", buckets.day) + microUsd;
216
+ if (maxPerHour != null && hourTotal > maxPerHour) {
217
+ return { abort: true, reason: `Hourly cap exceeded (${hourTotal} > ${maxPerHour} \xB5USD)` };
218
+ }
219
+ if (maxPerDay != null && dayTotal > maxPerDay) {
220
+ return { abort: true, reason: `Daily cap exceeded (${dayTotal} > ${maxPerDay} \xB5USD)` };
221
+ }
222
+ writeSpend(address, "hr", buckets.hour, hourTotal);
223
+ writeSpend(address, "day", buckets.day, dayTotal);
224
+ return { abort: false, reservation: { address, microUsd, buckets } };
225
+ }
226
+ function browserRollbackReservation(reservation) {
227
+ if (!reservation) return;
228
+ const { address, microUsd, buckets } = reservation;
229
+ const hourCurrent = readSpend(address, "hr", buckets.hour);
230
+ const dayCurrent = readSpend(address, "day", buckets.day);
231
+ const hourNext = hourCurrent - microUsd;
232
+ const dayNext = dayCurrent - microUsd;
233
+ writeSpend(address, "hr", buckets.hour, hourNext < 0n ? 0n : hourNext);
234
+ writeSpend(address, "day", buckets.day, dayNext < 0n ? 0n : dayNext);
235
+ }
236
+ var BUILDER_CODE_KEY = "builder-code";
237
+ var BUILDER_CODE_PATTERN = /^[a-z0-9_]{1,32}$/;
238
+ function buildBuilderCodeEcho(challenge) {
239
+ const codes = config.builderCode;
240
+ if (!codes) return null;
241
+ const ext = challenge?.extensions?.[BUILDER_CODE_KEY];
242
+ const declaredA = ext?.info?.a;
243
+ if (!declaredA || !BUILDER_CODE_PATTERN.test(declaredA)) return null;
244
+ const out = { a: declaredA };
245
+ if (codes.service && BUILDER_CODE_PATTERN.test(codes.service)) out.s = [codes.service];
246
+ if (codes.wallet && BUILDER_CODE_PATTERN.test(codes.wallet)) out.w = codes.wallet;
247
+ return out;
248
+ }
249
+ function extractSiwxExtension(body) {
250
+ const ext = body?.extensions?.[SIWX_EXTENSION_KEY];
251
+ if (!ext || !ext.info || !Array.isArray(ext.supportedChains) || !ext.supportedChains.length) return null;
252
+ return ext;
253
+ }
254
+ function pickSiwxChain(ext, walletKind) {
255
+ for (const chain of ext.supportedChains) {
256
+ if (walletKind === "evm" && chain.type === "eip191") return { chain, kind: "evm" };
257
+ if (walletKind === "solana" && chain.type === "ed25519") return { chain, kind: "solana" };
258
+ }
259
+ return null;
260
+ }
261
+ function encodeSiwxHeaderValue(payload) {
262
+ const json = JSON.stringify(payload);
263
+ if (typeof Buffer !== "undefined") return Buffer.from(json, "utf8").toString("base64");
264
+ return btoa(unescape(encodeURIComponent(json)));
265
+ }
266
+ var _evmChecksum = null;
267
+ async function loadEvmChecksum() {
268
+ if (_evmChecksum) return _evmChecksum;
269
+ const sha3 = await import(
270
+ /* @vite-ignore */
271
+ config.nobleHashesUrl
272
+ );
273
+ const keccak = sha3.keccak_256;
274
+ _evmChecksum = (addr) => {
275
+ const a = String(addr).toLowerCase().replace(/^0x/, "");
276
+ if (!/^[0-9a-f]{40}$/.test(a)) throw new Error(`invalid EVM address: ${addr}`);
277
+ const hashBytes = keccak(new TextEncoder().encode(a));
278
+ let hex = "";
279
+ for (let i = 0; i < hashBytes.length; i++) hex += hashBytes[i].toString(16).padStart(2, "0");
280
+ let out = "0x";
281
+ for (let i = 0; i < 40; i++) {
282
+ out += parseInt(hex[i], 16) >= 8 ? a[i].toUpperCase() : a[i];
283
+ }
284
+ return out;
285
+ };
286
+ return _evmChecksum;
287
+ }
288
+ var STYLE_ID = "x402-styles";
289
+ var STYLES = `
290
+ :root {
291
+ --x402-z: 2147483600;
292
+ }
293
+ .x402-overlay {
294
+ position: fixed; inset: 0;
295
+ background: rgba(8, 10, 18, 0.55);
296
+ backdrop-filter: blur(10px);
297
+ -webkit-backdrop-filter: blur(10px);
298
+ display: flex; align-items: center; justify-content: center;
299
+ z-index: var(--x402-z);
300
+ opacity: 0; transition: opacity 0.16s ease-out;
301
+ font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
302
+ -webkit-font-smoothing: antialiased;
303
+ color: #0f0f0f;
304
+ }
305
+ .x402-overlay.x402-open { opacity: 1; }
306
+ .x402-overlay * { box-sizing: border-box; }
307
+ .x402-modal {
308
+ width: calc(100% - 32px); max-width: 420px;
309
+ background: #ffffff;
310
+ border-radius: 18px;
311
+ box-shadow: 0 24px 80px rgba(8, 10, 18, 0.28), 0 4px 16px rgba(8, 10, 18, 0.12);
312
+ overflow: hidden;
313
+ transform: translateY(8px) scale(0.985);
314
+ transition: transform 0.18s ease-out;
315
+ display: flex; flex-direction: column;
316
+ max-height: calc(100dvh - 32px);
317
+ }
318
+ .x402-overlay.x402-open .x402-modal { transform: translateY(0) scale(1); }
319
+ .x402-head {
320
+ padding: 18px 20px 14px;
321
+ border-bottom: 1px solid #eef0f4;
322
+ display: flex; align-items: center; gap: 12px;
323
+ }
324
+ .x402-head .x402-merchant {
325
+ flex: 1; min-width: 0;
326
+ }
327
+ .x402-merchant .x402-name {
328
+ font-size: 12px; color: #5a6378; font-weight: 600; letter-spacing: 0.02em; text-transform: uppercase;
329
+ margin-bottom: 2px;
330
+ }
331
+ .x402-merchant .x402-action {
332
+ font-size: 17px; font-weight: 700; color: #0f0f0f;
333
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
334
+ letter-spacing: -0.01em;
335
+ }
336
+ .x402-close {
337
+ width: 32px; height: 32px;
338
+ border-radius: 8px; border: none; background: #f3f4f7;
339
+ font-size: 16px; color: #5a6378; cursor: pointer;
340
+ display: flex; align-items: center; justify-content: center;
341
+ transition: background 0.12s;
342
+ }
343
+ .x402-close:hover { background: #e7e9ee; color: #0f0f0f; }
344
+
345
+ .x402-price-row {
346
+ padding: 18px 20px;
347
+ display: flex; align-items: baseline; justify-content: space-between;
348
+ background: linear-gradient(180deg, #fafbfc 0%, #ffffff 100%);
349
+ border-bottom: 1px solid #eef0f4;
350
+ }
351
+ .x402-price {
352
+ font-size: 32px; font-weight: 700; letter-spacing: -0.02em; color: #0f0f0f;
353
+ font-variant-numeric: tabular-nums;
354
+ }
355
+ .x402-price .x402-currency { font-size: 14px; color: #5a6378; font-weight: 600; margin-left: 6px; letter-spacing: 0; }
356
+ .x402-network {
357
+ font-size: 12px; color: #5a6378; font-weight: 500;
358
+ background: #f3f4f7; padding: 5px 10px; border-radius: 99px;
359
+ display: inline-flex; align-items: center; gap: 6px;
360
+ }
361
+ .x402-network::before {
362
+ content: ''; width: 6px; height: 6px; border-radius: 50%;
363
+ background: #22c55e;
364
+ }
365
+
366
+ .x402-body {
367
+ padding: 16px 20px 18px;
368
+ flex: 1 1 auto; overflow-y: auto;
369
+ display: flex; flex-direction: column; gap: 10px;
370
+ }
371
+ .x402-step {
372
+ display: flex; gap: 12px; align-items: flex-start;
373
+ padding: 10px 0;
374
+ }
375
+ .x402-step + .x402-step { border-top: 1px solid #f3f4f7; }
376
+ .x402-step-num {
377
+ width: 22px; height: 22px; flex: 0 0 auto;
378
+ border-radius: 50%; border: 1.5px solid #d0d4dd; background: #fff;
379
+ color: #5a6378;
380
+ font-size: 11px; font-weight: 700;
381
+ display: flex; align-items: center; justify-content: center;
382
+ }
383
+ .x402-step.x402-active .x402-step-num {
384
+ border-color: #0a84ff; background: #0a84ff; color: #fff;
385
+ animation: x402-spin 1.2s linear infinite;
386
+ }
387
+ .x402-step.x402-done .x402-step-num {
388
+ border-color: #22c55e; background: #22c55e; color: #fff;
389
+ }
390
+ .x402-step.x402-error .x402-step-num {
391
+ border-color: #ef4444; background: #ef4444; color: #fff;
392
+ }
393
+ @keyframes x402-spin {
394
+ from { box-shadow: 0 0 0 0 rgba(10, 132, 255, 0.4); }
395
+ to { box-shadow: 0 0 0 8px rgba(10, 132, 255, 0); }
396
+ }
397
+ .x402-step-body { flex: 1; min-width: 0; }
398
+ .x402-step-label { font-size: 14px; font-weight: 600; color: #0f0f0f; line-height: 1.35; }
399
+ .x402-step-meta { font-size: 12px; color: #5a6378; margin-top: 2px; font-feature-settings: 'tnum' 1; }
400
+ .x402-step.x402-error .x402-step-meta { color: #ef4444; }
401
+
402
+ .x402-wallet-buttons {
403
+ display: flex; flex-direction: column; gap: 8px;
404
+ margin-top: 4px;
405
+ }
406
+ .x402-wallet-btn {
407
+ width: 100%; padding: 13px 14px;
408
+ background: #ffffff; border: 1.5px solid #e2e5ec; border-radius: 11px;
409
+ font-size: 14px; font-weight: 600; color: #0f0f0f;
410
+ cursor: pointer; font-family: inherit;
411
+ display: flex; align-items: center; gap: 12px;
412
+ transition: border-color 0.12s, background 0.12s, transform 0.05s;
413
+ }
414
+ .x402-wallet-btn:hover:not(:disabled) { border-color: #0a84ff; background: #f7faff; }
415
+ .x402-wallet-btn:active:not(:disabled) { transform: translateY(1px); }
416
+ .x402-wallet-btn:disabled { opacity: 0.45; cursor: not-allowed; }
417
+ .x402-wallet-icon {
418
+ width: 28px; height: 28px; flex: 0 0 auto;
419
+ border-radius: 7px;
420
+ display: flex; align-items: center; justify-content: center;
421
+ font-size: 16px;
422
+ background: #f3f4f7;
423
+ }
424
+ .x402-wallet-icon.x402-phantom { background: linear-gradient(135deg, #ab9ff2, #534bb1); color: #fff; }
425
+ .x402-wallet-icon.x402-metamask { background: linear-gradient(135deg, #f6851b, #e2761b); color: #fff; }
426
+ .x402-wallet-name { flex: 1; text-align: left; }
427
+ .x402-wallet-meta { font-size: 11px; color: #8a90a8; font-weight: 500; }
428
+
429
+ .x402-pay-btn {
430
+ width: 100%; padding: 14px 16px;
431
+ background: #0f0f0f; color: #fff; border: none;
432
+ border-radius: 12px;
433
+ font-size: 15px; font-weight: 700; font-family: inherit;
434
+ cursor: pointer; letter-spacing: -0.005em;
435
+ transition: background 0.12s, transform 0.05s;
436
+ margin-top: 4px;
437
+ display: flex; align-items: center; justify-content: center; gap: 8px;
438
+ }
439
+ .x402-pay-btn:hover:not(:disabled) { background: #1d1d1d; }
440
+ .x402-pay-btn:active:not(:disabled) { transform: translateY(1px); }
441
+ .x402-pay-btn:disabled { background: #c8ccd4; cursor: not-allowed; }
442
+
443
+ .x402-pay-secondary {
444
+ width: 100%; padding: 12px 14px;
445
+ background: #ffffff; color: #0f0f0f;
446
+ border: 1.5px solid #e2e5ec; border-radius: 11px;
447
+ font-size: 14px; font-weight: 600; font-family: inherit;
448
+ cursor: pointer; letter-spacing: -0.005em;
449
+ margin-top: 6px;
450
+ transition: border-color 0.12s, background 0.12s, transform 0.05s;
451
+ }
452
+ .x402-pay-secondary:hover:not(:disabled) { border-color: #0a84ff; background: #f7faff; }
453
+ .x402-pay-secondary:active:not(:disabled) { transform: translateY(1px); }
454
+
455
+ .x402-siwx-hint {
456
+ font-size: 11px; color: #5a6378; text-align: center;
457
+ margin-top: 8px; line-height: 1.4;
458
+ }
459
+ .x402-siwx-fallback {
460
+ font-size: 12px; color: #b45309; line-height: 1.45;
461
+ padding: 8px 10px; border-radius: 8px;
462
+ background: #fffbeb; border: 1px solid #fde68a;
463
+ margin-bottom: 6px;
464
+ }
465
+
466
+ .x402-error-box {
467
+ padding: 12px 14px; border-radius: 10px;
468
+ background: #fef2f2; border: 1px solid #fecaca; color: #b91c1c;
469
+ font-size: 13px; line-height: 1.45;
470
+ font-family: ui-monospace, "JetBrains Mono", Menlo, monospace;
471
+ word-break: break-word;
472
+ }
473
+ .x402-error-box strong { font-weight: 700; }
474
+
475
+ .x402-receipt {
476
+ padding: 14px 16px; border-radius: 12px;
477
+ background: linear-gradient(180deg, #f0fdf4 0%, #ffffff 100%);
478
+ border: 1px solid #bbf7d0;
479
+ }
480
+ .x402-receipt-title {
481
+ font-size: 11px; font-weight: 700; color: #15803d;
482
+ text-transform: uppercase; letter-spacing: 0.06em;
483
+ margin-bottom: 8px;
484
+ display: flex; align-items: center; gap: 6px;
485
+ }
486
+ .x402-receipt-title::before { content: '\u2713'; font-size: 14px; }
487
+ .x402-receipt-row {
488
+ display: flex; justify-content: space-between; gap: 12px;
489
+ font-size: 12px; padding: 2px 0;
490
+ font-family: ui-monospace, "JetBrains Mono", Menlo, monospace;
491
+ }
492
+ .x402-receipt-row .x402-k { color: #5a6378; }
493
+ .x402-receipt-row .x402-v { color: #0f0f0f; text-align: right; word-break: break-all; }
494
+ .x402-receipt-row a { color: #0a84ff; text-decoration: none; }
495
+ .x402-receipt-row a:hover { text-decoration: underline; }
496
+
497
+ .x402-result {
498
+ padding: 12px 14px; border-radius: 10px;
499
+ background: #fafbfc; border: 1px solid #e2e5ec;
500
+ max-height: 240px; overflow: auto;
501
+ font-family: ui-monospace, "JetBrains Mono", Menlo, monospace;
502
+ font-size: 12px; line-height: 1.5; color: #0f0f0f;
503
+ white-space: pre-wrap; word-break: break-word;
504
+ }
505
+
506
+ .x402-foot {
507
+ padding: 10px 20px 14px;
508
+ border-top: 1px solid #eef0f4;
509
+ display: flex; align-items: center; justify-content: space-between;
510
+ font-size: 11px; color: #8a90a8;
511
+ }
512
+ .x402-foot a { color: #5a6378; text-decoration: none; font-weight: 600; }
513
+ .x402-foot a:hover { color: #0f0f0f; }
514
+ .x402-foot .x402-secure { display: flex; align-items: center; gap: 5px; }
515
+ .x402-foot .x402-secure::before { content: '\u{1F512}'; font-size: 10px; }
516
+
517
+ @media (max-width: 480px) {
518
+ .x402-modal { max-width: none; width: calc(100% - 16px); border-radius: 16px; }
519
+ .x402-price { font-size: 26px; }
520
+ }
521
+
522
+ @media (prefers-color-scheme: dark) {
523
+ .x402-overlay { color: #e6e8f0; }
524
+ .x402-modal { background: #161616; box-shadow: 0 24px 80px rgba(0, 0, 0, 0.6); }
525
+ .x402-head, .x402-price-row, .x402-foot { border-color: #272727; }
526
+ .x402-step + .x402-step { border-top-color: #272727; }
527
+ .x402-merchant .x402-name { color: #8a90a8; }
528
+ .x402-merchant .x402-action, .x402-price, .x402-step-label { color: #e6e8f0; }
529
+ .x402-step-meta { color: #8a90a8; }
530
+ .x402-close { background: #222222; color: #8a90a8; }
531
+ .x402-close:hover { background: #2e2e2e; color: #e6e8f0; }
532
+ .x402-price-row { background: linear-gradient(180deg, #1d1d1d 0%, #161616 100%); }
533
+ .x402-network { background: #222222; color: #b0b6cc; }
534
+ .x402-wallet-btn { background: #1d1d1d; border-color: #2e2e2e; color: #e6e8f0; }
535
+ .x402-wallet-btn:hover:not(:disabled) { background: #252525; border-color: #0a84ff; }
536
+ .x402-wallet-icon { background: #2e2e2e; }
537
+ .x402-wallet-meta { color: #6b7088; }
538
+ .x402-pay-btn { background: #ffffff; color: #0f0f0f; }
539
+ .x402-pay-btn:hover:not(:disabled) { background: #e7e9ee; }
540
+ .x402-pay-btn:disabled { background: #2e2e2e; color: #5a6378; }
541
+ .x402-pay-secondary { background: #1d1d1d; border-color: #2e2e2e; color: #e6e8f0; }
542
+ .x402-pay-secondary:hover:not(:disabled) { background: #252525; border-color: #0a84ff; }
543
+ .x402-siwx-hint { color: #8a90a8; }
544
+ .x402-siwx-fallback { background: #2a1d10; border-color: #78350f; color: #fcd34d; }
545
+ .x402-step-num { background: #161616; border-color: #2e2e2e; color: #8a90a8; }
546
+ .x402-result { background: #1d1d1d; border-color: #2e2e2e; color: #e6e8f0; }
547
+ .x402-receipt { background: linear-gradient(180deg, #0b1f17 0%, #161616 100%); border-color: #14532d; }
548
+ .x402-receipt-title { color: #4ade80; }
549
+ .x402-receipt-row .x402-k { color: #8a90a8; }
550
+ .x402-receipt-row .x402-v { color: #e6e8f0; }
551
+ .x402-receipt-row a { color: #60a5fa; }
552
+ .x402-error-box { background: #1f1416; border-color: #7f1d1d; color: #fca5a5; }
553
+ .x402-foot a { color: #b0b6cc; }
554
+ .x402-foot a:hover { color: #ffffff; }
555
+ }
556
+ `;
557
+ function injectStyles() {
558
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
559
+ const el = document.createElement("style");
560
+ el.id = STYLE_ID;
561
+ el.textContent = STYLES;
562
+ document.head.appendChild(el);
563
+ }
564
+ var CheckoutModal = class {
565
+ constructor(opts) {
566
+ this.opts = opts;
567
+ this.steps = [
568
+ { id: "discover", label: "Confirming price" },
569
+ { id: "connect", label: "Connect wallet" },
570
+ { id: "authorize", label: "Authorize payment" },
571
+ { id: "verify", label: "Verify & complete" }
572
+ ];
573
+ this.activeNetwork = null;
574
+ this.payerAddress = null;
575
+ this.accept = null;
576
+ this.challenge = null;
577
+ this.disposed = false;
578
+ this.autoConnectTried = false;
579
+ }
580
+ _apiOrigin() {
581
+ return apiOriginFor(this.opts);
582
+ }
583
+ mount() {
584
+ injectStyles();
585
+ const brand = this.opts.brand || config.brand || {};
586
+ const overlay = document.createElement("div");
587
+ overlay.className = "x402-overlay";
588
+ const brandHtml = brand.href ? `<a href="${escapeHtml(brand.href)}" target="_blank" rel="noopener">${escapeHtml(brand.label || brand.href)}</a>` : brand.label ? `<span>${escapeHtml(brand.label)}</span>` : "";
589
+ overlay.innerHTML = `
590
+ <div class="x402-modal" role="dialog" aria-modal="true" aria-label="x402 payment">
591
+ <div class="x402-head">
592
+ <div class="x402-merchant">
593
+ <div class="x402-name" data-merchant>${escapeHtml(this.opts.merchant || "Payment")}</div>
594
+ <div class="x402-action" data-action>${escapeHtml(this.opts.action || "Pay-per-call")}</div>
595
+ </div>
596
+ <button class="x402-close" data-close aria-label="Close">\u2715</button>
597
+ </div>
598
+ <div class="x402-price-row">
599
+ <div class="x402-price" data-price>\u2014<span class="x402-currency"> USDC</span></div>
600
+ <div class="x402-network" data-network>resolving\u2026</div>
601
+ </div>
602
+ <div class="x402-body" data-body></div>
603
+ <div class="x402-foot">
604
+ <span class="x402-secure">x402 \xB7 onchain settled</span>
605
+ ${brandHtml}
606
+ </div>
607
+ </div>
608
+ `;
609
+ document.body.appendChild(overlay);
610
+ this.overlay = overlay;
611
+ this.bodyEl = overlay.querySelector("[data-body]");
612
+ this.priceEl = overlay.querySelector("[data-price]");
613
+ this.networkEl = overlay.querySelector("[data-network]");
614
+ overlay.querySelector("[data-close]").addEventListener("click", () => this.close("cancelled"));
615
+ overlay.addEventListener("click", (e) => {
616
+ if (e.target === overlay) this.close("cancelled");
617
+ });
618
+ this.onKey = (e) => {
619
+ if (e.key === "Escape") this.close("cancelled");
620
+ };
621
+ document.addEventListener("keydown", this.onKey);
622
+ requestAnimationFrame(() => overlay.classList.add("x402-open"));
623
+ return new Promise((resolve, reject) => {
624
+ this.resolve = resolve;
625
+ this.reject = reject;
626
+ });
627
+ }
628
+ close(reason) {
629
+ if (this.disposed) return;
630
+ this.disposed = true;
631
+ document.removeEventListener("keydown", this.onKey);
632
+ this.overlay.classList.remove("x402-open");
633
+ setTimeout(() => this.overlay.remove(), 180);
634
+ if (reason === "cancelled" && this.reject) {
635
+ const err = new Error("cancelled");
636
+ err.code = "cancelled";
637
+ this.reject(err);
638
+ }
639
+ }
640
+ renderSteps(activeId, status = {}) {
641
+ const html = this.steps.map((s) => {
642
+ const state = status[s.id] || (s.id === activeId ? "active" : "idle");
643
+ const cls = state === "active" ? "x402-active" : state === "done" ? "x402-done" : state === "error" ? "x402-error" : "";
644
+ const meta = status[`${s.id}_meta`] || "";
645
+ const sym = state === "done" ? "\u2713" : state === "error" ? "!" : s.id === activeId && state === "active" ? " " : this.steps.findIndex((x) => x.id === s.id) + 1;
646
+ return `<div class="x402-step ${cls}">
647
+ <div class="x402-step-num">${sym}</div>
648
+ <div class="x402-step-body">
649
+ <div class="x402-step-label">${s.label}</div>
650
+ ${meta ? `<div class="x402-step-meta">${escapeHtml(meta)}</div>` : ""}
651
+ </div>
652
+ </div>`;
653
+ }).join("");
654
+ return html;
655
+ }
656
+ setPrice(accept) {
657
+ const decimals = accept.extra?.decimals ?? 6;
658
+ const amount = formatAmount(accept.amount, decimals);
659
+ const sym = (accept.extra?.name || "USDC").replace(/^USD Coin$/, "USDC");
660
+ this.priceEl.innerHTML = `${amount}<span class="x402-currency"> ${sym}</span>`;
661
+ this.networkEl.textContent = networkLabel(accept.network, accept);
662
+ }
663
+ renderConnect() {
664
+ const phantomDetected = typeof window !== "undefined" && (window.solana?.isPhantom || window.phantom?.solana);
665
+ const evmDetected = typeof window !== "undefined" && window.ethereum;
666
+ const solanaAccept = this.challenge?.accepts.find((a) => isSolanaNetwork(a.network));
667
+ const evmAccept = this.challenge?.accepts.find(isEip3009Accept);
668
+ if (this.siwx && !this.payFlowOverride) {
669
+ const siwxSolana = phantomDetected ? pickSiwxChain(this.siwx, "solana") : null;
670
+ const siwxEvm = evmDetected ? pickSiwxChain(this.siwx, "evm") : null;
671
+ if (siwxSolana || siwxEvm) {
672
+ this.renderSiwxChoice({ siwxSolana, siwxEvm });
673
+ return;
674
+ }
675
+ }
676
+ if (this.opts.autoConnect && !this.autoConnectTried && !this.siwxFallbackNotice) {
677
+ this.autoConnectTried = true;
678
+ const solanaViable = !!(solanaAccept && phantomDetected);
679
+ const evmViable = !!(evmAccept && evmDetected);
680
+ if (solanaViable && !evmViable) {
681
+ this.runSolana(solanaAccept);
682
+ return;
683
+ }
684
+ if (evmViable && !solanaViable) {
685
+ this.runEvm(evmAccept);
686
+ return;
687
+ }
688
+ }
689
+ const buttons = [];
690
+ if (solanaAccept) {
691
+ buttons.push(`
692
+ <button class="x402-wallet-btn" data-wallet="phantom" ${phantomDetected ? "" : "disabled"}>
693
+ <div class="x402-wallet-icon x402-phantom">P</div>
694
+ <span class="x402-wallet-name">${phantomDetected ? "Phantom" : "Phantom (not detected)"}</span>
695
+ <span class="x402-wallet-meta">${networkLabel(solanaAccept.network, solanaAccept)}</span>
696
+ </button>
697
+ `);
698
+ }
699
+ if (evmAccept) {
700
+ buttons.push(`
701
+ <button class="x402-wallet-btn" data-wallet="evm" ${evmDetected ? "" : "disabled"}>
702
+ <div class="x402-wallet-icon x402-metamask">M</div>
703
+ <span class="x402-wallet-name">${evmDetected ? "Browser wallet" : "No EVM wallet detected"}</span>
704
+ <span class="x402-wallet-meta">${networkLabel(evmAccept.network, evmAccept)}</span>
705
+ </button>
706
+ `);
707
+ }
708
+ const fallbackBox = this.siwxFallbackNotice ? `<div class="x402-siwx-fallback">${escapeHtml(this.siwxFallbackNotice)}</div>` : "";
709
+ this.bodyEl.innerHTML = `
710
+ ${this.renderSteps("connect", { discover: "done" })}
711
+ ${fallbackBox}
712
+ <div class="x402-wallet-buttons">${buttons.join("")}</div>
713
+ `;
714
+ const onClick = (e) => {
715
+ const btn = e.target.closest("[data-wallet]");
716
+ if (!btn || btn.disabled) return;
717
+ const wallet = btn.dataset.wallet;
718
+ if (wallet === "phantom") this.runSolana(solanaAccept);
719
+ else if (wallet === "evm") this.runEvm(evmAccept);
720
+ };
721
+ this.bodyEl.querySelectorAll("[data-wallet]").forEach((b) => b.addEventListener("click", onClick));
722
+ }
723
+ renderSiwxChoice({ siwxSolana, siwxEvm }) {
724
+ const priceText = formatAmount(this.accept.amount, this.accept.extra?.decimals ?? 6);
725
+ const siwxTarget = siwxSolana ? { kind: "solana", chain: siwxSolana.chain } : { kind: "evm", chain: siwxEvm.chain };
726
+ const siwxLabel = siwxTarget.kind === "solana" ? "Sign in with Phantom" : "Sign in with wallet";
727
+ this.bodyEl.innerHTML = `
728
+ ${this.renderSteps("connect", { discover: "done" })}
729
+ <button class="x402-pay-btn" data-action="siwx">${siwxLabel}</button>
730
+ <button class="x402-pay-secondary" data-action="pay">Pay ${priceText} USDC instead</button>
731
+ <div class="x402-siwx-hint">Already paid for this once? Sign in to re-enter without paying again.</div>
732
+ `;
733
+ const siwxBtn = this.bodyEl.querySelector('[data-action="siwx"]');
734
+ const payBtn = this.bodyEl.querySelector('[data-action="pay"]');
735
+ siwxBtn.addEventListener("click", () => {
736
+ if (siwxTarget.kind === "solana") this.runSiwxSolana(siwxTarget.chain);
737
+ else this.runSiwxEvm(siwxTarget.chain);
738
+ });
739
+ payBtn.addEventListener("click", () => {
740
+ this.payFlowOverride = true;
741
+ this.renderConnect();
742
+ });
743
+ requestAnimationFrame(() => siwxBtn.focus());
744
+ }
745
+ renderProgress(activeId, meta = {}) {
746
+ this.bodyEl.innerHTML = this.renderSteps(activeId, {
747
+ discover: "done",
748
+ connect: "done",
749
+ ...activeId === "verify" ? { authorize: "done" } : {},
750
+ [`${activeId}_meta`]: meta.text || "",
751
+ ...meta.statuses
752
+ });
753
+ }
754
+ renderError(stepId, message) {
755
+ this.bodyEl.innerHTML = `
756
+ ${this.renderSteps(stepId, {
757
+ ...stepId !== "discover" ? { discover: "done" } : {},
758
+ ...stepId === "authorize" || stepId === "verify" ? { connect: "done" } : {},
759
+ ...stepId === "verify" ? { authorize: "done" } : {},
760
+ [stepId]: "error",
761
+ [`${stepId}_meta`]: "failed"
762
+ })}
763
+ <div class="x402-error-box"><strong>${escapeHtml(stepId)}:</strong> ${escapeHtml(message)}</div>
764
+ <button class="x402-pay-btn" data-retry>Try again</button>
765
+ `;
766
+ this.bodyEl.querySelector("[data-retry]").addEventListener("click", () => this.start());
767
+ }
768
+ renderDone({ result, payment, siwx }) {
769
+ const resultStr = typeof result === "string" ? result : JSON.stringify(result, null, 2);
770
+ let receiptHtml;
771
+ if (siwx) {
772
+ const addrShort = siwx.address ? `${siwx.address.slice(0, 8)}\u2026${siwx.address.slice(-6)}` : "\u2014";
773
+ receiptHtml = `
774
+ <div class="x402-receipt">
775
+ <div class="x402-receipt-title">Welcome back!</div>
776
+ <div class="x402-receipt-row">
777
+ <span class="x402-k">network</span>
778
+ <span class="x402-v">${escapeHtml(networkLabel(siwx.network) || siwx.network || "\u2014")}</span>
779
+ </div>
780
+ <div class="x402-receipt-row">
781
+ <span class="x402-k">wallet</span>
782
+ <span class="x402-v">${escapeHtml(addrShort)}</span>
783
+ </div>
784
+ <div class="x402-receipt-row">
785
+ <span class="x402-k">paid</span>
786
+ <span class="x402-v">previously \xB7 re-entered free</span>
787
+ </div>
788
+ </div>
789
+ `;
790
+ } else {
791
+ const explorer = explorerUrl(payment?.network, payment?.transaction);
792
+ const txShort = payment?.transaction ? `${payment.transaction.slice(0, 8)}\u2026${payment.transaction.slice(-6)}` : "\u2014";
793
+ receiptHtml = `
794
+ <div class="x402-receipt">
795
+ <div class="x402-receipt-title">Payment confirmed!</div>
796
+ <div class="x402-receipt-row">
797
+ <span class="x402-k">network</span>
798
+ <span class="x402-v">${escapeHtml(networkLabel(payment?.network) || "\u2014")}</span>
799
+ </div>
800
+ <div class="x402-receipt-row">
801
+ <span class="x402-k">payer</span>
802
+ <span class="x402-v">${escapeHtml(payment?.payer ? `${payment.payer.slice(0, 8)}\u2026${payment.payer.slice(-6)}` : "\u2014")}</span>
803
+ </div>
804
+ ${payment?.transaction ? `<div class="x402-receipt-row"><span class="x402-k">tx</span><span class="x402-v">${explorer ? `<a href="${explorer}" target="_blank" rel="noopener">${txShort} \u2197</a>` : txShort}</span></div>` : ""}
805
+ </div>
806
+ `;
807
+ }
808
+ this.bodyEl.innerHTML = `
809
+ ${receiptHtml}
810
+ <div class="x402-result">${escapeHtml(resultStr).slice(0, 4e3)}</div>
811
+ <button class="x402-pay-btn" data-done>Done</button>
812
+ `;
813
+ this.bodyEl.querySelector("[data-done]").addEventListener("click", () => {
814
+ this.disposed = true;
815
+ document.removeEventListener("keydown", this.onKey);
816
+ this.overlay.classList.remove("x402-open");
817
+ setTimeout(() => this.overlay.remove(), 180);
818
+ });
819
+ }
820
+ async start() {
821
+ this.bodyEl.innerHTML = this.renderSteps("discover");
822
+ try {
823
+ const challenge = await discoverChallenge(this.opts);
824
+ this.challenge = challenge;
825
+ this.siwx = extractSiwxExtension(challenge);
826
+ this.payFlowOverride = false;
827
+ this.siwxFallbackNotice = null;
828
+ const solana = challenge.accepts.find((a) => isSolanaNetwork(a.network));
829
+ const evm = challenge.accepts.find(isEip3009Accept);
830
+ const phantomDetected = typeof window !== "undefined" && (window.solana?.isPhantom || window.phantom?.solana);
831
+ this.accept = phantomDetected && solana || evm || challenge.accepts[0];
832
+ this.setPrice(this.accept);
833
+ this.renderConnect();
834
+ } catch (err) {
835
+ this.renderError("discover", err.message || String(err));
836
+ }
837
+ }
838
+ async runSolana(accept) {
839
+ this.accept = accept;
840
+ this.setPrice(accept);
841
+ this.renderProgress("connect", { text: "Opening Phantom\u2026" });
842
+ try {
843
+ const provider = window.phantom?.solana || window.solana;
844
+ if (!provider) throw new Error("Phantom wallet not detected");
845
+ const conn = await provider.connect();
846
+ const payerAddress = (conn?.publicKey || provider.publicKey)?.toString();
847
+ if (!payerAddress) throw new Error("Phantom did not return a public key");
848
+ this.payerAddress = payerAddress;
849
+ const capCheck = browserEnforceCap({
850
+ accept,
851
+ caps: this.opts.caps,
852
+ address: payerAddress
853
+ });
854
+ if (capCheck.abort) {
855
+ this.renderError("authorize", capCheck.reason);
856
+ return;
857
+ }
858
+ this.spendReservation = capCheck.reservation || null;
859
+ this.renderProgress("authorize", { text: `Building Solana payment for ${payerAddress.slice(0, 6)}\u2026${payerAddress.slice(-4)}` });
860
+ const origin = this._apiOrigin();
861
+ const prep = await postJson(`${origin}/api/x402-checkout?action=prepare`, {
862
+ accept,
863
+ buyer: payerAddress
864
+ });
865
+ this.renderProgress("authorize", { text: "Confirm in Phantom\u2026" });
866
+ const txBytes = base64ToUint8Array(prep.tx_base64);
867
+ const SolanaWeb3 = await loadSolanaWeb3();
868
+ const tx = SolanaWeb3.VersionedTransaction.deserialize(txBytes);
869
+ const signed = await provider.signTransaction(tx);
870
+ const signedB64 = uint8ArrayToBase64(signed.serialize());
871
+ const builderCodeBlock = buildBuilderCodeEcho(this.challenge);
872
+ const enc = await postJson(`${origin}/api/x402-checkout?action=encode`, {
873
+ accept,
874
+ signed_tx_base64: signedB64,
875
+ resource_url: new URL(this.opts.endpoint, location.href).href,
876
+ ...builderCodeBlock ? { builder_code: builderCodeBlock } : {}
877
+ });
878
+ await this.executePaid(enc.x_payment);
879
+ } catch (err) {
880
+ if (this.spendReservation) {
881
+ browserRollbackReservation(this.spendReservation);
882
+ this.spendReservation = null;
883
+ }
884
+ this.renderError(this.payerAddress ? "authorize" : "connect", friendlyError(err));
885
+ }
886
+ }
887
+ async runEvm(accept) {
888
+ this.accept = accept;
889
+ this.setPrice(accept);
890
+ this.renderProgress("connect", { text: "Opening browser wallet\u2026" });
891
+ try {
892
+ const eth = window.ethereum;
893
+ if (!eth) throw new Error("No EVM wallet detected");
894
+ const accounts = await eth.request({ method: "eth_requestAccounts" });
895
+ const payerAddress = accounts?.[0];
896
+ if (!payerAddress) throw new Error("Wallet did not return an account");
897
+ this.payerAddress = payerAddress;
898
+ const capCheck = browserEnforceCap({
899
+ accept,
900
+ caps: this.opts.caps,
901
+ address: payerAddress
902
+ });
903
+ if (capCheck.abort) {
904
+ this.renderError("authorize", capCheck.reason);
905
+ return;
906
+ }
907
+ this.spendReservation = capCheck.reservation || null;
908
+ const meta = EVM_NETWORKS[accept.network];
909
+ if (!meta) throw new Error(`Unknown EVM network ${accept.network}`);
910
+ const currentChainHex = await eth.request({ method: "eth_chainId" });
911
+ const desiredChainHex = "0x" + meta.chainId.toString(16);
912
+ if (currentChainHex !== desiredChainHex) {
913
+ this.renderProgress("connect", { text: `Switch wallet to ${meta.name}\u2026` });
914
+ try {
915
+ await eth.request({ method: "wallet_switchEthereumChain", params: [{ chainId: desiredChainHex }] });
916
+ } catch (e) {
917
+ throw new Error(`Wallet is on ${currentChainHex}; please switch to ${meta.name} (${desiredChainHex}) and retry`);
918
+ }
919
+ }
920
+ this.renderProgress("authorize", { text: `Authorize ${formatAmount(accept.amount)} USDC\u2026` });
921
+ const validAfter = 0;
922
+ const validBefore = Math.floor(Date.now() / 1e3) + (accept.maxTimeoutSeconds || 600);
923
+ const nonce = "0x" + randomHex(32);
924
+ const domain = {
925
+ name: accept.extra?.name || "USD Coin",
926
+ version: accept.extra?.version || "2",
927
+ chainId: meta.chainId,
928
+ verifyingContract: accept.asset
929
+ };
930
+ const types = {
931
+ EIP712Domain: [
932
+ { name: "name", type: "string" },
933
+ { name: "version", type: "string" },
934
+ { name: "chainId", type: "uint256" },
935
+ { name: "verifyingContract", type: "address" }
936
+ ],
937
+ TransferWithAuthorization: [
938
+ { name: "from", type: "address" },
939
+ { name: "to", type: "address" },
940
+ { name: "value", type: "uint256" },
941
+ { name: "validAfter", type: "uint256" },
942
+ { name: "validBefore", type: "uint256" },
943
+ { name: "nonce", type: "bytes32" }
944
+ ]
945
+ };
946
+ const message = {
947
+ from: payerAddress,
948
+ to: accept.payTo,
949
+ value: accept.amount,
950
+ validAfter,
951
+ validBefore,
952
+ nonce
953
+ };
954
+ const typedData = {
955
+ primaryType: "TransferWithAuthorization",
956
+ types,
957
+ domain,
958
+ message
959
+ };
960
+ const signature = await eth.request({
961
+ method: "eth_signTypedData_v4",
962
+ params: [payerAddress, JSON.stringify(typedData)]
963
+ });
964
+ const paymentPayload = {
965
+ x402Version: 2,
966
+ scheme: "exact",
967
+ network: accept.network,
968
+ resource: { url: this.opts.endpoint, mimeType: "application/json" },
969
+ accepted: accept,
970
+ payload: {
971
+ signature,
972
+ // The facilitator /verify requires the EIP-3009 time bounds as
973
+ // decimal strings, not JSON numbers. The signature is unaffected:
974
+ // uint256 0 and "0" encode identically.
975
+ authorization: { from: payerAddress, to: accept.payTo, value: accept.amount, validAfter: String(validAfter), validBefore: String(validBefore), nonce }
976
+ }
977
+ };
978
+ const builderCodeBlock = buildBuilderCodeEcho(this.challenge);
979
+ if (builderCodeBlock) {
980
+ paymentPayload.extensions = { "builder-code": builderCodeBlock };
981
+ }
982
+ const xPayment = b64encode(paymentPayload);
983
+ await this.executePaid(xPayment);
984
+ } catch (err) {
985
+ if (this.spendReservation) {
986
+ browserRollbackReservation(this.spendReservation);
987
+ this.spendReservation = null;
988
+ }
989
+ this.renderError(this.payerAddress ? "authorize" : "connect", friendlyError(err));
990
+ }
991
+ }
992
+ async executePaid(xPayment, attempt = 0) {
993
+ this.renderProgress("verify", {
994
+ text: attempt ? "Retrying after upstream throttle\u2026" : "Calling merchant endpoint\u2026"
995
+ });
996
+ try {
997
+ const res = await fetch(this.opts.endpoint, {
998
+ method: this.opts.method || "GET",
999
+ headers: {
1000
+ ...this.opts.headers || {},
1001
+ ...this.opts.body && !this.opts.headers?.["content-type"] ? { "content-type": "application/json" } : {},
1002
+ "X-PAYMENT": xPayment
1003
+ },
1004
+ body: this.opts.body ? typeof this.opts.body === "string" ? this.opts.body : JSON.stringify(this.opts.body) : void 0
1005
+ });
1006
+ const ct = res.headers.get("content-type") || "";
1007
+ const text = await res.text();
1008
+ let result;
1009
+ if (ct.includes("json")) {
1010
+ try {
1011
+ result = JSON.parse(text);
1012
+ } catch {
1013
+ result = text;
1014
+ }
1015
+ } else {
1016
+ result = text;
1017
+ }
1018
+ if (!res.ok) {
1019
+ if (res.status === 429 && attempt < MAX_THROTTLE_RETRIES) {
1020
+ await this.waitForThrottle(retryAfterSeconds(res, result));
1021
+ return this.executePaid(xPayment, attempt + 1);
1022
+ }
1023
+ const msg = result && typeof result === "object" && (result.error_description || result.error) || `HTTP ${res.status}`;
1024
+ throw new Error(msg);
1025
+ }
1026
+ const settleHeader = res.headers.get("x-payment-response");
1027
+ const payment = b64decode(settleHeader) || {};
1028
+ this.spendReservation = null;
1029
+ this.renderDone({ result, payment });
1030
+ this.resolve?.({ ok: true, result, payment, response: { status: res.status, headers: headersToObject(res.headers) } });
1031
+ } catch (err) {
1032
+ if (this.spendReservation) {
1033
+ browserRollbackReservation(this.spendReservation);
1034
+ this.spendReservation = null;
1035
+ }
1036
+ this.renderError("verify", friendlyError(err));
1037
+ }
1038
+ }
1039
+ // Hold the verify step on a live countdown while an upstream throttle resets,
1040
+ // then return so the caller re-sends the same signed payment. The reservation
1041
+ // is deliberately left intact — this is the same payment, not a new one.
1042
+ async waitForThrottle(seconds) {
1043
+ const total = Math.max(1, Math.min(30, Math.round(seconds) || 6));
1044
+ for (let left = total; left > 0; left--) {
1045
+ this.renderProgress("verify", { text: `Service is busy \u2014 retrying in ${left}s\u2026` });
1046
+ await new Promise((r) => setTimeout(r, 1e3));
1047
+ }
1048
+ this.renderProgress("verify", { text: "Retrying\u2026" });
1049
+ }
1050
+ async runSiwxEvm(chain) {
1051
+ this.renderProgress("connect", { text: "Opening browser wallet\u2026" });
1052
+ try {
1053
+ const eth = window.ethereum;
1054
+ if (!eth) throw new Error("No EVM wallet detected");
1055
+ const accounts = await eth.request({ method: "eth_requestAccounts" });
1056
+ const rawAddress = accounts?.[0];
1057
+ if (!rawAddress) throw new Error("Wallet did not return an account");
1058
+ const checksum = await loadEvmChecksum();
1059
+ const address = checksum(rawAddress);
1060
+ this.payerAddress = address;
1061
+ this.renderProgress("authorize", { text: `Sign sign-in message as ${address.slice(0, 6)}\u2026${address.slice(-4)}` });
1062
+ const message = buildSiwxMessage(this.siwx.info, chain, address);
1063
+ const signature = await eth.request({
1064
+ method: "personal_sign",
1065
+ params: [message, address]
1066
+ });
1067
+ const info = this.siwx.info;
1068
+ const payload = {
1069
+ domain: info.domain,
1070
+ address,
1071
+ ...info.statement ? { statement: info.statement } : {},
1072
+ uri: info.uri,
1073
+ version: info.version || "1",
1074
+ chainId: chain.chainId,
1075
+ type: "eip191",
1076
+ nonce: info.nonce,
1077
+ issuedAt: info.issuedAt,
1078
+ ...info.expirationTime ? { expirationTime: info.expirationTime } : {},
1079
+ ...info.notBefore ? { notBefore: info.notBefore } : {},
1080
+ ...info.requestId !== void 0 && info.requestId !== null ? { requestId: info.requestId } : {},
1081
+ ...Array.isArray(info.resources) ? { resources: info.resources } : {},
1082
+ signatureScheme: "eip191",
1083
+ signature
1084
+ };
1085
+ await this.executeSiwx(payload, chain.chainId);
1086
+ } catch (err) {
1087
+ this.renderError(this.payerAddress ? "authorize" : "connect", friendlyError(err));
1088
+ }
1089
+ }
1090
+ async runSiwxSolana(chain) {
1091
+ this.renderProgress("connect", { text: "Opening Phantom\u2026" });
1092
+ try {
1093
+ const provider = window.phantom?.solana || window.solana;
1094
+ if (!provider) throw new Error("Phantom wallet not detected");
1095
+ const conn = await provider.connect();
1096
+ const pubkey = conn?.publicKey || provider.publicKey;
1097
+ const address = pubkey?.toString();
1098
+ if (!address) throw new Error("Phantom did not return a public key");
1099
+ this.payerAddress = address;
1100
+ this.renderProgress("authorize", { text: `Sign sign-in message as ${address.slice(0, 6)}\u2026${address.slice(-4)}` });
1101
+ const message = buildSiwxMessage(this.siwx.info, chain, address);
1102
+ const encoded = new TextEncoder().encode(message);
1103
+ const signed = await provider.signMessage(encoded, "utf8");
1104
+ const sigBytes = signed?.signature instanceof Uint8Array ? signed.signature : new Uint8Array(signed?.signature || signed);
1105
+ if (!sigBytes || !sigBytes.length) throw new Error("Phantom did not return a signature");
1106
+ const signature = base58encode(sigBytes);
1107
+ const info = this.siwx.info;
1108
+ const payload = {
1109
+ domain: info.domain,
1110
+ address,
1111
+ ...info.statement ? { statement: info.statement } : {},
1112
+ uri: info.uri,
1113
+ version: info.version || "1",
1114
+ chainId: chain.chainId,
1115
+ type: "ed25519",
1116
+ nonce: info.nonce,
1117
+ issuedAt: info.issuedAt,
1118
+ ...info.expirationTime ? { expirationTime: info.expirationTime } : {},
1119
+ ...info.notBefore ? { notBefore: info.notBefore } : {},
1120
+ ...info.requestId !== void 0 && info.requestId !== null ? { requestId: info.requestId } : {},
1121
+ ...Array.isArray(info.resources) ? { resources: info.resources } : {},
1122
+ signatureScheme: "siws",
1123
+ signature
1124
+ };
1125
+ await this.executeSiwx(payload, chain.chainId);
1126
+ } catch (err) {
1127
+ this.renderError(this.payerAddress ? "authorize" : "connect", friendlyError(err));
1128
+ }
1129
+ }
1130
+ async executeSiwx(payload, chainId) {
1131
+ this.renderProgress("verify", { text: "Verifying sign-in\u2026" });
1132
+ const headerValue = encodeSiwxHeaderValue(payload);
1133
+ let res;
1134
+ try {
1135
+ res = await fetch(this.opts.endpoint, {
1136
+ method: this.opts.method || "GET",
1137
+ headers: {
1138
+ ...this.opts.headers || {},
1139
+ ...this.opts.body && !this.opts.headers?.["content-type"] ? { "content-type": "application/json" } : {},
1140
+ [SIWX_HEADER]: headerValue
1141
+ },
1142
+ body: this.opts.body ? typeof this.opts.body === "string" ? this.opts.body : JSON.stringify(this.opts.body) : void 0
1143
+ });
1144
+ } catch (err) {
1145
+ this.renderError("verify", friendlyError(err));
1146
+ return;
1147
+ }
1148
+ if (res.status === 200) {
1149
+ const ct = res.headers.get("content-type") || "";
1150
+ const text2 = await res.text();
1151
+ let result;
1152
+ if (ct.includes("json")) {
1153
+ try {
1154
+ result = JSON.parse(text2);
1155
+ } catch {
1156
+ result = text2;
1157
+ }
1158
+ } else {
1159
+ result = text2;
1160
+ }
1161
+ const siwx = { address: payload.address, network: chainId };
1162
+ this.renderDone({ result, siwx });
1163
+ this.resolve?.({
1164
+ ok: true,
1165
+ result,
1166
+ siwx,
1167
+ response: { status: res.status, headers: headersToObject(res.headers) }
1168
+ });
1169
+ return;
1170
+ }
1171
+ if (res.status === 401 || res.status === 402) {
1172
+ let parsed = null;
1173
+ try {
1174
+ parsed = await res.clone().json();
1175
+ } catch (_) {
1176
+ }
1177
+ const code = parsed?.code || parsed?.error;
1178
+ this.siwx = null;
1179
+ this.payerAddress = null;
1180
+ this.payFlowOverride = false;
1181
+ this.siwxFallbackNotice = code === "siwx_not_paid" || res.status === 402 ? "You haven't paid for this yet \u2014 pay now to unlock re-entry." : "Sign-in not accepted \u2014 please pay to continue.";
1182
+ if (!this.challenge || !Array.isArray(this.challenge.accepts) || !this.challenge.accepts.length) {
1183
+ this.start();
1184
+ } else {
1185
+ this.renderConnect();
1186
+ }
1187
+ return;
1188
+ }
1189
+ const text = await res.text().catch(() => "");
1190
+ this.renderError("verify", `SIWX retry failed: HTTP ${res.status}${text ? ` \xB7 ${text.slice(0, 120)}` : ""}`);
1191
+ }
1192
+ };
1193
+ function escapeHtml(s) {
1194
+ return String(s == null ? "" : s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
1195
+ }
1196
+ function headersToObject(headers) {
1197
+ const out = {};
1198
+ headers.forEach((v, k) => out[k] = v);
1199
+ return out;
1200
+ }
1201
+ var MAX_THROTTLE_RETRIES = 2;
1202
+ function retryAfterSeconds(res, result, fallback = 6) {
1203
+ const header = Number.parseInt(res.headers.get("retry-after") || "", 10);
1204
+ if (Number.isFinite(header) && header > 0) return header;
1205
+ const body = result && typeof result === "object" ? Number(result.retry_after) : NaN;
1206
+ if (Number.isFinite(body) && body > 0) return body;
1207
+ return fallback;
1208
+ }
1209
+ function friendlyError(err) {
1210
+ const msg = err?.shortMessage || err?.message || String(err);
1211
+ if (/user rejected|user denied|reject/i.test(msg)) return "cancelled in wallet";
1212
+ if (/throttl|rate.?limit|too many requests|less than \$|in credit|\b429\b/i.test(msg)) {
1213
+ return "The service is briefly busy and your payment was not taken \u2014 retry in a few seconds.";
1214
+ }
1215
+ if (/dynamically imported module|esm\.sh|module script failed/i.test(msg)) {
1216
+ return "A component this wallet path needs (loaded from a CDN) was blocked \u2014 often by a strict host security policy. Pay with MetaMask on Base instead; it needs no third-party code.";
1217
+ }
1218
+ return msg.slice(0, 240);
1219
+ }
1220
+ function base64ToUint8Array(b64) {
1221
+ if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(b64, "base64"));
1222
+ const bin = atob(b64);
1223
+ const arr = new Uint8Array(bin.length);
1224
+ for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
1225
+ return arr;
1226
+ }
1227
+ function uint8ArrayToBase64(arr) {
1228
+ if (typeof Buffer !== "undefined") return Buffer.from(arr).toString("base64");
1229
+ let bin = "";
1230
+ for (let i = 0; i < arr.length; i++) bin += String.fromCharCode(arr[i]);
1231
+ return btoa(bin);
1232
+ }
1233
+ function randomHex(bytes) {
1234
+ const arr = new Uint8Array(bytes);
1235
+ crypto.getRandomValues(arr);
1236
+ return Array.from(arr).map((b) => b.toString(16).padStart(2, "0")).join("");
1237
+ }
1238
+ var _solanaWeb3 = null;
1239
+ async function loadSolanaWeb3() {
1240
+ if (_solanaWeb3) return _solanaWeb3;
1241
+ _solanaWeb3 = await import(
1242
+ /* @vite-ignore */
1243
+ config.solanaWeb3Url
1244
+ );
1245
+ return _solanaWeb3;
1246
+ }
1247
+ async function postJson(url, body) {
1248
+ const res = await fetch(url, {
1249
+ method: "POST",
1250
+ headers: { "content-type": "application/json" },
1251
+ body: JSON.stringify(body)
1252
+ });
1253
+ const text = await res.text();
1254
+ let data;
1255
+ try {
1256
+ data = JSON.parse(text);
1257
+ } catch {
1258
+ data = { error: "parse_error", error_description: text.slice(0, 200) };
1259
+ }
1260
+ if (!res.ok) {
1261
+ const err = new Error(data.error_description || data.error || `HTTP ${res.status}`);
1262
+ err.status = res.status;
1263
+ err.data = data;
1264
+ throw err;
1265
+ }
1266
+ return data;
1267
+ }
1268
+ async function discoverChallenge(opts) {
1269
+ const headers = { ...opts.headers || {} };
1270
+ const init2 = {
1271
+ method: opts.method || "GET",
1272
+ headers,
1273
+ body: opts.body ? typeof opts.body === "string" ? opts.body : JSON.stringify(opts.body) : void 0
1274
+ };
1275
+ if (init2.body && !headers["content-type"]) headers["content-type"] = "application/json";
1276
+ const res = await fetch(opts.endpoint, init2);
1277
+ const prHeader = res.headers.get("payment-required");
1278
+ const is401WithChallenge = res.status === 401 && !!prHeader;
1279
+ if (res.status !== 402 && !is401WithChallenge) {
1280
+ const txt = await res.text();
1281
+ throw new Error(`Endpoint did not return 402 (got ${res.status}). Body: ${txt.slice(0, 120)}`);
1282
+ }
1283
+ let body = is401WithChallenge ? b64decode(prHeader) : await res.json().catch(() => null);
1284
+ if (!body || !Array.isArray(body.accepts) || !body.accepts.length) {
1285
+ const decoded = b64decode(prHeader);
1286
+ if (decoded && Array.isArray(decoded.accepts) && decoded.accepts.length) {
1287
+ body = decoded;
1288
+ }
1289
+ }
1290
+ if (!body || !Array.isArray(body.accepts) || !body.accepts.length) {
1291
+ throw new Error("Endpoint returned 402 but no `accepts` array could be found in body or header");
1292
+ }
1293
+ body.accepts = body.accepts.map(normalizeAccept);
1294
+ return body;
1295
+ }
1296
+ async function pay(opts) {
1297
+ if (!opts?.endpoint) throw new Error("X402.pay: endpoint is required");
1298
+ const modal = new CheckoutModal(opts);
1299
+ const result = modal.mount();
1300
+ queueMicrotask(() => modal.start());
1301
+ return result;
1302
+ }
1303
+ function readOptsFrom(el) {
1304
+ const ds = el.dataset;
1305
+ let body = ds.x402Body;
1306
+ if (body) {
1307
+ try {
1308
+ body = JSON.parse(body);
1309
+ } catch {
1310
+ }
1311
+ }
1312
+ let headers = ds.x402Headers;
1313
+ if (headers) {
1314
+ try {
1315
+ headers = JSON.parse(headers);
1316
+ } catch {
1317
+ headers = void 0;
1318
+ }
1319
+ }
1320
+ let caps = ds.x402Caps;
1321
+ if (caps) {
1322
+ try {
1323
+ caps = JSON.parse(caps);
1324
+ } catch {
1325
+ caps = void 0;
1326
+ }
1327
+ }
1328
+ return {
1329
+ endpoint: ds.x402Endpoint,
1330
+ method: ds.x402Method || (body ? "POST" : "GET"),
1331
+ body,
1332
+ headers,
1333
+ caps,
1334
+ apiOrigin: ds.x402ApiOrigin,
1335
+ merchant: ds.x402Merchant,
1336
+ action: ds.x402Action || el.textContent?.trim().slice(0, 60)
1337
+ };
1338
+ }
1339
+ function bindElement(el) {
1340
+ if (el.dataset.x402Bound === "1") return;
1341
+ el.dataset.x402Bound = "1";
1342
+ el.addEventListener("click", async (e) => {
1343
+ e.preventDefault();
1344
+ const opts = readOptsFrom(el);
1345
+ try {
1346
+ const out = await pay(opts);
1347
+ if (out?.siwx) {
1348
+ el.dispatchEvent(new CustomEvent("x402:siwx-signed", { detail: out.siwx, bubbles: true }));
1349
+ }
1350
+ el.dispatchEvent(new CustomEvent("x402:result", { detail: out, bubbles: true }));
1351
+ } catch (err) {
1352
+ if (err?.code === "cancelled") return;
1353
+ el.dispatchEvent(new CustomEvent("x402:error", { detail: { error: err?.message || String(err) }, bubbles: true }));
1354
+ }
1355
+ });
1356
+ }
1357
+ function init() {
1358
+ if (typeof document === "undefined") return;
1359
+ document.querySelectorAll("[data-x402-endpoint]").forEach(bindElement);
1360
+ }
1361
+ export {
1362
+ CheckoutModal,
1363
+ bindElement,
1364
+ configure,
1365
+ getConfig,
1366
+ init,
1367
+ pay,
1368
+ readOptsFrom,
1369
+ VERSION as version
1370
+ };
1371
+ //# sourceMappingURL=x402-modal.mjs.map