@relai-fi/x402 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +314 -0
- package/dist/client.cjs +393 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +27 -0
- package/dist/client.d.ts +27 -0
- package/dist/client.js +379 -0
- package/dist/client.js.map +1 -0
- package/dist/index.cjs +720 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +664 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +519 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +80 -0
- package/dist/react/index.d.ts +80 -0
- package/dist/react/index.js +503 -0
- package/dist/react/index.js.map +1 -0
- package/dist/server.cjs +171 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +18 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.js +136 -0
- package/dist/server.js.map +1 -0
- package/dist/types-DGRfrYd3.d.cts +124 -0
- package/dist/types-DGRfrYd3.d.ts +124 -0
- package/dist/utils/index.cjs +192 -0
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +136 -0
- package/dist/utils/index.d.ts +136 -0
- package/dist/utils/index.js +152 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +92 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
BASE_MAINNET_NETWORK: () => BASE_MAINNET_NETWORK,
|
|
34
|
+
CAIP2_TO_NETWORK: () => CAIP2_TO_NETWORK,
|
|
35
|
+
CHAIN_IDS: () => CHAIN_IDS,
|
|
36
|
+
EXPLORER_TX_URL: () => EXPLORER_TX_URL,
|
|
37
|
+
NETWORK_CAIP2: () => NETWORK_CAIP2,
|
|
38
|
+
NETWORK_LABELS: () => NETWORK_LABELS,
|
|
39
|
+
NETWORK_V1_TO_V2: () => NETWORK_V1_TO_V2,
|
|
40
|
+
NETWORK_V2_TO_V1: () => NETWORK_V2_TO_V1,
|
|
41
|
+
RELAI_FACILITATOR_URL: () => RELAI_FACILITATOR_URL,
|
|
42
|
+
RELAI_NETWORKS: () => RELAI_NETWORKS,
|
|
43
|
+
Relai: () => server_default,
|
|
44
|
+
SOLANA_MAINNET_NETWORK: () => SOLANA_MAINNET_NETWORK,
|
|
45
|
+
USDC_ADDRESSES: () => USDC_ADDRESSES,
|
|
46
|
+
USDC_BASE: () => USDC_BASE,
|
|
47
|
+
USDC_SOLANA: () => USDC_SOLANA,
|
|
48
|
+
convertPayloadToVersion: () => convertPayloadToVersion,
|
|
49
|
+
convertV1ToV2: () => convertV1ToV2,
|
|
50
|
+
convertV2ToV1: () => convertV2ToV1,
|
|
51
|
+
createX402Client: () => createX402Client,
|
|
52
|
+
default: () => Relai,
|
|
53
|
+
detectPayloadVersion: () => detectPayloadVersion,
|
|
54
|
+
formatUsd: () => formatUsd,
|
|
55
|
+
fromAtomicUnits: () => fromAtomicUnits,
|
|
56
|
+
isEvm: () => isEvm,
|
|
57
|
+
isEvmNetwork: () => isEvmNetwork,
|
|
58
|
+
isSolana: () => isSolana,
|
|
59
|
+
isSolanaNetwork: () => isSolanaNetwork,
|
|
60
|
+
networkV1ToV2: () => networkV1ToV2,
|
|
61
|
+
networkV2ToV1: () => networkV2ToV1,
|
|
62
|
+
normalizeNetwork: () => normalizeNetwork,
|
|
63
|
+
normalizePaymentHeader: () => normalizePaymentHeader,
|
|
64
|
+
toAtomicUnits: () => toAtomicUnits
|
|
65
|
+
});
|
|
66
|
+
module.exports = __toCommonJS(index_exports);
|
|
67
|
+
|
|
68
|
+
// src/server.ts
|
|
69
|
+
var import_axios = __toESM(require("axios"), 1);
|
|
70
|
+
|
|
71
|
+
// src/types.ts
|
|
72
|
+
var RELAI_FACILITATOR_URL = "https://facilitator.x402.fi";
|
|
73
|
+
var NETWORK_CAIP2 = {
|
|
74
|
+
"solana": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
75
|
+
"base": "eip155:8453",
|
|
76
|
+
"avalanche": "eip155:43114",
|
|
77
|
+
"skale-base": "eip155:1187947933"
|
|
78
|
+
};
|
|
79
|
+
var CAIP2_TO_NETWORK = Object.fromEntries(
|
|
80
|
+
Object.entries(NETWORK_CAIP2).map(([k, v]) => [v, k])
|
|
81
|
+
);
|
|
82
|
+
var CHAIN_IDS = {
|
|
83
|
+
"base": 8453,
|
|
84
|
+
"avalanche": 43114,
|
|
85
|
+
"skale-base": 1187947933
|
|
86
|
+
};
|
|
87
|
+
var USDC_ADDRESSES = {
|
|
88
|
+
"solana": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
89
|
+
"base": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
90
|
+
"avalanche": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
|
|
91
|
+
"skale-base": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20"
|
|
92
|
+
};
|
|
93
|
+
var EXPLORER_TX_URL = {
|
|
94
|
+
"solana": (tx) => `https://solscan.io/tx/${tx}`,
|
|
95
|
+
"base": (tx) => `https://basescan.org/tx/${tx}`,
|
|
96
|
+
"avalanche": (tx) => `https://snowtrace.io/tx/${tx}`,
|
|
97
|
+
"skale-base": (tx) => `https://skale-base-explorer.skalenodes.com/tx/${tx}`
|
|
98
|
+
};
|
|
99
|
+
var NETWORK_LABELS = {
|
|
100
|
+
"solana": "Solana",
|
|
101
|
+
"base": "Base",
|
|
102
|
+
"avalanche": "Avalanche",
|
|
103
|
+
"skale-base": "SKALE Base"
|
|
104
|
+
};
|
|
105
|
+
var SOLANA_MAINNET_NETWORK = NETWORK_CAIP2["solana"];
|
|
106
|
+
var BASE_MAINNET_NETWORK = NETWORK_CAIP2["base"];
|
|
107
|
+
var USDC_SOLANA = USDC_ADDRESSES["solana"];
|
|
108
|
+
var USDC_BASE = USDC_ADDRESSES["base"];
|
|
109
|
+
var RELAI_NETWORKS = ["solana", "base", "avalanche", "skale-base"];
|
|
110
|
+
function isSolana(network) {
|
|
111
|
+
return network === "solana" || network.startsWith("solana:");
|
|
112
|
+
}
|
|
113
|
+
function isEvm(network) {
|
|
114
|
+
return ["base", "avalanche", "skale-base"].includes(network) || network.startsWith("eip155:");
|
|
115
|
+
}
|
|
116
|
+
function normalizeNetwork(network) {
|
|
117
|
+
if (RELAI_NETWORKS.includes(network)) return network;
|
|
118
|
+
const fromCaip2 = CAIP2_TO_NETWORK[network];
|
|
119
|
+
if (fromCaip2) return fromCaip2;
|
|
120
|
+
if (network.startsWith("solana:")) return "solana";
|
|
121
|
+
if (network.startsWith("eip155:")) {
|
|
122
|
+
const chainId = parseInt(network.split(":")[1]);
|
|
123
|
+
const entry = Object.entries(CHAIN_IDS).find(([, id]) => id === chainId);
|
|
124
|
+
if (entry) return entry[0];
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/server.ts
|
|
130
|
+
var Relai = class {
|
|
131
|
+
constructor(config) {
|
|
132
|
+
this.config = {
|
|
133
|
+
network: "solana",
|
|
134
|
+
apiBaseUrl: "https://relai.fi/api",
|
|
135
|
+
facilitatorUrl: RELAI_FACILITATOR_URL,
|
|
136
|
+
...config
|
|
137
|
+
};
|
|
138
|
+
if (!this.config.apiKey || typeof this.config.apiKey !== "string") {
|
|
139
|
+
throw new Error("[Relai] Missing required apiKey in configuration");
|
|
140
|
+
}
|
|
141
|
+
this.client = import_axios.default.create({
|
|
142
|
+
baseURL: this.config.apiBaseUrl,
|
|
143
|
+
timeout: 1e4,
|
|
144
|
+
headers: {
|
|
145
|
+
"Authorization": `Bearer ${this.config.apiKey}`,
|
|
146
|
+
"Content-Type": "application/json"
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Express middleware to protect an endpoint with x402 micropayments.
|
|
152
|
+
* Returns a 402 with `accepts` for the configured network.
|
|
153
|
+
* Supports: Solana, Base, Avalanche, SKALE Base.
|
|
154
|
+
*/
|
|
155
|
+
protect(options) {
|
|
156
|
+
return async (req, res, next) => {
|
|
157
|
+
try {
|
|
158
|
+
const resolvedPrice = typeof options.price === "function" ? await options.price(req) : options.price;
|
|
159
|
+
if (typeof resolvedPrice !== "number" || !isFinite(resolvedPrice) || resolvedPrice <= 0) {
|
|
160
|
+
return res.status(400).json({ error: "Invalid price configuration", code: "INVALID_PRICE" });
|
|
161
|
+
}
|
|
162
|
+
const paymentSignature = req.headers["x-payment-signature"] || req.headers["x-relai-payment"];
|
|
163
|
+
if (!paymentSignature) {
|
|
164
|
+
const network = this.config.network;
|
|
165
|
+
const caip2 = NETWORK_CAIP2[network] || NETWORK_CAIP2["solana"];
|
|
166
|
+
const asset = USDC_ADDRESSES[network] || USDC_ADDRESSES["solana"];
|
|
167
|
+
return res.status(402).json({
|
|
168
|
+
x402Version: 2,
|
|
169
|
+
error: "Payment required",
|
|
170
|
+
resource: {
|
|
171
|
+
url: `${req.protocol}://${req.get("host")}${req.originalUrl}`,
|
|
172
|
+
description: options.description || "API access",
|
|
173
|
+
mimeType: "application/json"
|
|
174
|
+
},
|
|
175
|
+
accepts: [{
|
|
176
|
+
scheme: "exact",
|
|
177
|
+
network: caip2,
|
|
178
|
+
maxAmountRequired: String(Math.floor(resolvedPrice * 1e6)),
|
|
179
|
+
asset,
|
|
180
|
+
payTo: options.payTo,
|
|
181
|
+
maxTimeoutSeconds: 60,
|
|
182
|
+
extra: {
|
|
183
|
+
name: "USD Coin",
|
|
184
|
+
version: "2",
|
|
185
|
+
decimals: 6,
|
|
186
|
+
facilitatorUrl: this.config.facilitatorUrl
|
|
187
|
+
}
|
|
188
|
+
}]
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const verification = await this.verifyPayment(paymentSignature, resolvedPrice, options.maxTimeout);
|
|
192
|
+
if (!verification.verified) {
|
|
193
|
+
return res.status(402).json({
|
|
194
|
+
error: "Payment verification failed",
|
|
195
|
+
code: "PAYMENT_INVALID",
|
|
196
|
+
details: verification.error
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
req.payment = verification;
|
|
200
|
+
if (options.customRules) {
|
|
201
|
+
const customValid = await options.customRules(req);
|
|
202
|
+
if (!customValid) {
|
|
203
|
+
return res.status(403).json({ error: "Custom validation failed", code: "VALIDATION_FAILED" });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
next();
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error("Relai protection error:", error);
|
|
209
|
+
res.status(500).json({ error: "Internal server error", code: "SERVER_ERROR" });
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async verifyPayment(signature, expectedPrice, timeoutMs) {
|
|
214
|
+
try {
|
|
215
|
+
const response = await this.client.post(
|
|
216
|
+
"/verify-payment",
|
|
217
|
+
{ signature, expectedPrice, network: this.config.network },
|
|
218
|
+
{ timeout: typeof timeoutMs === "number" ? Math.max(1, timeoutMs) : void 0 }
|
|
219
|
+
);
|
|
220
|
+
return response.data;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return { verified: false, error: error.response?.data?.error || "Verification failed" };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async getStats(apiId) {
|
|
226
|
+
const params = apiId ? { apiId } : {};
|
|
227
|
+
const response = await this.client.get("/stats", { params });
|
|
228
|
+
return response.data;
|
|
229
|
+
}
|
|
230
|
+
createProtectedEndpoint(options) {
|
|
231
|
+
return this.protect(options);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
var server_default = Relai;
|
|
235
|
+
|
|
236
|
+
// src/client.ts
|
|
237
|
+
var import_web3 = require("@solana/web3.js");
|
|
238
|
+
var import_spl_token = require("@solana/spl-token");
|
|
239
|
+
var PERMIT_NETWORKS = /* @__PURE__ */ new Set([]);
|
|
240
|
+
var DEFAULT_EVM_RPC_URLS = {
|
|
241
|
+
"skale-base": "https://skale-base.skalenodes.com/v1/base",
|
|
242
|
+
"base": "https://mainnet.base.org",
|
|
243
|
+
"avalanche": "https://api.avax.network/ext/bc/C/rpc"
|
|
244
|
+
};
|
|
245
|
+
function createX402Client(config) {
|
|
246
|
+
const {
|
|
247
|
+
wallets = {},
|
|
248
|
+
wallet: legacyWallet,
|
|
249
|
+
facilitatorUrl = RELAI_FACILITATOR_URL,
|
|
250
|
+
preferredNetwork,
|
|
251
|
+
solanaRpcUrl = "https://api.mainnet-beta.solana.com",
|
|
252
|
+
evmRpcUrls = {},
|
|
253
|
+
maxAmountAtomic,
|
|
254
|
+
verbose = false
|
|
255
|
+
} = config;
|
|
256
|
+
const log = verbose ? console.log.bind(console, "[relai-x402]") : () => {
|
|
257
|
+
};
|
|
258
|
+
const effectiveWallets = { ...wallets };
|
|
259
|
+
if (legacyWallet && !effectiveWallets.solana) {
|
|
260
|
+
effectiveWallets.solana = legacyWallet;
|
|
261
|
+
}
|
|
262
|
+
const hasSolanaWallet = Boolean(
|
|
263
|
+
effectiveWallets.solana?.publicKey && effectiveWallets.solana?.signTransaction
|
|
264
|
+
);
|
|
265
|
+
if (hasSolanaWallet) log("Solana wallet ready");
|
|
266
|
+
function selectAccept(accepts) {
|
|
267
|
+
if (preferredNetwork) {
|
|
268
|
+
const caip2 = NETWORK_CAIP2[preferredNetwork];
|
|
269
|
+
for (const a of accepts) {
|
|
270
|
+
const net = a.network || "";
|
|
271
|
+
if (net === preferredNetwork || net === caip2) {
|
|
272
|
+
const chain = isSolana(net) ? "solana" : "evm";
|
|
273
|
+
if (chain === "solana" && hasSolanaWallet || chain === "evm" && effectiveWallets.evm) {
|
|
274
|
+
return { accept: a, chain };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const a of accepts) {
|
|
280
|
+
const net = a.network || "";
|
|
281
|
+
if (isSolana(net) && hasSolanaWallet) return { accept: a, chain: "solana" };
|
|
282
|
+
if (isEvm(net) && effectiveWallets.evm) return { accept: a, chain: "evm" };
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
async function evmRpcCall(rpcUrl, to, data) {
|
|
287
|
+
const res = await fetch(rpcUrl, {
|
|
288
|
+
method: "POST",
|
|
289
|
+
headers: { "Content-Type": "application/json" },
|
|
290
|
+
body: JSON.stringify({
|
|
291
|
+
jsonrpc: "2.0",
|
|
292
|
+
method: "eth_call",
|
|
293
|
+
params: [{ to, data }, "latest"],
|
|
294
|
+
id: 1
|
|
295
|
+
})
|
|
296
|
+
});
|
|
297
|
+
const json = await res.json();
|
|
298
|
+
if (json.error) throw new Error(`RPC error: ${json.error.message}`);
|
|
299
|
+
return json.result;
|
|
300
|
+
}
|
|
301
|
+
function getEvmRpcUrl(network) {
|
|
302
|
+
return evmRpcUrls[network] || DEFAULT_EVM_RPC_URLS[network] || "";
|
|
303
|
+
}
|
|
304
|
+
async function buildEvmPermitPayment(accept, requirements, url) {
|
|
305
|
+
const evmWallet = effectiveWallets.evm;
|
|
306
|
+
const extra = accept.extra || {};
|
|
307
|
+
const rawNetwork = accept.network || "";
|
|
308
|
+
const network = normalizeNetwork(rawNetwork);
|
|
309
|
+
const chainId = network ? CHAIN_IDS[network] : parseInt(rawNetwork.split(":")[1] || "8453");
|
|
310
|
+
const paymentAmount = accept.amount || accept.maxAmountRequired;
|
|
311
|
+
const spender = extra.feePayer || accept.payTo;
|
|
312
|
+
const usdcAddress = accept.asset;
|
|
313
|
+
const rpcUrl = getEvmRpcUrl(network || rawNetwork);
|
|
314
|
+
if (!rpcUrl) throw new Error(`[relai-x402] No EVM RPC URL for network ${network || rawNetwork}`);
|
|
315
|
+
log("Building EIP-2612 permit on chain", chainId);
|
|
316
|
+
const paddedAddress = evmWallet.address.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
317
|
+
const nonceHex = await evmRpcCall(rpcUrl, usdcAddress, "0x7ecebe00" + paddedAddress);
|
|
318
|
+
const nonce = nonceHex ? parseInt(nonceHex, 16) : 0;
|
|
319
|
+
if (isNaN(nonce)) throw new Error(`[relai-x402] Failed to read permit nonce from ${usdcAddress} on ${rpcUrl}`);
|
|
320
|
+
log(" Permit nonce:", nonce);
|
|
321
|
+
const nameHex = await evmRpcCall(rpcUrl, usdcAddress, "0x06fdde03");
|
|
322
|
+
let tokenName = "USD Coin";
|
|
323
|
+
try {
|
|
324
|
+
const offset = parseInt(nameHex.slice(2, 66), 16) * 2;
|
|
325
|
+
const length = parseInt(nameHex.slice(2 + offset, 2 + offset + 64), 16);
|
|
326
|
+
const hex = nameHex.slice(2 + offset + 64, 2 + offset + 64 + length * 2);
|
|
327
|
+
tokenName = decodeURIComponent(hex.replace(/[0-9a-f]{2}/g, "%$&"));
|
|
328
|
+
} catch {
|
|
329
|
+
tokenName = extra.name || "USD Coin";
|
|
330
|
+
}
|
|
331
|
+
log(" Token name:", tokenName);
|
|
332
|
+
const deadline = Math.floor(Date.now() / 1e3) + 600;
|
|
333
|
+
const domain = {
|
|
334
|
+
name: tokenName,
|
|
335
|
+
version: extra.version || "2",
|
|
336
|
+
chainId,
|
|
337
|
+
verifyingContract: usdcAddress
|
|
338
|
+
};
|
|
339
|
+
const types = {
|
|
340
|
+
Permit: [
|
|
341
|
+
{ name: "owner", type: "address" },
|
|
342
|
+
{ name: "spender", type: "address" },
|
|
343
|
+
{ name: "value", type: "uint256" },
|
|
344
|
+
{ name: "nonce", type: "uint256" },
|
|
345
|
+
{ name: "deadline", type: "uint256" }
|
|
346
|
+
]
|
|
347
|
+
};
|
|
348
|
+
const message = {
|
|
349
|
+
owner: evmWallet.address,
|
|
350
|
+
spender,
|
|
351
|
+
value: paymentAmount,
|
|
352
|
+
nonce: String(nonce),
|
|
353
|
+
deadline: String(deadline)
|
|
354
|
+
};
|
|
355
|
+
log("Signing EIP-2612 permit:", message);
|
|
356
|
+
const signature = await evmWallet.signTypedData({
|
|
357
|
+
domain,
|
|
358
|
+
types,
|
|
359
|
+
message,
|
|
360
|
+
primaryType: "Permit"
|
|
361
|
+
});
|
|
362
|
+
const sigHex = signature.replace("0x", "");
|
|
363
|
+
const r = "0x" + sigHex.slice(0, 64);
|
|
364
|
+
const s = "0x" + sigHex.slice(64, 128);
|
|
365
|
+
const v = parseInt(sigHex.slice(128, 130), 16);
|
|
366
|
+
log(" Permit signed: v=%d r=%s s=%s", v, r, s);
|
|
367
|
+
const paymentPayload = {
|
|
368
|
+
x402Version: 2,
|
|
369
|
+
scheme: "exact",
|
|
370
|
+
network: network || rawNetwork,
|
|
371
|
+
payload: {
|
|
372
|
+
userAddress: evmWallet.address,
|
|
373
|
+
permit: { deadline: String(deadline), v, r, s },
|
|
374
|
+
amount: paymentAmount
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
return btoa(JSON.stringify(paymentPayload));
|
|
378
|
+
}
|
|
379
|
+
async function buildEvmPayment(accept, requirements, url) {
|
|
380
|
+
const evmWallet = effectiveWallets.evm;
|
|
381
|
+
const extra = accept.extra || {};
|
|
382
|
+
const rawNetwork = accept.network || "";
|
|
383
|
+
const network = normalizeNetwork(rawNetwork);
|
|
384
|
+
const chainId = network ? CHAIN_IDS[network] : parseInt(rawNetwork.split(":")[1] || "8453");
|
|
385
|
+
const paymentAmount = accept.amount || accept.maxAmountRequired;
|
|
386
|
+
const domain = {
|
|
387
|
+
name: extra.name || "USD Coin",
|
|
388
|
+
version: extra.version || "2",
|
|
389
|
+
chainId,
|
|
390
|
+
verifyingContract: accept.asset
|
|
391
|
+
};
|
|
392
|
+
const validAfter = 0;
|
|
393
|
+
const validBefore = Math.floor(Date.now() / 1e3) + 3600;
|
|
394
|
+
const nonce = "0x" + [...crypto.getRandomValues(new Uint8Array(32))].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
395
|
+
const types = {
|
|
396
|
+
TransferWithAuthorization: [
|
|
397
|
+
{ name: "from", type: "address" },
|
|
398
|
+
{ name: "to", type: "address" },
|
|
399
|
+
{ name: "value", type: "uint256" },
|
|
400
|
+
{ name: "validAfter", type: "uint256" },
|
|
401
|
+
{ name: "validBefore", type: "uint256" },
|
|
402
|
+
{ name: "nonce", type: "bytes32" }
|
|
403
|
+
]
|
|
404
|
+
};
|
|
405
|
+
const spender = extra.feePayer || accept.payTo;
|
|
406
|
+
const message = {
|
|
407
|
+
from: evmWallet.address,
|
|
408
|
+
to: spender,
|
|
409
|
+
value: paymentAmount,
|
|
410
|
+
validAfter: String(validAfter),
|
|
411
|
+
validBefore: String(validBefore),
|
|
412
|
+
nonce
|
|
413
|
+
};
|
|
414
|
+
log("Signing EIP-3009 transferWithAuthorization on chain", chainId);
|
|
415
|
+
const signature = await evmWallet.signTypedData({
|
|
416
|
+
domain,
|
|
417
|
+
types,
|
|
418
|
+
message,
|
|
419
|
+
primaryType: "TransferWithAuthorization"
|
|
420
|
+
});
|
|
421
|
+
const paymentPayload = {
|
|
422
|
+
x402Version: 2,
|
|
423
|
+
resource: requirements.resource || { url },
|
|
424
|
+
accepted: accept,
|
|
425
|
+
payload: {
|
|
426
|
+
authorization: message,
|
|
427
|
+
signature
|
|
428
|
+
},
|
|
429
|
+
facilitatorUrl
|
|
430
|
+
};
|
|
431
|
+
return btoa(JSON.stringify(paymentPayload));
|
|
432
|
+
}
|
|
433
|
+
async function buildSolanaPayment(accept, requirements, url) {
|
|
434
|
+
const solWallet = effectiveWallets.solana;
|
|
435
|
+
const extra = accept.extra || {};
|
|
436
|
+
if (!extra.feePayer) {
|
|
437
|
+
throw new Error("[relai-x402] Missing feePayer in Solana payment requirements");
|
|
438
|
+
}
|
|
439
|
+
const connection = new import_web3.Connection(solanaRpcUrl, "confirmed");
|
|
440
|
+
const userPubkey = new import_web3.PublicKey(solWallet.publicKey.toString());
|
|
441
|
+
const merchantPubkey = new import_web3.PublicKey(accept.payTo);
|
|
442
|
+
const feePayerPubkey = new import_web3.PublicKey(extra.feePayer);
|
|
443
|
+
const mintPubkey = new import_web3.PublicKey(accept.asset);
|
|
444
|
+
const paymentAmount = BigInt(accept.amount || accept.maxAmountRequired);
|
|
445
|
+
log("Building Solana SPL transfer");
|
|
446
|
+
log(" User:", userPubkey.toBase58());
|
|
447
|
+
log(" Merchant:", merchantPubkey.toBase58());
|
|
448
|
+
log(" FeePayer:", feePayerPubkey.toBase58());
|
|
449
|
+
log(" Mint:", mintPubkey.toBase58());
|
|
450
|
+
log(" Amount:", paymentAmount.toString());
|
|
451
|
+
const mintInfo = await (0, import_spl_token.getMint)(connection, mintPubkey);
|
|
452
|
+
const programId = mintInfo.address.equals(mintPubkey) ? mintInfo.owner?.toBase58?.() === import_spl_token.TOKEN_2022_PROGRAM_ID.toBase58() ? import_spl_token.TOKEN_2022_PROGRAM_ID : import_spl_token.TOKEN_PROGRAM_ID : import_spl_token.TOKEN_PROGRAM_ID;
|
|
453
|
+
const sourceAta = await (0, import_spl_token.getAssociatedTokenAddress)(
|
|
454
|
+
mintPubkey,
|
|
455
|
+
userPubkey,
|
|
456
|
+
false,
|
|
457
|
+
programId
|
|
458
|
+
);
|
|
459
|
+
const destinationAta = await (0, import_spl_token.getAssociatedTokenAddress)(
|
|
460
|
+
mintPubkey,
|
|
461
|
+
merchantPubkey,
|
|
462
|
+
true,
|
|
463
|
+
programId
|
|
464
|
+
);
|
|
465
|
+
log(" Source ATA:", sourceAta.toBase58());
|
|
466
|
+
log(" Dest ATA:", destinationAta.toBase58());
|
|
467
|
+
const transferIx = (0, import_spl_token.createTransferCheckedInstruction)(
|
|
468
|
+
sourceAta,
|
|
469
|
+
mintPubkey,
|
|
470
|
+
destinationAta,
|
|
471
|
+
userPubkey,
|
|
472
|
+
paymentAmount,
|
|
473
|
+
mintInfo.decimals,
|
|
474
|
+
[],
|
|
475
|
+
programId
|
|
476
|
+
);
|
|
477
|
+
const { blockhash } = await connection.getLatestBlockhash("confirmed");
|
|
478
|
+
const message = new import_web3.TransactionMessage({
|
|
479
|
+
payerKey: feePayerPubkey,
|
|
480
|
+
recentBlockhash: blockhash,
|
|
481
|
+
instructions: [transferIx]
|
|
482
|
+
}).compileToV0Message();
|
|
483
|
+
const transaction = new import_web3.VersionedTransaction(message);
|
|
484
|
+
const signedTx = await solWallet.signTransaction(transaction);
|
|
485
|
+
log("Transaction signed by user");
|
|
486
|
+
const serializedTx = Buffer.from(signedTx.serialize()).toString("base64");
|
|
487
|
+
const paymentPayload = {
|
|
488
|
+
x402Version: 2,
|
|
489
|
+
resource: requirements.resource || { url },
|
|
490
|
+
accepted: accept,
|
|
491
|
+
payload: {
|
|
492
|
+
transaction: serializedTx
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
return btoa(JSON.stringify(paymentPayload));
|
|
496
|
+
}
|
|
497
|
+
async function x402Fetch(input, init) {
|
|
498
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
499
|
+
log("Request:", url);
|
|
500
|
+
const response = await fetch(input, init);
|
|
501
|
+
if (response.status !== 402) return response;
|
|
502
|
+
log("Got 402 Payment Required");
|
|
503
|
+
let requirements;
|
|
504
|
+
try {
|
|
505
|
+
requirements = await response.clone().json();
|
|
506
|
+
} catch {
|
|
507
|
+
throw new Error("[relai-x402] Failed to parse 402 response body");
|
|
508
|
+
}
|
|
509
|
+
const accepts = requirements.accepts || [];
|
|
510
|
+
if (!accepts.length) throw new Error("[relai-x402] No payment options in 402 response");
|
|
511
|
+
const selected = selectAccept(accepts);
|
|
512
|
+
if (!selected) {
|
|
513
|
+
const networks = accepts.map((a) => a.network).join(", ");
|
|
514
|
+
throw new Error(`[relai-x402] No wallet available for networks: ${networks}`);
|
|
515
|
+
}
|
|
516
|
+
const { accept, chain } = selected;
|
|
517
|
+
const amount = accept.amount || accept.maxAmountRequired;
|
|
518
|
+
log(`Selected: ${chain} / ${accept.network} / amount=${amount}`);
|
|
519
|
+
if (maxAmountAtomic && BigInt(amount) > BigInt(maxAmountAtomic)) {
|
|
520
|
+
throw new Error(`[relai-x402] Amount ${amount} exceeds max ${maxAmountAtomic}`);
|
|
521
|
+
}
|
|
522
|
+
if (chain === "solana" && hasSolanaWallet) {
|
|
523
|
+
const paymentHeader = await buildSolanaPayment(accept, requirements, url);
|
|
524
|
+
log("Retrying with X-PAYMENT header (Solana)");
|
|
525
|
+
return fetch(input, {
|
|
526
|
+
...init,
|
|
527
|
+
headers: {
|
|
528
|
+
...init?.headers || {},
|
|
529
|
+
"X-PAYMENT": paymentHeader
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
if (chain === "evm") {
|
|
534
|
+
const evmNetwork = normalizeNetwork(accept.network || "");
|
|
535
|
+
const usePermit = evmNetwork && PERMIT_NETWORKS.has(evmNetwork);
|
|
536
|
+
const paymentHeader = usePermit ? await buildEvmPermitPayment(accept, requirements, url) : await buildEvmPayment(accept, requirements, url);
|
|
537
|
+
log("Retrying with X-PAYMENT header");
|
|
538
|
+
return fetch(input, {
|
|
539
|
+
...init,
|
|
540
|
+
headers: {
|
|
541
|
+
...init?.headers || {},
|
|
542
|
+
"X-PAYMENT": paymentHeader
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
throw new Error("[relai-x402] Unexpected state \u2014 no payment handler matched");
|
|
547
|
+
}
|
|
548
|
+
return { fetch: x402Fetch };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/utils/payload-converter.ts
|
|
552
|
+
var NETWORK_V1_TO_V2 = {
|
|
553
|
+
"solana": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
554
|
+
"solana-devnet": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
|
|
555
|
+
"base": "eip155:8453",
|
|
556
|
+
"base-sepolia": "eip155:84532",
|
|
557
|
+
"ethereum": "eip155:1",
|
|
558
|
+
"polygon": "eip155:137",
|
|
559
|
+
"avalanche": "eip155:43114",
|
|
560
|
+
"skale-base": "eip155:1187947933",
|
|
561
|
+
"peaq": "eip155:3338",
|
|
562
|
+
"sei": "eip155:1329"
|
|
563
|
+
};
|
|
564
|
+
var NETWORK_V2_TO_V1 = Object.fromEntries(
|
|
565
|
+
Object.entries(NETWORK_V1_TO_V2).map(([k, v]) => [v, k])
|
|
566
|
+
);
|
|
567
|
+
function networkV2ToV1(caip2Network) {
|
|
568
|
+
if (!caip2Network) return "solana";
|
|
569
|
+
if (NETWORK_V2_TO_V1[caip2Network]) {
|
|
570
|
+
return NETWORK_V2_TO_V1[caip2Network];
|
|
571
|
+
}
|
|
572
|
+
if (caip2Network.startsWith("solana:")) {
|
|
573
|
+
const chainId = caip2Network.split(":")[1];
|
|
574
|
+
if (chainId === "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp") return "solana";
|
|
575
|
+
if (chainId === "EtWTRABZaYq6iMfeYKouRu166VU2xqa1") return "solana-devnet";
|
|
576
|
+
return "solana";
|
|
577
|
+
}
|
|
578
|
+
if (caip2Network.startsWith("eip155:")) {
|
|
579
|
+
const chainId = caip2Network.split(":")[1];
|
|
580
|
+
const mapping = {
|
|
581
|
+
"1": "ethereum",
|
|
582
|
+
"137": "polygon",
|
|
583
|
+
"8453": "base",
|
|
584
|
+
"84532": "base-sepolia",
|
|
585
|
+
"43114": "avalanche",
|
|
586
|
+
"1187947933": "skale-base",
|
|
587
|
+
"3338": "peaq",
|
|
588
|
+
"1329": "sei"
|
|
589
|
+
};
|
|
590
|
+
return mapping[chainId] || caip2Network;
|
|
591
|
+
}
|
|
592
|
+
return caip2Network;
|
|
593
|
+
}
|
|
594
|
+
function networkV1ToV2(v1Network) {
|
|
595
|
+
if (!v1Network) return "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
596
|
+
return NETWORK_V1_TO_V2[v1Network] || v1Network;
|
|
597
|
+
}
|
|
598
|
+
function detectPayloadVersion(payload) {
|
|
599
|
+
if (!payload || typeof payload !== "object") return null;
|
|
600
|
+
const p = payload;
|
|
601
|
+
if (p.x402Version === 1) return 1;
|
|
602
|
+
if (p.x402Version === 2) return 2;
|
|
603
|
+
if ("accepted" in p && "resource" in p) return 2;
|
|
604
|
+
if ("scheme" in p && "network" in p && !("accepted" in p)) return 1;
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
function convertV2ToV1(v2Payload) {
|
|
608
|
+
const accepted = v2Payload.accepted || {};
|
|
609
|
+
return {
|
|
610
|
+
x402Version: 1,
|
|
611
|
+
scheme: accepted.scheme || "exact",
|
|
612
|
+
network: networkV2ToV1(accepted.network),
|
|
613
|
+
payload: v2Payload.payload
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function convertV1ToV2(v1Payload, resourceInfo = {}) {
|
|
617
|
+
return {
|
|
618
|
+
x402Version: 2,
|
|
619
|
+
resource: {
|
|
620
|
+
url: resourceInfo.url || "",
|
|
621
|
+
description: resourceInfo.description || "",
|
|
622
|
+
mimeType: resourceInfo.mimeType || "application/json"
|
|
623
|
+
},
|
|
624
|
+
accepted: {
|
|
625
|
+
scheme: v1Payload.scheme || "exact",
|
|
626
|
+
network: networkV1ToV2(v1Payload.network)
|
|
627
|
+
},
|
|
628
|
+
payload: v1Payload.payload
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function convertPayloadToVersion(payload, targetVersion, options = {}) {
|
|
632
|
+
const sourceVersion = detectPayloadVersion(payload);
|
|
633
|
+
if (!sourceVersion) {
|
|
634
|
+
console.warn("[payload-converter] Could not detect source payload version");
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
if (sourceVersion === targetVersion) {
|
|
638
|
+
return payload;
|
|
639
|
+
}
|
|
640
|
+
if (sourceVersion === 2 && targetVersion === 1) {
|
|
641
|
+
return convertV2ToV1(payload);
|
|
642
|
+
}
|
|
643
|
+
if (sourceVersion === 1 && targetVersion === 2) {
|
|
644
|
+
return convertV1ToV2(payload, options.resourceInfo);
|
|
645
|
+
}
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
function normalizePaymentHeader(base64Header, targetVersion, options = {}) {
|
|
649
|
+
if (!base64Header) return null;
|
|
650
|
+
try {
|
|
651
|
+
const decoded = JSON.parse(
|
|
652
|
+
typeof window !== "undefined" ? atob(base64Header) : Buffer.from(base64Header, "base64").toString()
|
|
653
|
+
);
|
|
654
|
+
const converted = convertPayloadToVersion(decoded, targetVersion, options);
|
|
655
|
+
if (!converted) {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
const encoded = typeof window !== "undefined" ? btoa(JSON.stringify(converted)) : Buffer.from(JSON.stringify(converted)).toString("base64");
|
|
659
|
+
return {
|
|
660
|
+
header: encoded,
|
|
661
|
+
payload: converted
|
|
662
|
+
};
|
|
663
|
+
} catch (e) {
|
|
664
|
+
console.error("[payload-converter] Failed to normalize header:", e);
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function isSolanaNetwork(network) {
|
|
669
|
+
return network === "solana" || network === "solana-devnet" || network.startsWith("solana:");
|
|
670
|
+
}
|
|
671
|
+
function isEvmNetwork(network) {
|
|
672
|
+
const evmNetworks = ["base", "base-sepolia", "ethereum", "polygon", "avalanche", "skale-base", "peaq", "sei"];
|
|
673
|
+
return evmNetworks.includes(network) || network.startsWith("eip155:");
|
|
674
|
+
}
|
|
675
|
+
function toAtomicUnits(usd, decimals = 6) {
|
|
676
|
+
return Math.floor(usd * Math.pow(10, decimals)).toString();
|
|
677
|
+
}
|
|
678
|
+
function fromAtomicUnits(atomic, decimals = 6) {
|
|
679
|
+
const value = typeof atomic === "bigint" ? atomic : BigInt(atomic);
|
|
680
|
+
return Number(value) / Math.pow(10, decimals);
|
|
681
|
+
}
|
|
682
|
+
function formatUsd(usd, maxDecimals = 4) {
|
|
683
|
+
if (usd < 1e-4) return "<$0.0001";
|
|
684
|
+
return `$${usd.toFixed(maxDecimals).replace(/\.?0+$/, "")}`;
|
|
685
|
+
}
|
|
686
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
687
|
+
0 && (module.exports = {
|
|
688
|
+
BASE_MAINNET_NETWORK,
|
|
689
|
+
CAIP2_TO_NETWORK,
|
|
690
|
+
CHAIN_IDS,
|
|
691
|
+
EXPLORER_TX_URL,
|
|
692
|
+
NETWORK_CAIP2,
|
|
693
|
+
NETWORK_LABELS,
|
|
694
|
+
NETWORK_V1_TO_V2,
|
|
695
|
+
NETWORK_V2_TO_V1,
|
|
696
|
+
RELAI_FACILITATOR_URL,
|
|
697
|
+
RELAI_NETWORKS,
|
|
698
|
+
Relai,
|
|
699
|
+
SOLANA_MAINNET_NETWORK,
|
|
700
|
+
USDC_ADDRESSES,
|
|
701
|
+
USDC_BASE,
|
|
702
|
+
USDC_SOLANA,
|
|
703
|
+
convertPayloadToVersion,
|
|
704
|
+
convertV1ToV2,
|
|
705
|
+
convertV2ToV1,
|
|
706
|
+
createX402Client,
|
|
707
|
+
detectPayloadVersion,
|
|
708
|
+
formatUsd,
|
|
709
|
+
fromAtomicUnits,
|
|
710
|
+
isEvm,
|
|
711
|
+
isEvmNetwork,
|
|
712
|
+
isSolana,
|
|
713
|
+
isSolanaNetwork,
|
|
714
|
+
networkV1ToV2,
|
|
715
|
+
networkV2ToV1,
|
|
716
|
+
normalizeNetwork,
|
|
717
|
+
normalizePaymentHeader,
|
|
718
|
+
toAtomicUnits
|
|
719
|
+
});
|
|
720
|
+
//# sourceMappingURL=index.cjs.map
|