@relai-fi/x402 0.5.8 → 0.5.9
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/README.md +104 -0
- package/dist/client.cjs +535 -9
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +64 -3
- package/dist/client.d.ts +64 -3
- package/dist/client.js +535 -9
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +812 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +810 -34
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +540 -10
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +7 -2
- package/dist/react/index.d.ts +7 -2
- package/dist/react/index.js +540 -10
- package/dist/react/index.js.map +1 -1
- package/dist/server.cjs +273 -25
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +17 -2
- package/dist/server.d.ts +17 -2
- package/dist/server.js +273 -25
- package/dist/server.js.map +1 -1
- package/dist/{types-C-2GNyMh.d.cts → types-CWtUxi3l.d.cts} +12 -1
- package/dist/{types-C-2GNyMh.d.ts → types-CWtUxi3l.d.ts} +12 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -26,6 +26,7 @@ __export(index_exports, {
|
|
|
26
26
|
EXPLORER_TX_URL: () => EXPLORER_TX_URL,
|
|
27
27
|
NETWORK_CAIP2: () => NETWORK_CAIP2,
|
|
28
28
|
NETWORK_LABELS: () => NETWORK_LABELS,
|
|
29
|
+
NETWORK_TOKENS: () => NETWORK_TOKENS,
|
|
29
30
|
NETWORK_V1_TO_V2: () => NETWORK_V1_TO_V2,
|
|
30
31
|
NETWORK_V2_TO_V1: () => NETWORK_V2_TO_V1,
|
|
31
32
|
RELAI_FACILITATOR_URL: () => RELAI_FACILITATOR_URL,
|
|
@@ -51,6 +52,7 @@ __export(index_exports, {
|
|
|
51
52
|
networkV2ToV1: () => networkV2ToV1,
|
|
52
53
|
normalizeNetwork: () => normalizeNetwork,
|
|
53
54
|
normalizePaymentHeader: () => normalizePaymentHeader,
|
|
55
|
+
resolveToken: () => resolveToken,
|
|
54
56
|
stripePayTo: () => stripePayTo,
|
|
55
57
|
toAtomicUnits: () => toAtomicUnits
|
|
56
58
|
});
|
|
@@ -80,6 +82,117 @@ var CHAIN_IDS = {
|
|
|
80
82
|
"polygon": 137,
|
|
81
83
|
"ethereum": 1
|
|
82
84
|
};
|
|
85
|
+
var NETWORK_TOKENS = {
|
|
86
|
+
"solana": [
|
|
87
|
+
{
|
|
88
|
+
address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
89
|
+
symbol: "USDC",
|
|
90
|
+
name: "USD Coin",
|
|
91
|
+
decimals: 6
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"base": [
|
|
95
|
+
{
|
|
96
|
+
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
97
|
+
symbol: "USDC",
|
|
98
|
+
name: "USD Coin",
|
|
99
|
+
decimals: 6,
|
|
100
|
+
domainVersion: "2",
|
|
101
|
+
isStableUsd: true
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"avalanche": [
|
|
105
|
+
{
|
|
106
|
+
address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
|
|
107
|
+
symbol: "USDC",
|
|
108
|
+
name: "USD Coin",
|
|
109
|
+
decimals: 6,
|
|
110
|
+
domainVersion: "2",
|
|
111
|
+
isStableUsd: true
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
"skale-base": [
|
|
115
|
+
{
|
|
116
|
+
address: "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20",
|
|
117
|
+
symbol: "USDC",
|
|
118
|
+
name: "Bridged USDC (SKALE Bridge)",
|
|
119
|
+
decimals: 6,
|
|
120
|
+
domainVersion: "2",
|
|
121
|
+
isStableUsd: true
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
address: "0x2bF5bF154b515EaA82C31a65ec11554fF5aF7fCA",
|
|
125
|
+
symbol: "USDT",
|
|
126
|
+
name: "Bridged USDT (SKALE Bridge)",
|
|
127
|
+
decimals: 6,
|
|
128
|
+
domainVersion: "2",
|
|
129
|
+
isStableUsd: true
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
address: "0x1aeeCFE5454c83B42D8A316246CAc9739E7f690e",
|
|
133
|
+
symbol: "WBTC",
|
|
134
|
+
name: "Wrapped Bitcoin",
|
|
135
|
+
decimals: 8,
|
|
136
|
+
domainVersion: "2",
|
|
137
|
+
isStableUsd: false
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
address: "0x7bD39ABBd0Dd13103542cAe3276C7fA332bCA486",
|
|
141
|
+
symbol: "WETH",
|
|
142
|
+
name: "Wrapped Ether",
|
|
143
|
+
decimals: 18,
|
|
144
|
+
domainVersion: "2",
|
|
145
|
+
isStableUsd: false
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
"skale-base-sepolia": [
|
|
149
|
+
{
|
|
150
|
+
address: "0x2e08028E3C4c2356572E096d8EF835cD5C6030bD",
|
|
151
|
+
symbol: "USDC",
|
|
152
|
+
name: "Bridged USDC (SKALE Bridge)",
|
|
153
|
+
decimals: 6,
|
|
154
|
+
domainVersion: "2",
|
|
155
|
+
isStableUsd: true
|
|
156
|
+
}
|
|
157
|
+
],
|
|
158
|
+
"skale-bite": [
|
|
159
|
+
{
|
|
160
|
+
address: "0xc4083B1E81ceb461Ccef3FDa8A9F24F0d764B6D8",
|
|
161
|
+
symbol: "USDC",
|
|
162
|
+
name: "USDC",
|
|
163
|
+
decimals: 6,
|
|
164
|
+
domainVersion: "1",
|
|
165
|
+
isStableUsd: true
|
|
166
|
+
}
|
|
167
|
+
],
|
|
168
|
+
"polygon": [
|
|
169
|
+
{
|
|
170
|
+
address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
171
|
+
symbol: "USDC",
|
|
172
|
+
name: "USD Coin",
|
|
173
|
+
decimals: 6,
|
|
174
|
+
domainVersion: "2",
|
|
175
|
+
isStableUsd: true
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
"ethereum": [
|
|
179
|
+
{
|
|
180
|
+
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
181
|
+
symbol: "USDC",
|
|
182
|
+
name: "USD Coin",
|
|
183
|
+
decimals: 6,
|
|
184
|
+
domainVersion: "2",
|
|
185
|
+
isStableUsd: true
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
};
|
|
189
|
+
function resolveToken(network, asset) {
|
|
190
|
+
const tokens = NETWORK_TOKENS[network];
|
|
191
|
+
if (!tokens || tokens.length === 0) return null;
|
|
192
|
+
if (!asset) return tokens[0];
|
|
193
|
+
const normalized = String(asset).toLowerCase();
|
|
194
|
+
return tokens.find((token) => token.address.toLowerCase() === normalized) || null;
|
|
195
|
+
}
|
|
83
196
|
var USDC_ADDRESSES = {
|
|
84
197
|
"solana": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
85
198
|
"base": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
@@ -154,9 +267,103 @@ function stripePayTo(stripeSecretKey, options) {
|
|
|
154
267
|
stripeNetwork: options?.network || "base"
|
|
155
268
|
};
|
|
156
269
|
}
|
|
270
|
+
var USD_PRICE_CACHE_TTL_MS = 60 * 1e3;
|
|
271
|
+
var usdPriceCache = /* @__PURE__ */ new Map();
|
|
272
|
+
var COINGECKO_ID_BY_SYMBOL = {
|
|
273
|
+
WETH: "ethereum",
|
|
274
|
+
WBTC: "bitcoin"
|
|
275
|
+
};
|
|
276
|
+
function isStableUsdToken(token) {
|
|
277
|
+
if (token.isStableUsd === true) return true;
|
|
278
|
+
const symbol = String(token.symbol || "").toUpperCase();
|
|
279
|
+
return symbol === "USDC" || symbol === "USDT";
|
|
280
|
+
}
|
|
281
|
+
async function fetchUsdPriceFromCoinGecko(coinId) {
|
|
282
|
+
const now = Date.now();
|
|
283
|
+
const cached = usdPriceCache.get(coinId);
|
|
284
|
+
if (cached && cached.expiresAt > now) {
|
|
285
|
+
return cached.usd;
|
|
286
|
+
}
|
|
287
|
+
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${encodeURIComponent(coinId)}&vs_currencies=usd`;
|
|
288
|
+
const res = await fetch(url, {
|
|
289
|
+
method: "GET",
|
|
290
|
+
headers: {
|
|
291
|
+
Accept: "application/json"
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
if (!res.ok) {
|
|
295
|
+
throw new Error(`CoinGecko price request failed: HTTP ${res.status}`);
|
|
296
|
+
}
|
|
297
|
+
const payload = await res.json();
|
|
298
|
+
const usd = Number(payload?.[coinId]?.usd);
|
|
299
|
+
if (!Number.isFinite(usd) || usd <= 0) {
|
|
300
|
+
throw new Error(`CoinGecko returned invalid usd price for ${coinId}`);
|
|
301
|
+
}
|
|
302
|
+
usdPriceCache.set(coinId, {
|
|
303
|
+
usd,
|
|
304
|
+
expiresAt: now + USD_PRICE_CACHE_TTL_MS
|
|
305
|
+
});
|
|
306
|
+
return usd;
|
|
307
|
+
}
|
|
308
|
+
async function resolveAmountAtomicFromUsd({
|
|
309
|
+
priceUsd,
|
|
310
|
+
token,
|
|
311
|
+
network
|
|
312
|
+
}) {
|
|
313
|
+
const decimals = Number(token.decimals);
|
|
314
|
+
if (!Number.isFinite(decimals) || decimals < 0) {
|
|
315
|
+
throw new Error(`Invalid token decimals for ${token.symbol || token.address}`);
|
|
316
|
+
}
|
|
317
|
+
if (isStableUsdToken(token)) {
|
|
318
|
+
const units = Math.floor(priceUsd * Math.pow(10, decimals));
|
|
319
|
+
return String(Math.max(1, units));
|
|
320
|
+
}
|
|
321
|
+
const symbol = String(token.symbol || "").toUpperCase();
|
|
322
|
+
const coinGeckoId = COINGECKO_ID_BY_SYMBOL[symbol];
|
|
323
|
+
if (!coinGeckoId) {
|
|
324
|
+
throw new Error(`No USD quote source configured for token ${symbol || token.address} on ${network}`);
|
|
325
|
+
}
|
|
326
|
+
const overrideEnv = process.env[`EVM_TOKEN_PRICE_${symbol}_USD`];
|
|
327
|
+
const usdPerToken = overrideEnv && Number(overrideEnv) > 0 ? Number(overrideEnv) : await fetchUsdPriceFromCoinGecko(coinGeckoId);
|
|
328
|
+
const tokenAmount = priceUsd / usdPerToken;
|
|
329
|
+
const rawUnits = tokenAmount * Math.pow(10, decimals);
|
|
330
|
+
if (!Number.isFinite(rawUnits) || rawUnits <= 0) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Invalid conversion result for token ${symbol || token.address}: priceUsd=${priceUsd}, usdPerToken=${usdPerToken}`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
return String(Math.max(1, Math.floor(rawUnits)));
|
|
336
|
+
}
|
|
157
337
|
function isStripePayTo(payTo) {
|
|
158
338
|
return typeof payTo === "object" && payTo !== null && payTo.__brand === "stripePayTo";
|
|
159
339
|
}
|
|
340
|
+
function parseBooleanHeader(value) {
|
|
341
|
+
if (value == null) return null;
|
|
342
|
+
const normalized = String(value).trim().toLowerCase();
|
|
343
|
+
if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on") {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "off") {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
function normalizeIntegritasFlow(value) {
|
|
352
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
353
|
+
if (normalized === "single") return "single";
|
|
354
|
+
if (normalized === "dual") return "dual";
|
|
355
|
+
return void 0;
|
|
356
|
+
}
|
|
357
|
+
function normalizeIntegritasOptions(value) {
|
|
358
|
+
if (value === true) return { enabled: true };
|
|
359
|
+
if (value === false || value == null) return { enabled: false };
|
|
360
|
+
const flow = normalizeIntegritasFlow(value.flow);
|
|
361
|
+
const enabled = typeof value.enabled === "boolean" ? value.enabled : true;
|
|
362
|
+
return {
|
|
363
|
+
enabled,
|
|
364
|
+
...flow ? { flow } : {}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
160
367
|
async function createStripeDepositAddress(secretKey, amountUsdCents, network = "base") {
|
|
161
368
|
const params = new URLSearchParams();
|
|
162
369
|
params.append("amount", String(amountUsdCents));
|
|
@@ -251,8 +458,45 @@ var Relai = class {
|
|
|
251
458
|
const stripeConfig = isStripePayTo(options.payTo) ? options.payTo : null;
|
|
252
459
|
const network = stripeConfig ? stripeConfig.stripeNetwork || "base" : options.network || self.network;
|
|
253
460
|
const caip2 = NETWORK_CAIP2[network];
|
|
254
|
-
const
|
|
255
|
-
const
|
|
461
|
+
const requestedAsset = typeof options.asset === "string" && options.asset.trim() !== "" ? options.asset.trim() : void 0;
|
|
462
|
+
const explicitToken = requestedAsset ? resolveToken(network, requestedAsset) : null;
|
|
463
|
+
if (requestedAsset && !explicitToken) {
|
|
464
|
+
return res.status(400).json({
|
|
465
|
+
error: `Unsupported asset ${requestedAsset} for network ${network}`
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
const fallbackToken = {
|
|
469
|
+
address: USDC_ADDRESSES[network],
|
|
470
|
+
symbol: "USDC",
|
|
471
|
+
name: network === "skale-bite" ? "USDC" : "USD Coin",
|
|
472
|
+
decimals: 6,
|
|
473
|
+
domainVersion: network === "skale-bite" ? "1" : "2",
|
|
474
|
+
isStableUsd: true
|
|
475
|
+
};
|
|
476
|
+
const token = explicitToken || resolveToken(network) || fallbackToken;
|
|
477
|
+
const asset = token.address;
|
|
478
|
+
const tokenName = token.name || "USD Coin";
|
|
479
|
+
const tokenVersion = token.domainVersion || (network === "skale-bite" ? "1" : "2");
|
|
480
|
+
const tokenDecimals = Number.isFinite(Number(token.decimals)) ? Number(token.decimals) : 6;
|
|
481
|
+
let amount;
|
|
482
|
+
try {
|
|
483
|
+
amount = await resolveAmountAtomicFromUsd({
|
|
484
|
+
priceUsd: resolvedPrice,
|
|
485
|
+
token,
|
|
486
|
+
network
|
|
487
|
+
});
|
|
488
|
+
} catch (err) {
|
|
489
|
+
console.error("[Relai] Failed to convert USD amount to token units:", err);
|
|
490
|
+
return res.status(500).json({
|
|
491
|
+
error: "Failed to quote token amount for payment requirements"
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
const configuredIntegritas = normalizeIntegritasOptions(options.integritas);
|
|
495
|
+
const headerIntegritasEnabled = parseBooleanHeader(req.headers["x-integritas"]);
|
|
496
|
+
const headerIntegritasFlow = normalizeIntegritasFlow(req.headers["x-integritas-flow"]);
|
|
497
|
+
const integritasEnabled = headerIntegritasEnabled === null ? configuredIntegritas.enabled : headerIntegritasEnabled;
|
|
498
|
+
const integritasFlow = headerIntegritasFlow || configuredIntegritas.flow;
|
|
499
|
+
const integritasMode = integritasFlow === "single" ? "single_signature_fee_included" : integritasFlow === "dual" ? "dual_signature_split" : void 0;
|
|
256
500
|
const paymentHeader = req.headers["x-payment"] || req.headers["payment-signature"] || req.headers["x-payment-signature"];
|
|
257
501
|
if (!paymentHeader) {
|
|
258
502
|
options.onPaymentRequired?.(req, { price: resolvedPrice, network });
|
|
@@ -268,23 +512,6 @@ var Relai = class {
|
|
|
268
512
|
resolvedPayTo = options.payTo;
|
|
269
513
|
}
|
|
270
514
|
const feePayer = await self.getFeePayer(caip2);
|
|
271
|
-
const tokenMetadata = {
|
|
272
|
-
"eip155:103698795": { name: "USDC", version: "1" },
|
|
273
|
-
// SKALE BITE
|
|
274
|
-
"eip155:1187947933": { name: "Bridged USDC (SKALE Bridge)", version: "2" },
|
|
275
|
-
// SKALE Base
|
|
276
|
-
"eip155:324705682": { name: "Bridged USDC (SKALE Bridge)", version: "2" },
|
|
277
|
-
// SKALE Base Sepolia
|
|
278
|
-
"eip155:8453": { name: "USD Coin", version: "2" },
|
|
279
|
-
// Base
|
|
280
|
-
"eip155:43114": { name: "USD Coin", version: "2" },
|
|
281
|
-
// Avalanche
|
|
282
|
-
"eip155:137": { name: "USD Coin", version: "2" },
|
|
283
|
-
// Polygon
|
|
284
|
-
"eip155:1": { name: "USD Coin", version: "2" }
|
|
285
|
-
// Ethereum
|
|
286
|
-
};
|
|
287
|
-
const metadata = tokenMetadata[caip2] || { name: "USDC", version: "1" };
|
|
288
515
|
return res.status(402).json({
|
|
289
516
|
x402Version: 2,
|
|
290
517
|
error: "Payment required",
|
|
@@ -301,13 +528,28 @@ var Relai = class {
|
|
|
301
528
|
payTo: resolvedPayTo,
|
|
302
529
|
maxTimeoutSeconds: options.maxTimeoutSeconds || 60,
|
|
303
530
|
extra: {
|
|
304
|
-
name:
|
|
305
|
-
version:
|
|
306
|
-
decimals:
|
|
307
|
-
|
|
531
|
+
name: tokenName,
|
|
532
|
+
version: tokenVersion,
|
|
533
|
+
decimals: tokenDecimals,
|
|
534
|
+
symbol: token.symbol,
|
|
535
|
+
...feePayer && { feePayer },
|
|
308
536
|
// Add feePayer if available
|
|
537
|
+
...integritasEnabled ? { integritasEnabled: true } : {},
|
|
538
|
+
...integritasFlow ? { integritasFlow } : {},
|
|
539
|
+
...integritasMode ? { integritasMode } : {},
|
|
540
|
+
...integritasFlow === "single" ? { integritasSingleSignature: true } : {}
|
|
541
|
+
}
|
|
542
|
+
}],
|
|
543
|
+
...configuredIntegritas.enabled || integritasEnabled || !!integritasFlow ? {
|
|
544
|
+
extensions: {
|
|
545
|
+
integritas: {
|
|
546
|
+
available: true,
|
|
547
|
+
selectedFlow: integritasFlow || null,
|
|
548
|
+
availableFlows: ["single", "dual"],
|
|
549
|
+
enabled: integritasEnabled
|
|
550
|
+
}
|
|
309
551
|
}
|
|
310
|
-
}
|
|
552
|
+
} : {}
|
|
311
553
|
});
|
|
312
554
|
}
|
|
313
555
|
let paymentProof;
|
|
@@ -342,7 +584,15 @@ var Relai = class {
|
|
|
342
584
|
amount,
|
|
343
585
|
asset,
|
|
344
586
|
payTo: settlePayTo,
|
|
345
|
-
maxTimeoutSeconds: options.maxTimeoutSeconds || 60
|
|
587
|
+
maxTimeoutSeconds: options.maxTimeoutSeconds || 60,
|
|
588
|
+
...integritasEnabled ? {
|
|
589
|
+
extra: {
|
|
590
|
+
integritasEnabled: true,
|
|
591
|
+
...integritasFlow ? { integritasFlow } : {},
|
|
592
|
+
...integritasMode ? { integritasMode } : {},
|
|
593
|
+
...integritasFlow === "single" ? { integritasSingleSignature: true } : {}
|
|
594
|
+
}
|
|
595
|
+
} : {}
|
|
346
596
|
};
|
|
347
597
|
const settleUrl = `${self.facilitatorUrl}/settle`;
|
|
348
598
|
const settleRes = await fetch(settleUrl, {
|
|
@@ -421,12 +671,32 @@ function createX402Client(config) {
|
|
|
421
671
|
wallets = {},
|
|
422
672
|
wallet: legacyWallet,
|
|
423
673
|
facilitatorUrl = RELAI_FACILITATOR_URL,
|
|
674
|
+
relayWs,
|
|
424
675
|
preferredNetwork,
|
|
425
676
|
solanaRpcUrl = "https://api.mainnet-beta.solana.com",
|
|
426
677
|
evmRpcUrls = {},
|
|
427
678
|
maxAmountAtomic,
|
|
679
|
+
integritas,
|
|
428
680
|
verbose = false
|
|
429
681
|
} = config;
|
|
682
|
+
const relayWsEnabled = relayWs?.enabled === true;
|
|
683
|
+
const relayWsPreflightTimeoutMs = relayWs?.preflightTimeoutMs ?? 5e3;
|
|
684
|
+
const relayWsPaymentTimeoutMs = relayWs?.paymentTimeoutMs ?? 1e4;
|
|
685
|
+
const relayWsFallbackToHttp = relayWs?.fallbackToHttp ?? true;
|
|
686
|
+
const defaultIntegritas = normalizeIntegritasOptions2(integritas);
|
|
687
|
+
const relayWsReservedSubdomains = /* @__PURE__ */ new Set([
|
|
688
|
+
"www",
|
|
689
|
+
"api",
|
|
690
|
+
"localhost",
|
|
691
|
+
"admin",
|
|
692
|
+
"app",
|
|
693
|
+
"dashboard",
|
|
694
|
+
"docs",
|
|
695
|
+
"documentation",
|
|
696
|
+
"status",
|
|
697
|
+
"blog",
|
|
698
|
+
"facilitator"
|
|
699
|
+
]);
|
|
430
700
|
const log = verbose ? console.log.bind(console, "[relai-x402]") : () => {
|
|
431
701
|
};
|
|
432
702
|
const effectiveWallets = { ...wallets };
|
|
@@ -437,6 +707,408 @@ function createX402Client(config) {
|
|
|
437
707
|
effectiveWallets.solana?.publicKey && effectiveWallets.solana?.signTransaction
|
|
438
708
|
);
|
|
439
709
|
if (hasSolanaWallet) log("Solana wallet ready");
|
|
710
|
+
function isRecord(value) {
|
|
711
|
+
return typeof value === "object" && value !== null;
|
|
712
|
+
}
|
|
713
|
+
function parseJsonSafe(value) {
|
|
714
|
+
try {
|
|
715
|
+
return JSON.parse(value);
|
|
716
|
+
} catch {
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function addSocketListener(socket, eventName, listener) {
|
|
721
|
+
if (socket.addEventListener) {
|
|
722
|
+
socket.addEventListener(eventName, listener);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (socket.on) {
|
|
726
|
+
socket.on(eventName, listener);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function removeSocketListener(socket, eventName, listener) {
|
|
730
|
+
if (socket.removeEventListener) {
|
|
731
|
+
socket.removeEventListener(eventName, listener);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (socket.off) {
|
|
735
|
+
socket.off(eventName, listener);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (socket.removeListener) {
|
|
739
|
+
socket.removeListener(eventName, listener);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
function resolveRelayWsUrl(relayUrl) {
|
|
743
|
+
if (relayWs?.wsUrl && relayWs.wsUrl.trim() !== "") {
|
|
744
|
+
return relayWs.wsUrl.trim();
|
|
745
|
+
}
|
|
746
|
+
const parsedRelayUrl = new URL(relayUrl);
|
|
747
|
+
const wsProtocol = parsedRelayUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
748
|
+
return `${wsProtocol}//${parsedRelayUrl.host}/api/ws/relay`;
|
|
749
|
+
}
|
|
750
|
+
function resolveRelayWhitelabel(parsedRelayUrl) {
|
|
751
|
+
const hostParts = parsedRelayUrl.hostname.toLowerCase().split(".").filter(Boolean);
|
|
752
|
+
if (hostParts.length < 2) {
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
const candidate = decodeURIComponent(hostParts[0] || "").trim();
|
|
756
|
+
if (!candidate) {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
if (relayWsReservedSubdomains.has(candidate.toLowerCase())) {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
const lastPart = hostParts[hostParts.length - 1];
|
|
763
|
+
const secondLastPart = hostParts[hostParts.length - 2];
|
|
764
|
+
const isX402WhitelabelHost = hostParts.length >= 3 && secondLastPart === "x402" && lastPart === "fi";
|
|
765
|
+
const isLocalWhitelabelHost = hostParts.length === 2 && lastPart === "localhost";
|
|
766
|
+
if (!isX402WhitelabelHost && !isLocalWhitelabelHost) {
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
return candidate;
|
|
770
|
+
}
|
|
771
|
+
function resolveRelayTarget(relayUrl) {
|
|
772
|
+
const parsedRelayUrl = new URL(relayUrl);
|
|
773
|
+
const match = parsedRelayUrl.pathname.match(/\/relay\/([^/]+)(\/.*)?$/);
|
|
774
|
+
if (match) {
|
|
775
|
+
const apiId = decodeURIComponent(match[1]);
|
|
776
|
+
const pathPart2 = match[2] || "/";
|
|
777
|
+
return {
|
|
778
|
+
apiId,
|
|
779
|
+
path: `${pathPart2}${parsedRelayUrl.search || ""}`
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
const whitelabel = resolveRelayWhitelabel(parsedRelayUrl);
|
|
783
|
+
if (!whitelabel) {
|
|
784
|
+
throw new Error(
|
|
785
|
+
`[relai-x402] Unsupported relay URL format for WS transport: ${relayUrl}. Expected /relay/:apiId/... or <whitelabel>.x402.fi/...`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
const pathPart = parsedRelayUrl.pathname && parsedRelayUrl.pathname !== "" ? parsedRelayUrl.pathname : "/";
|
|
789
|
+
return {
|
|
790
|
+
apiId: whitelabel,
|
|
791
|
+
path: `${pathPart}${parsedRelayUrl.search || ""}`
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
function isRelayRequestUrl(requestUrl) {
|
|
795
|
+
try {
|
|
796
|
+
resolveRelayTarget(requestUrl);
|
|
797
|
+
return true;
|
|
798
|
+
} catch {
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function headersToRecord(headersInit) {
|
|
803
|
+
if (!headersInit) return {};
|
|
804
|
+
const output = {};
|
|
805
|
+
if (typeof Headers !== "undefined" && headersInit instanceof Headers) {
|
|
806
|
+
headersInit.forEach((value, key) => {
|
|
807
|
+
output[key] = value;
|
|
808
|
+
});
|
|
809
|
+
return output;
|
|
810
|
+
}
|
|
811
|
+
if (Array.isArray(headersInit)) {
|
|
812
|
+
for (const [key, value] of headersInit) {
|
|
813
|
+
output[key] = value;
|
|
814
|
+
}
|
|
815
|
+
return output;
|
|
816
|
+
}
|
|
817
|
+
for (const [key, value] of Object.entries(headersInit)) {
|
|
818
|
+
if (typeof value === "string") {
|
|
819
|
+
output[key] = value;
|
|
820
|
+
} else if (Array.isArray(value)) {
|
|
821
|
+
output[key] = value.join(", ");
|
|
822
|
+
} else if (value !== void 0 && value !== null) {
|
|
823
|
+
output[key] = String(value);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return output;
|
|
827
|
+
}
|
|
828
|
+
function hasHeaderCaseInsensitive(headers, headerName) {
|
|
829
|
+
const normalized = headerName.toLowerCase();
|
|
830
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === normalized);
|
|
831
|
+
}
|
|
832
|
+
function normalizeIntegritasFlow2(value) {
|
|
833
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
834
|
+
if (normalized === "single") return "single";
|
|
835
|
+
if (normalized === "dual") return "dual";
|
|
836
|
+
return void 0;
|
|
837
|
+
}
|
|
838
|
+
function normalizeIntegritasOptions2(value) {
|
|
839
|
+
if (value === true) return { enabled: true };
|
|
840
|
+
if (value === false || value == null) return { enabled: false };
|
|
841
|
+
const flow = normalizeIntegritasFlow2(value.flow);
|
|
842
|
+
const enabled = typeof value.enabled === "boolean" ? value.enabled : true;
|
|
843
|
+
return {
|
|
844
|
+
enabled,
|
|
845
|
+
...flow ? { flow } : {}
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
function resolveIntegritasOptions(override) {
|
|
849
|
+
if (override === void 0) {
|
|
850
|
+
return defaultIntegritas;
|
|
851
|
+
}
|
|
852
|
+
if (typeof override === "boolean") {
|
|
853
|
+
return {
|
|
854
|
+
enabled: override,
|
|
855
|
+
...override && defaultIntegritas.flow ? { flow: defaultIntegritas.flow } : {}
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
const flow = normalizeIntegritasFlow2(override.flow) || defaultIntegritas.flow;
|
|
859
|
+
const enabled = typeof override.enabled === "boolean" ? override.enabled : defaultIntegritas.enabled;
|
|
860
|
+
return {
|
|
861
|
+
enabled,
|
|
862
|
+
...enabled && flow ? { flow } : {}
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
function stripInternalInit(init) {
|
|
866
|
+
if (!init) return void 0;
|
|
867
|
+
const { x402: _x402, ...requestInit } = init;
|
|
868
|
+
return requestInit;
|
|
869
|
+
}
|
|
870
|
+
function applyIntegritasHeaders(headers, options) {
|
|
871
|
+
if (!options.enabled) return headers;
|
|
872
|
+
if (!hasHeaderCaseInsensitive(headers, "x-integritas")) {
|
|
873
|
+
headers["X-Integritas"] = "true";
|
|
874
|
+
}
|
|
875
|
+
if (options.flow && !hasHeaderCaseInsensitive(headers, "x-integritas-flow")) {
|
|
876
|
+
headers["X-Integritas-Flow"] = options.flow;
|
|
877
|
+
}
|
|
878
|
+
return headers;
|
|
879
|
+
}
|
|
880
|
+
function getRequestMethod(input, init) {
|
|
881
|
+
const inputMethod = input instanceof Request ? input.method : void 0;
|
|
882
|
+
return (init?.method || inputMethod || "GET").toUpperCase();
|
|
883
|
+
}
|
|
884
|
+
async function bodyInitToWsPayload(bodyInit) {
|
|
885
|
+
if (bodyInit === void 0 || bodyInit === null) {
|
|
886
|
+
return void 0;
|
|
887
|
+
}
|
|
888
|
+
if (typeof bodyInit === "string") {
|
|
889
|
+
const parsed = parseJsonSafe(bodyInit);
|
|
890
|
+
return parsed === null ? bodyInit : parsed;
|
|
891
|
+
}
|
|
892
|
+
if (typeof URLSearchParams !== "undefined" && bodyInit instanceof URLSearchParams) {
|
|
893
|
+
return bodyInit.toString();
|
|
894
|
+
}
|
|
895
|
+
if (typeof FormData !== "undefined" && bodyInit instanceof FormData) {
|
|
896
|
+
const entries = {};
|
|
897
|
+
for (const [key, value] of bodyInit.entries()) {
|
|
898
|
+
entries[key] = typeof value === "string" ? value : value.name;
|
|
899
|
+
}
|
|
900
|
+
return entries;
|
|
901
|
+
}
|
|
902
|
+
if (typeof Blob !== "undefined" && bodyInit instanceof Blob) {
|
|
903
|
+
const text = await bodyInit.text();
|
|
904
|
+
if (!text) return void 0;
|
|
905
|
+
const parsed = parseJsonSafe(text);
|
|
906
|
+
return parsed === null ? text : parsed;
|
|
907
|
+
}
|
|
908
|
+
if (bodyInit instanceof ArrayBuffer) {
|
|
909
|
+
return Array.from(new Uint8Array(bodyInit));
|
|
910
|
+
}
|
|
911
|
+
if (ArrayBuffer.isView(bodyInit)) {
|
|
912
|
+
return Array.from(new Uint8Array(bodyInit.buffer, bodyInit.byteOffset, bodyInit.byteLength));
|
|
913
|
+
}
|
|
914
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(bodyInit)) {
|
|
915
|
+
return Array.from(bodyInit.values());
|
|
916
|
+
}
|
|
917
|
+
if (isRecord(bodyInit)) {
|
|
918
|
+
return bodyInit;
|
|
919
|
+
}
|
|
920
|
+
return String(bodyInit);
|
|
921
|
+
}
|
|
922
|
+
async function resolveRequestBody(input, init) {
|
|
923
|
+
if (init && Object.prototype.hasOwnProperty.call(init, "body")) {
|
|
924
|
+
return bodyInitToWsPayload(init.body);
|
|
925
|
+
}
|
|
926
|
+
if (input instanceof Request) {
|
|
927
|
+
const method = getRequestMethod(input, init);
|
|
928
|
+
if (method === "GET" || method === "HEAD") {
|
|
929
|
+
return void 0;
|
|
930
|
+
}
|
|
931
|
+
try {
|
|
932
|
+
const cloned = input.clone();
|
|
933
|
+
const text = await cloned.text();
|
|
934
|
+
if (!text) return void 0;
|
|
935
|
+
const parsed = parseJsonSafe(text);
|
|
936
|
+
return parsed === null ? text : parsed;
|
|
937
|
+
} catch {
|
|
938
|
+
return void 0;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return void 0;
|
|
942
|
+
}
|
|
943
|
+
function getRequestHeaders(input, init) {
|
|
944
|
+
const fromInput = input instanceof Request ? headersToRecord(input.headers) : {};
|
|
945
|
+
const fromInit = headersToRecord(init?.headers);
|
|
946
|
+
const merged = {
|
|
947
|
+
...fromInput,
|
|
948
|
+
...fromInit
|
|
949
|
+
};
|
|
950
|
+
if (!merged.Accept && !merged.accept) {
|
|
951
|
+
merged.Accept = "application/json";
|
|
952
|
+
}
|
|
953
|
+
return merged;
|
|
954
|
+
}
|
|
955
|
+
function toMessageString(data) {
|
|
956
|
+
if (typeof data === "string") {
|
|
957
|
+
return data;
|
|
958
|
+
}
|
|
959
|
+
if (isRecord(data) && typeof data.data !== "undefined") {
|
|
960
|
+
return toMessageString(data.data);
|
|
961
|
+
}
|
|
962
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
|
|
963
|
+
return data.toString("utf8");
|
|
964
|
+
}
|
|
965
|
+
if (data instanceof ArrayBuffer) {
|
|
966
|
+
const bytes = new Uint8Array(data);
|
|
967
|
+
if (typeof Buffer !== "undefined") {
|
|
968
|
+
return Buffer.from(bytes).toString("utf8");
|
|
969
|
+
}
|
|
970
|
+
if (typeof TextDecoder !== "undefined") {
|
|
971
|
+
return new TextDecoder().decode(bytes);
|
|
972
|
+
}
|
|
973
|
+
throw new Error("Unsupported WebSocket message data type");
|
|
974
|
+
}
|
|
975
|
+
if (ArrayBuffer.isView(data)) {
|
|
976
|
+
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
977
|
+
if (typeof Buffer !== "undefined") {
|
|
978
|
+
return Buffer.from(bytes).toString("utf8");
|
|
979
|
+
}
|
|
980
|
+
if (typeof TextDecoder !== "undefined") {
|
|
981
|
+
return new TextDecoder().decode(bytes);
|
|
982
|
+
}
|
|
983
|
+
throw new Error("Unsupported WebSocket message data type");
|
|
984
|
+
}
|
|
985
|
+
throw new Error("Unsupported WebSocket message data type");
|
|
986
|
+
}
|
|
987
|
+
function getWebSocketFactory() {
|
|
988
|
+
if (relayWs?.webSocketFactory) {
|
|
989
|
+
return relayWs.webSocketFactory;
|
|
990
|
+
}
|
|
991
|
+
if (typeof WebSocket !== "undefined") {
|
|
992
|
+
return (wsUrl) => new WebSocket(wsUrl);
|
|
993
|
+
}
|
|
994
|
+
throw new Error(
|
|
995
|
+
"[relai-x402] WebSocket is not available in this runtime. Provide relayWs.webSocketFactory."
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
async function relayCallOverWebSocket(request) {
|
|
999
|
+
const wsFactory = getWebSocketFactory();
|
|
1000
|
+
const wsUrl = resolveRelayWsUrl(request.relayUrl);
|
|
1001
|
+
const target = resolveRelayTarget(request.relayUrl);
|
|
1002
|
+
const requestId = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
1003
|
+
const socket = wsFactory(wsUrl);
|
|
1004
|
+
return new Promise((resolve, reject) => {
|
|
1005
|
+
let settled = false;
|
|
1006
|
+
const settleResolve = (value) => {
|
|
1007
|
+
if (settled) return;
|
|
1008
|
+
settled = true;
|
|
1009
|
+
cleanup();
|
|
1010
|
+
try {
|
|
1011
|
+
socket.close();
|
|
1012
|
+
} catch {
|
|
1013
|
+
}
|
|
1014
|
+
resolve(value);
|
|
1015
|
+
};
|
|
1016
|
+
const settleReject = (error) => {
|
|
1017
|
+
if (settled) return;
|
|
1018
|
+
settled = true;
|
|
1019
|
+
cleanup();
|
|
1020
|
+
try {
|
|
1021
|
+
socket.close();
|
|
1022
|
+
} catch {
|
|
1023
|
+
}
|
|
1024
|
+
reject(error);
|
|
1025
|
+
};
|
|
1026
|
+
const timeoutId = setTimeout(() => {
|
|
1027
|
+
settleReject(new Error(`[relai-x402] Timed out waiting for WS relay response after ${request.timeoutMs}ms`));
|
|
1028
|
+
}, request.timeoutMs);
|
|
1029
|
+
const cleanup = () => {
|
|
1030
|
+
clearTimeout(timeoutId);
|
|
1031
|
+
removeSocketListener(socket, "open", onOpen);
|
|
1032
|
+
removeSocketListener(socket, "message", onMessage);
|
|
1033
|
+
removeSocketListener(socket, "error", onError);
|
|
1034
|
+
removeSocketListener(socket, "close", onClose);
|
|
1035
|
+
};
|
|
1036
|
+
const onOpen = () => {
|
|
1037
|
+
const envelope = {
|
|
1038
|
+
id: requestId,
|
|
1039
|
+
method: "relay.call",
|
|
1040
|
+
params: {
|
|
1041
|
+
apiId: target.apiId,
|
|
1042
|
+
path: target.path,
|
|
1043
|
+
requestMethod: request.requestMethod,
|
|
1044
|
+
requestHeaders: request.requestHeaders,
|
|
1045
|
+
...request.requestBody !== void 0 ? { requestBody: request.requestBody } : {}
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
if (request.paymentPayload !== void 0) {
|
|
1049
|
+
envelope.payment = request.paymentPayload;
|
|
1050
|
+
}
|
|
1051
|
+
try {
|
|
1052
|
+
socket.send(JSON.stringify(envelope));
|
|
1053
|
+
} catch {
|
|
1054
|
+
settleReject(new Error("[relai-x402] Failed to send WS relay request"));
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
const onMessage = (...args) => {
|
|
1058
|
+
const payload = args.length > 0 ? args[0] : void 0;
|
|
1059
|
+
let parsed;
|
|
1060
|
+
try {
|
|
1061
|
+
parsed = parseJsonSafe(toMessageString(payload));
|
|
1062
|
+
} catch {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
if (!isRecord(parsed)) return;
|
|
1066
|
+
const responseId = typeof parsed.id === "string" || typeof parsed.id === "number" ? String(parsed.id) : "";
|
|
1067
|
+
if (responseId !== requestId) return;
|
|
1068
|
+
settleResolve(parsed);
|
|
1069
|
+
};
|
|
1070
|
+
const onError = () => {
|
|
1071
|
+
settleReject(new Error("[relai-x402] WebSocket relay transport error"));
|
|
1072
|
+
};
|
|
1073
|
+
const onClose = () => {
|
|
1074
|
+
settleReject(new Error("[relai-x402] WebSocket relay connection closed before response"));
|
|
1075
|
+
};
|
|
1076
|
+
addSocketListener(socket, "open", onOpen);
|
|
1077
|
+
addSocketListener(socket, "message", onMessage);
|
|
1078
|
+
addSocketListener(socket, "error", onError);
|
|
1079
|
+
addSocketListener(socket, "close", onClose);
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
function extractPaymentRequirementsFromWsError(error) {
|
|
1083
|
+
const candidates = [error.paymentRequired, error.data];
|
|
1084
|
+
for (const candidate of candidates) {
|
|
1085
|
+
if (!isRecord(candidate)) continue;
|
|
1086
|
+
if (Array.isArray(candidate.accepts)) {
|
|
1087
|
+
return candidate;
|
|
1088
|
+
}
|
|
1089
|
+
if (isRecord(candidate.paymentRequired) && Array.isArray(candidate.paymentRequired.accepts)) {
|
|
1090
|
+
return candidate.paymentRequired;
|
|
1091
|
+
}
|
|
1092
|
+
if (isRecord(candidate.data) && Array.isArray(candidate.data.accepts)) {
|
|
1093
|
+
return candidate.data;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
function buildWsResponse(wsResponse) {
|
|
1099
|
+
const statusFromMetadata = isRecord(wsResponse.metadata) && typeof wsResponse.metadata.status === "number" ? wsResponse.metadata.status : 200;
|
|
1100
|
+
const status = Number.isInteger(statusFromMetadata) && statusFromMetadata >= 100 && statusFromMetadata <= 599 ? statusFromMetadata : 200;
|
|
1101
|
+
const headers = new Headers();
|
|
1102
|
+
headers.set("Content-Type", "application/json");
|
|
1103
|
+
if (wsResponse.paymentResponse !== void 0) {
|
|
1104
|
+
headers.set("PAYMENT-RESPONSE", encodeBase64Json(wsResponse.paymentResponse));
|
|
1105
|
+
}
|
|
1106
|
+
const bodyPayload = wsResponse.result === void 0 ? null : wsResponse.result;
|
|
1107
|
+
return new Response(JSON.stringify(bodyPayload), {
|
|
1108
|
+
status,
|
|
1109
|
+
headers
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
440
1112
|
function selectAccept(accepts) {
|
|
441
1113
|
if (preferredNetwork) {
|
|
442
1114
|
const caip2 = NETWORK_CAIP2[preferredNetwork];
|
|
@@ -548,7 +1220,7 @@ function createX402Client(config) {
|
|
|
548
1220
|
amount: paymentAmount
|
|
549
1221
|
}
|
|
550
1222
|
};
|
|
551
|
-
return
|
|
1223
|
+
return encodeBase64Json(paymentPayload);
|
|
552
1224
|
}
|
|
553
1225
|
async function buildEvmPayment(accept, requirements, url) {
|
|
554
1226
|
const evmWallet = effectiveWallets.evm;
|
|
@@ -616,7 +1288,7 @@ function createX402Client(config) {
|
|
|
616
1288
|
},
|
|
617
1289
|
facilitatorUrl
|
|
618
1290
|
};
|
|
619
|
-
return
|
|
1291
|
+
return encodeBase64Json(paymentPayload);
|
|
620
1292
|
}
|
|
621
1293
|
async function buildSolanaPayment(accept, requirements, url) {
|
|
622
1294
|
const solWallet = effectiveWallets.solana;
|
|
@@ -680,7 +1352,17 @@ function createX402Client(config) {
|
|
|
680
1352
|
transaction: serializedTx
|
|
681
1353
|
}
|
|
682
1354
|
};
|
|
683
|
-
return
|
|
1355
|
+
return encodeBase64Json(paymentPayload);
|
|
1356
|
+
}
|
|
1357
|
+
function encodeBase64Json(payload) {
|
|
1358
|
+
const serialized = JSON.stringify(payload);
|
|
1359
|
+
if (typeof Buffer !== "undefined") {
|
|
1360
|
+
return Buffer.from(serialized, "utf8").toString("base64");
|
|
1361
|
+
}
|
|
1362
|
+
if (typeof btoa !== "undefined") {
|
|
1363
|
+
return btoa(serialized);
|
|
1364
|
+
}
|
|
1365
|
+
throw new Error("[relai-x402] Base64 encoding is not available in this runtime");
|
|
684
1366
|
}
|
|
685
1367
|
function decodeBase64Json(encoded) {
|
|
686
1368
|
try {
|
|
@@ -721,7 +1403,101 @@ function createX402Client(config) {
|
|
|
721
1403
|
async function x402Fetch(input, init) {
|
|
722
1404
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
723
1405
|
log("Request:", url);
|
|
724
|
-
const
|
|
1406
|
+
const requestInit = stripInternalInit(init);
|
|
1407
|
+
const integritasOptions = resolveIntegritasOptions(init?.x402?.integritas);
|
|
1408
|
+
const requestMethod = getRequestMethod(input, requestInit);
|
|
1409
|
+
const requestHeaders = applyIntegritasHeaders(
|
|
1410
|
+
getRequestHeaders(input, requestInit),
|
|
1411
|
+
integritasOptions
|
|
1412
|
+
);
|
|
1413
|
+
const requestInitWithHeaders = {
|
|
1414
|
+
...requestInit || {},
|
|
1415
|
+
headers: requestHeaders
|
|
1416
|
+
};
|
|
1417
|
+
const requestBody = await resolveRequestBody(input, requestInitWithHeaders);
|
|
1418
|
+
if (relayWsEnabled && isRelayRequestUrl(url)) {
|
|
1419
|
+
let wsPaymentPhaseStarted = false;
|
|
1420
|
+
try {
|
|
1421
|
+
log("Using WebSocket relay transport");
|
|
1422
|
+
const wsPreflightResponse = await relayCallOverWebSocket({
|
|
1423
|
+
relayUrl: url,
|
|
1424
|
+
requestMethod,
|
|
1425
|
+
requestHeaders,
|
|
1426
|
+
requestBody,
|
|
1427
|
+
timeoutMs: relayWsPreflightTimeoutMs
|
|
1428
|
+
});
|
|
1429
|
+
if (!wsPreflightResponse.error) {
|
|
1430
|
+
return buildWsResponse(wsPreflightResponse);
|
|
1431
|
+
}
|
|
1432
|
+
if (Number(wsPreflightResponse.error.code) !== 402) {
|
|
1433
|
+
throw new Error(wsPreflightResponse.error.message || "[relai-x402] WebSocket relay request failed");
|
|
1434
|
+
}
|
|
1435
|
+
const wsRequirements = extractPaymentRequirementsFromWsError(wsPreflightResponse.error);
|
|
1436
|
+
if (!wsRequirements) {
|
|
1437
|
+
throw new Error(
|
|
1438
|
+
wsPreflightResponse.error.message || "[relai-x402] No payment requirements in WS 402 response"
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
const wsAccepts = getAccepts(wsRequirements);
|
|
1442
|
+
if (!wsAccepts.length) {
|
|
1443
|
+
throw new Error("[relai-x402] No payment options in WS 402 response");
|
|
1444
|
+
}
|
|
1445
|
+
if (wsAccepts.length > 1) {
|
|
1446
|
+
throw new Error(
|
|
1447
|
+
"[relai-x402] WS relay currently supports a single payment payload; use HTTP flow for multi-accept payments"
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
const wsSelected = selectAccept(wsAccepts);
|
|
1451
|
+
if (!wsSelected) {
|
|
1452
|
+
const networks = wsAccepts.map((a) => a.network).join(", ");
|
|
1453
|
+
throw new Error(`[relai-x402] No wallet available for WS networks: ${networks}`);
|
|
1454
|
+
}
|
|
1455
|
+
const { accept: accept2, chain: chain2 } = wsSelected;
|
|
1456
|
+
const amount2 = accept2.amount || accept2.maxAmountRequired;
|
|
1457
|
+
if (maxAmountAtomic && BigInt(amount2) > BigInt(maxAmountAtomic)) {
|
|
1458
|
+
throw new Error(`[relai-x402] Amount ${amount2} exceeds max ${maxAmountAtomic}`);
|
|
1459
|
+
}
|
|
1460
|
+
wsPaymentPhaseStarted = true;
|
|
1461
|
+
let paymentHeader = null;
|
|
1462
|
+
if (chain2 === "solana" && hasSolanaWallet) {
|
|
1463
|
+
paymentHeader = await buildSolanaPayment(accept2, wsRequirements, url);
|
|
1464
|
+
} else if (chain2 === "evm") {
|
|
1465
|
+
const evmNetwork = normalizeNetwork(accept2.network || "");
|
|
1466
|
+
const usePermit = evmNetwork && PERMIT_NETWORKS.has(evmNetwork);
|
|
1467
|
+
paymentHeader = usePermit ? await buildEvmPermitPayment(accept2, wsRequirements, url) : await buildEvmPayment(accept2, wsRequirements, url);
|
|
1468
|
+
}
|
|
1469
|
+
if (!paymentHeader) {
|
|
1470
|
+
throw new Error("[relai-x402] Unexpected state - no WS payment handler matched");
|
|
1471
|
+
}
|
|
1472
|
+
const paymentPayload = decodeBase64Json(paymentHeader);
|
|
1473
|
+
if (!paymentPayload) {
|
|
1474
|
+
throw new Error("[relai-x402] Failed to decode payment payload for WS relay call");
|
|
1475
|
+
}
|
|
1476
|
+
const wsPaidResponse = await relayCallOverWebSocket({
|
|
1477
|
+
relayUrl: url,
|
|
1478
|
+
requestMethod,
|
|
1479
|
+
requestHeaders,
|
|
1480
|
+
requestBody,
|
|
1481
|
+
paymentPayload,
|
|
1482
|
+
timeoutMs: relayWsPaymentTimeoutMs
|
|
1483
|
+
});
|
|
1484
|
+
if (wsPaidResponse.error) {
|
|
1485
|
+
throw new Error(wsPaidResponse.error.message || "[relai-x402] WebSocket paid relay request failed");
|
|
1486
|
+
}
|
|
1487
|
+
return buildWsResponse(wsPaidResponse);
|
|
1488
|
+
} catch (wsError) {
|
|
1489
|
+
const wsMessage = wsError instanceof Error ? wsError.message : String(wsError);
|
|
1490
|
+
log("WebSocket relay transport failed:", wsMessage);
|
|
1491
|
+
if (wsPaymentPhaseStarted) {
|
|
1492
|
+
throw wsError instanceof Error ? wsError : new Error(`[relai-x402] ${wsMessage}`);
|
|
1493
|
+
}
|
|
1494
|
+
if (!relayWsFallbackToHttp) {
|
|
1495
|
+
throw wsError instanceof Error ? wsError : new Error(`[relai-x402] ${wsMessage}`);
|
|
1496
|
+
}
|
|
1497
|
+
log("Falling back to HTTP x402 flow");
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
const response = await fetch(input, requestInitWithHeaders);
|
|
725
1501
|
if (response.status !== 402) return response;
|
|
726
1502
|
log("Got 402 Payment Required");
|
|
727
1503
|
let requirementsFromBody = null;
|
|
@@ -759,9 +1535,9 @@ function createX402Client(config) {
|
|
|
759
1535
|
const paymentHeader = await buildSolanaPayment(accept, requirements, url);
|
|
760
1536
|
log("Retrying with X-PAYMENT header (Solana)");
|
|
761
1537
|
return fetch(input, {
|
|
762
|
-
...
|
|
1538
|
+
...requestInitWithHeaders,
|
|
763
1539
|
headers: {
|
|
764
|
-
...
|
|
1540
|
+
...requestHeaders,
|
|
765
1541
|
"X-PAYMENT": paymentHeader
|
|
766
1542
|
}
|
|
767
1543
|
});
|
|
@@ -772,14 +1548,14 @@ function createX402Client(config) {
|
|
|
772
1548
|
const paymentHeader = usePermit ? await buildEvmPermitPayment(accept, requirements, url) : await buildEvmPayment(accept, requirements, url);
|
|
773
1549
|
log("Retrying with X-PAYMENT header");
|
|
774
1550
|
return fetch(input, {
|
|
775
|
-
...
|
|
1551
|
+
...requestInitWithHeaders,
|
|
776
1552
|
headers: {
|
|
777
|
-
...
|
|
1553
|
+
...requestHeaders,
|
|
778
1554
|
"X-PAYMENT": paymentHeader
|
|
779
1555
|
}
|
|
780
1556
|
});
|
|
781
1557
|
}
|
|
782
|
-
throw new Error("[relai-x402] Unexpected state
|
|
1558
|
+
throw new Error("[relai-x402] Unexpected state - no payment handler matched");
|
|
783
1559
|
}
|
|
784
1560
|
return { fetch: x402Fetch };
|
|
785
1561
|
}
|
|
@@ -929,6 +1705,7 @@ function formatUsd(usd, maxDecimals = 4) {
|
|
|
929
1705
|
EXPLORER_TX_URL,
|
|
930
1706
|
NETWORK_CAIP2,
|
|
931
1707
|
NETWORK_LABELS,
|
|
1708
|
+
NETWORK_TOKENS,
|
|
932
1709
|
NETWORK_V1_TO_V2,
|
|
933
1710
|
NETWORK_V2_TO_V1,
|
|
934
1711
|
RELAI_FACILITATOR_URL,
|
|
@@ -953,6 +1730,7 @@ function formatUsd(usd, maxDecimals = 4) {
|
|
|
953
1730
|
networkV2ToV1,
|
|
954
1731
|
normalizeNetwork,
|
|
955
1732
|
normalizePaymentHeader,
|
|
1733
|
+
resolveToken,
|
|
956
1734
|
stripePayTo,
|
|
957
1735
|
toAtomicUnits
|
|
958
1736
|
});
|