@wazabiai/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/LICENSE +21 -0
- package/README.md +356 -0
- package/dist/bnb-BhqGnGQ1.d.cts +98 -0
- package/dist/bnb-SixQcDqs.d.ts +98 -0
- package/dist/chains/index.cjs +147 -0
- package/dist/chains/index.cjs.map +1 -0
- package/dist/chains/index.d.cts +23 -0
- package/dist/chains/index.d.ts +23 -0
- package/dist/chains/index.js +121 -0
- package/dist/chains/index.js.map +1 -0
- package/dist/client/index.cjs +429 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +69 -0
- package/dist/client/index.d.ts +69 -0
- package/dist/client/index.js +412 -0
- package/dist/client/index.js.map +1 -0
- package/dist/index.cjs +803 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +750 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.cjs +474 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +58 -0
- package/dist/server/index.d.ts +58 -0
- package/dist/server/index.js +455 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types/index.cjs +158 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.cts +408 -0
- package/dist/types/index.d.ts +408 -0
- package/dist/types/index.js +141 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +121 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
var axios = require('axios');
|
|
5
|
+
var viem = require('viem');
|
|
6
|
+
var accounts = require('viem/accounts');
|
|
7
|
+
var chains = require('viem/chains');
|
|
8
|
+
|
|
9
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
var axios__default = /*#__PURE__*/_interopDefault(axios);
|
|
12
|
+
|
|
13
|
+
/* @wazabiai/x402 - x402 v2 Payment Protocol SDK */
|
|
14
|
+
|
|
15
|
+
var X402_VERSION = "2.0.0";
|
|
16
|
+
var X402_DOMAIN_NAME = "x402";
|
|
17
|
+
var X402_HEADERS = {
|
|
18
|
+
PAYMENT_REQUIRED: "x-payment-required",
|
|
19
|
+
PAYMENT_SIGNATURE: "x-payment-signature",
|
|
20
|
+
PAYMENT_PAYLOAD: "x-payment-payload"
|
|
21
|
+
};
|
|
22
|
+
var PaymentRequirementSchema = zod.z.object({
|
|
23
|
+
/** Payment amount in smallest token unit (wei/satoshi) as string */
|
|
24
|
+
amount: zod.z.string().regex(/^\d+$/, "Amount must be a numeric string"),
|
|
25
|
+
/** Token contract address */
|
|
26
|
+
token: zod.z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid token address"),
|
|
27
|
+
/** CAIP-2 network identifier */
|
|
28
|
+
network_id: zod.z.string().regex(/^eip155:\d+$/, "Invalid CAIP-2 network ID"),
|
|
29
|
+
/** Recipient address for payment */
|
|
30
|
+
pay_to: zod.z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid recipient address"),
|
|
31
|
+
/** Optional: Payment description */
|
|
32
|
+
description: zod.z.string().optional(),
|
|
33
|
+
/** Optional: Resource identifier being accessed */
|
|
34
|
+
resource: zod.z.string().optional(),
|
|
35
|
+
/** Optional: Expiration timestamp (Unix epoch seconds) */
|
|
36
|
+
expires_at: zod.z.number().int().positive().optional(),
|
|
37
|
+
/** Optional: Unique nonce to prevent replay attacks */
|
|
38
|
+
nonce: zod.z.string().optional(),
|
|
39
|
+
/** Protocol version */
|
|
40
|
+
version: zod.z.string().optional()
|
|
41
|
+
});
|
|
42
|
+
var PaymentPayloadSchema = zod.z.object({
|
|
43
|
+
/** Payment amount in smallest token unit */
|
|
44
|
+
amount: zod.z.string().regex(/^\d+$/, "Amount must be a numeric string"),
|
|
45
|
+
/** Token contract address */
|
|
46
|
+
token: zod.z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid token address"),
|
|
47
|
+
/** Chain ID (numeric) */
|
|
48
|
+
chainId: zod.z.number().int().positive(),
|
|
49
|
+
/** Recipient address */
|
|
50
|
+
payTo: zod.z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid recipient address"),
|
|
51
|
+
/** Payer address (signer) */
|
|
52
|
+
payer: zod.z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid payer address"),
|
|
53
|
+
/** Unix timestamp when payment expires */
|
|
54
|
+
deadline: zod.z.number().int().positive(),
|
|
55
|
+
/** Unique nonce to prevent replay attacks */
|
|
56
|
+
nonce: zod.z.string(),
|
|
57
|
+
/** Optional: Resource being accessed */
|
|
58
|
+
resource: zod.z.string().optional()
|
|
59
|
+
});
|
|
60
|
+
var PAYMENT_TYPES = {
|
|
61
|
+
Payment: [
|
|
62
|
+
{ name: "amount", type: "uint256" },
|
|
63
|
+
{ name: "token", type: "address" },
|
|
64
|
+
{ name: "chainId", type: "uint256" },
|
|
65
|
+
{ name: "payTo", type: "address" },
|
|
66
|
+
{ name: "payer", type: "address" },
|
|
67
|
+
{ name: "deadline", type: "uint256" },
|
|
68
|
+
{ name: "nonce", type: "string" },
|
|
69
|
+
{ name: "resource", type: "string" }
|
|
70
|
+
]
|
|
71
|
+
};
|
|
72
|
+
var SignedPaymentSchema = zod.z.object({
|
|
73
|
+
payload: PaymentPayloadSchema,
|
|
74
|
+
signature: zod.z.string().regex(/^0x[a-fA-F0-9]+$/, "Invalid signature format"),
|
|
75
|
+
signer: zod.z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid signer address")
|
|
76
|
+
});
|
|
77
|
+
var X402Error = class _X402Error extends Error {
|
|
78
|
+
constructor(message, code, details) {
|
|
79
|
+
super(message);
|
|
80
|
+
this.code = code;
|
|
81
|
+
this.details = details;
|
|
82
|
+
this.name = "X402Error";
|
|
83
|
+
Object.setPrototypeOf(this, _X402Error.prototype);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var PaymentRequiredError = class _PaymentRequiredError extends X402Error {
|
|
87
|
+
constructor(requirement, message = "Payment required") {
|
|
88
|
+
super(message, "PAYMENT_REQUIRED", { requirement });
|
|
89
|
+
this.requirement = requirement;
|
|
90
|
+
this.name = "PaymentRequiredError";
|
|
91
|
+
Object.setPrototypeOf(this, _PaymentRequiredError.prototype);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
var PaymentVerificationError = class _PaymentVerificationError extends X402Error {
|
|
95
|
+
constructor(message, details) {
|
|
96
|
+
super(message, "PAYMENT_VERIFICATION_FAILED", details);
|
|
97
|
+
this.details = details;
|
|
98
|
+
this.name = "PaymentVerificationError";
|
|
99
|
+
Object.setPrototypeOf(this, _PaymentVerificationError.prototype);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var UnsupportedNetworkError = class _UnsupportedNetworkError extends X402Error {
|
|
103
|
+
constructor(networkId, supportedNetworks) {
|
|
104
|
+
super(
|
|
105
|
+
`Network ${networkId} is not supported. Supported networks: ${supportedNetworks.join(", ")}`,
|
|
106
|
+
"UNSUPPORTED_NETWORK",
|
|
107
|
+
{ networkId, supportedNetworks }
|
|
108
|
+
);
|
|
109
|
+
this.name = "UnsupportedNetworkError";
|
|
110
|
+
Object.setPrototypeOf(this, _UnsupportedNetworkError.prototype);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var PaymentExpiredError = class _PaymentExpiredError extends X402Error {
|
|
114
|
+
constructor(deadline) {
|
|
115
|
+
super(
|
|
116
|
+
`Payment has expired. Deadline: ${new Date(deadline * 1e3).toISOString()}`,
|
|
117
|
+
"PAYMENT_EXPIRED",
|
|
118
|
+
{ deadline }
|
|
119
|
+
);
|
|
120
|
+
this.name = "PaymentExpiredError";
|
|
121
|
+
Object.setPrototypeOf(this, _PaymentExpiredError.prototype);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
function extractChainId(caipId) {
|
|
125
|
+
const match = caipId.match(/^eip155:(\d+)$/);
|
|
126
|
+
if (!match?.[1]) {
|
|
127
|
+
throw new X402Error(`Invalid CAIP-2 identifier: ${caipId}`, "INVALID_CAIP_ID");
|
|
128
|
+
}
|
|
129
|
+
return parseInt(match[1], 10);
|
|
130
|
+
}
|
|
131
|
+
function createCaipId(chainId) {
|
|
132
|
+
return `eip155:${chainId}`;
|
|
133
|
+
}
|
|
134
|
+
function generateNonce() {
|
|
135
|
+
const bytes = new Uint8Array(16);
|
|
136
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
137
|
+
crypto.getRandomValues(bytes);
|
|
138
|
+
} else {
|
|
139
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
140
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
144
|
+
}
|
|
145
|
+
function calculateDeadline(durationSeconds = 300) {
|
|
146
|
+
return Math.floor(Date.now() / 1e3) + durationSeconds;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/chains/bnb.ts
|
|
150
|
+
var BSC_CHAIN_ID = 56;
|
|
151
|
+
var BSC_CAIP_ID = "eip155:56";
|
|
152
|
+
var BSC_RPC_URLS = [
|
|
153
|
+
"https://bsc-dataseed.binance.org",
|
|
154
|
+
"https://bsc-dataseed1.binance.org",
|
|
155
|
+
"https://bsc-dataseed2.binance.org",
|
|
156
|
+
"https://bsc-dataseed3.binance.org",
|
|
157
|
+
"https://bsc-dataseed4.binance.org",
|
|
158
|
+
"https://bsc-dataseed1.defibit.io",
|
|
159
|
+
"https://bsc-dataseed1.ninicoin.io"
|
|
160
|
+
];
|
|
161
|
+
var BSC_DEFAULT_RPC = BSC_RPC_URLS[0];
|
|
162
|
+
var BSC_BLOCK_EXPLORER = "https://bscscan.com";
|
|
163
|
+
var BSC_USDT = {
|
|
164
|
+
address: "0x55d398326f99059fF775485246999027B3197955",
|
|
165
|
+
symbol: "USDT",
|
|
166
|
+
decimals: 18,
|
|
167
|
+
name: "Tether USD"
|
|
168
|
+
};
|
|
169
|
+
var BSC_USDC = {
|
|
170
|
+
address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
171
|
+
symbol: "USDC",
|
|
172
|
+
decimals: 18,
|
|
173
|
+
name: "USD Coin"
|
|
174
|
+
};
|
|
175
|
+
var BSC_BUSD = {
|
|
176
|
+
address: "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56",
|
|
177
|
+
symbol: "BUSD",
|
|
178
|
+
decimals: 18,
|
|
179
|
+
name: "Binance USD"
|
|
180
|
+
};
|
|
181
|
+
var BSC_WBNB = {
|
|
182
|
+
address: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
|
|
183
|
+
symbol: "BNB",
|
|
184
|
+
decimals: 18,
|
|
185
|
+
name: "Wrapped BNB"
|
|
186
|
+
};
|
|
187
|
+
var BSC_DEFAULT_TOKEN = BSC_USDT;
|
|
188
|
+
var BSC_TOKENS = {
|
|
189
|
+
USDT: BSC_USDT,
|
|
190
|
+
USDC: BSC_USDC,
|
|
191
|
+
BUSD: BSC_BUSD,
|
|
192
|
+
WBNB: BSC_WBNB
|
|
193
|
+
};
|
|
194
|
+
var BSC_TOKEN_BY_ADDRESS = {
|
|
195
|
+
[BSC_USDT.address.toLowerCase()]: BSC_USDT,
|
|
196
|
+
[BSC_USDC.address.toLowerCase()]: BSC_USDC,
|
|
197
|
+
[BSC_BUSD.address.toLowerCase()]: BSC_BUSD,
|
|
198
|
+
[BSC_WBNB.address.toLowerCase()]: BSC_WBNB
|
|
199
|
+
};
|
|
200
|
+
var BSC_NETWORK_CONFIG = {
|
|
201
|
+
caipId: BSC_CAIP_ID,
|
|
202
|
+
chainId: BSC_CHAIN_ID,
|
|
203
|
+
name: "BNB Smart Chain",
|
|
204
|
+
rpcUrl: BSC_DEFAULT_RPC,
|
|
205
|
+
nativeCurrency: {
|
|
206
|
+
name: "BNB",
|
|
207
|
+
symbol: "BNB",
|
|
208
|
+
decimals: 18
|
|
209
|
+
},
|
|
210
|
+
blockExplorer: BSC_BLOCK_EXPLORER,
|
|
211
|
+
tokens: BSC_TOKENS
|
|
212
|
+
};
|
|
213
|
+
function getTokenByAddress(address) {
|
|
214
|
+
return BSC_TOKEN_BY_ADDRESS[address.toLowerCase()];
|
|
215
|
+
}
|
|
216
|
+
function getTokenBySymbol(symbol) {
|
|
217
|
+
const upperSymbol = symbol.toUpperCase();
|
|
218
|
+
return BSC_TOKENS[upperSymbol];
|
|
219
|
+
}
|
|
220
|
+
function isTokenSupported(address) {
|
|
221
|
+
return address.toLowerCase() in BSC_TOKEN_BY_ADDRESS;
|
|
222
|
+
}
|
|
223
|
+
function formatTokenAmount(amount, tokenAddress) {
|
|
224
|
+
const token = getTokenByAddress(tokenAddress);
|
|
225
|
+
const decimals = token?.decimals ?? 18;
|
|
226
|
+
const amountBigInt = typeof amount === "string" ? BigInt(amount) : amount;
|
|
227
|
+
const divisor = BigInt(10 ** decimals);
|
|
228
|
+
const wholePart = amountBigInt / divisor;
|
|
229
|
+
const fractionalPart = amountBigInt % divisor;
|
|
230
|
+
const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
|
|
231
|
+
const trimmedFractional = fractionalStr.replace(/0+$/, "").padEnd(2, "0");
|
|
232
|
+
return `${wholePart}.${trimmedFractional}`;
|
|
233
|
+
}
|
|
234
|
+
function parseTokenAmount(amount, tokenAddress) {
|
|
235
|
+
const token = getTokenByAddress(tokenAddress);
|
|
236
|
+
const decimals = token?.decimals ?? 18;
|
|
237
|
+
const [whole, fractional = ""] = amount.split(".");
|
|
238
|
+
const paddedFractional = fractional.padEnd(decimals, "0").slice(0, decimals);
|
|
239
|
+
return BigInt(whole + paddedFractional);
|
|
240
|
+
}
|
|
241
|
+
function getTxUrl(txHash) {
|
|
242
|
+
return `${BSC_BLOCK_EXPLORER}/tx/${txHash}`;
|
|
243
|
+
}
|
|
244
|
+
function getAddressUrl(address) {
|
|
245
|
+
return `${BSC_BLOCK_EXPLORER}/address/${address}`;
|
|
246
|
+
}
|
|
247
|
+
function getTokenUrl(tokenAddress) {
|
|
248
|
+
return `${BSC_BLOCK_EXPLORER}/token/${tokenAddress}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/chains/index.ts
|
|
252
|
+
var SUPPORTED_NETWORKS = {
|
|
253
|
+
[BSC_CAIP_ID]: BSC_NETWORK_CONFIG
|
|
254
|
+
};
|
|
255
|
+
function getNetworkConfig(caipId) {
|
|
256
|
+
return SUPPORTED_NETWORKS[caipId];
|
|
257
|
+
}
|
|
258
|
+
function isNetworkSupported(caipId) {
|
|
259
|
+
return caipId in SUPPORTED_NETWORKS;
|
|
260
|
+
}
|
|
261
|
+
function getSupportedNetworkIds() {
|
|
262
|
+
return Object.keys(SUPPORTED_NETWORKS);
|
|
263
|
+
}
|
|
264
|
+
var X402Client = class {
|
|
265
|
+
axiosInstance;
|
|
266
|
+
walletClient = null;
|
|
267
|
+
account = null;
|
|
268
|
+
config;
|
|
269
|
+
constructor(config = {}) {
|
|
270
|
+
this.config = {
|
|
271
|
+
supportedNetworks: [BSC_CAIP_ID],
|
|
272
|
+
defaultDeadline: 300,
|
|
273
|
+
// 5 minutes
|
|
274
|
+
autoRetry: true,
|
|
275
|
+
maxRetries: 1,
|
|
276
|
+
...config
|
|
277
|
+
};
|
|
278
|
+
this.axiosInstance = axios__default.default.create({
|
|
279
|
+
timeout: 3e4,
|
|
280
|
+
validateStatus: (status) => status < 500,
|
|
281
|
+
// Don't throw on 402
|
|
282
|
+
...config.axiosConfig
|
|
283
|
+
});
|
|
284
|
+
if (config.privateKey) {
|
|
285
|
+
const normalizedKey = config.privateKey.startsWith("0x") ? config.privateKey : `0x${config.privateKey}`;
|
|
286
|
+
this.account = accounts.privateKeyToAccount(normalizedKey);
|
|
287
|
+
this.walletClient = viem.createWalletClient({
|
|
288
|
+
account: this.account,
|
|
289
|
+
chain: chains.bsc,
|
|
290
|
+
transport: viem.http(config.rpcUrl ?? BSC_DEFAULT_RPC)
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Get the signer address if available
|
|
296
|
+
*/
|
|
297
|
+
get signerAddress() {
|
|
298
|
+
return this.account?.address ?? null;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Check if the client can sign payments
|
|
302
|
+
*/
|
|
303
|
+
get canSign() {
|
|
304
|
+
return this.account !== null;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Make an HTTP request with automatic 402 payment handling
|
|
308
|
+
*/
|
|
309
|
+
async fetch(url, options = {}) {
|
|
310
|
+
let lastError = null;
|
|
311
|
+
let attempts = 0;
|
|
312
|
+
while (attempts <= this.config.maxRetries) {
|
|
313
|
+
try {
|
|
314
|
+
const response = await this.axiosInstance.request({
|
|
315
|
+
url,
|
|
316
|
+
...options
|
|
317
|
+
});
|
|
318
|
+
if (response.status === 402) {
|
|
319
|
+
const requirement = this.parsePaymentRequirement(response);
|
|
320
|
+
if (this.config.onPaymentRequired) {
|
|
321
|
+
await this.config.onPaymentRequired(requirement);
|
|
322
|
+
}
|
|
323
|
+
if (!this.config.autoRetry || attempts >= this.config.maxRetries) {
|
|
324
|
+
throw new PaymentRequiredError(requirement);
|
|
325
|
+
}
|
|
326
|
+
if (!this.canSign) {
|
|
327
|
+
throw new PaymentRequiredError(
|
|
328
|
+
requirement,
|
|
329
|
+
"Payment required but no signer configured. Provide a privateKey in client config."
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (!this.config.supportedNetworks.includes(requirement.network_id)) {
|
|
333
|
+
throw new UnsupportedNetworkError(
|
|
334
|
+
requirement.network_id,
|
|
335
|
+
this.config.supportedNetworks
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
const signedPayment = await this.signPayment(requirement);
|
|
339
|
+
if (this.config.onPaymentSigned) {
|
|
340
|
+
await this.config.onPaymentSigned(signedPayment);
|
|
341
|
+
}
|
|
342
|
+
const retryOptions = this.addPaymentHeaders(options, signedPayment);
|
|
343
|
+
attempts++;
|
|
344
|
+
options = retryOptions;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
return response;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (error instanceof PaymentRequiredError || error instanceof UnsupportedNetworkError) {
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
lastError = error;
|
|
353
|
+
attempts++;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
throw lastError ?? new Error("Request failed after all retries");
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Convenience methods for common HTTP verbs
|
|
360
|
+
*/
|
|
361
|
+
async get(url, options) {
|
|
362
|
+
return this.fetch(url, { ...options, method: "GET" });
|
|
363
|
+
}
|
|
364
|
+
async post(url, data, options) {
|
|
365
|
+
return this.fetch(url, { ...options, method: "POST", data });
|
|
366
|
+
}
|
|
367
|
+
async put(url, data, options) {
|
|
368
|
+
return this.fetch(url, { ...options, method: "PUT", data });
|
|
369
|
+
}
|
|
370
|
+
async delete(url, options) {
|
|
371
|
+
return this.fetch(url, { ...options, method: "DELETE" });
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Sign a payment requirement and return the signed payment
|
|
375
|
+
*/
|
|
376
|
+
async signPayment(requirement) {
|
|
377
|
+
if (!this.walletClient || !this.account) {
|
|
378
|
+
throw new PaymentVerificationError(
|
|
379
|
+
"Cannot sign payment: no wallet configured"
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
const chainId = extractChainId(requirement.network_id);
|
|
383
|
+
const nonce = requirement.nonce ?? generateNonce();
|
|
384
|
+
const deadline = requirement.expires_at ?? calculateDeadline(this.config.defaultDeadline);
|
|
385
|
+
const payload = {
|
|
386
|
+
amount: requirement.amount,
|
|
387
|
+
token: requirement.token,
|
|
388
|
+
chainId,
|
|
389
|
+
payTo: requirement.pay_to,
|
|
390
|
+
payer: this.account.address,
|
|
391
|
+
deadline,
|
|
392
|
+
nonce,
|
|
393
|
+
resource: requirement.resource ?? ""
|
|
394
|
+
};
|
|
395
|
+
const domain = {
|
|
396
|
+
name: X402_DOMAIN_NAME,
|
|
397
|
+
version: X402_VERSION,
|
|
398
|
+
chainId
|
|
399
|
+
};
|
|
400
|
+
const signature = await this.walletClient.signTypedData({
|
|
401
|
+
account: this.account,
|
|
402
|
+
domain,
|
|
403
|
+
types: PAYMENT_TYPES,
|
|
404
|
+
primaryType: "Payment",
|
|
405
|
+
message: {
|
|
406
|
+
amount: BigInt(payload.amount),
|
|
407
|
+
token: payload.token,
|
|
408
|
+
chainId: BigInt(payload.chainId),
|
|
409
|
+
payTo: payload.payTo,
|
|
410
|
+
payer: payload.payer,
|
|
411
|
+
deadline: BigInt(payload.deadline),
|
|
412
|
+
nonce: payload.nonce,
|
|
413
|
+
resource: payload.resource ?? ""
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
return {
|
|
417
|
+
payload,
|
|
418
|
+
signature,
|
|
419
|
+
signer: this.account.address
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Parse payment requirement from 402 response
|
|
424
|
+
*/
|
|
425
|
+
parsePaymentRequirement(response) {
|
|
426
|
+
const headerValue = response.headers[X402_HEADERS.PAYMENT_REQUIRED] || response.headers[X402_HEADERS.PAYMENT_REQUIRED.toLowerCase()];
|
|
427
|
+
let requirementData;
|
|
428
|
+
if (headerValue) {
|
|
429
|
+
try {
|
|
430
|
+
requirementData = JSON.parse(
|
|
431
|
+
typeof headerValue === "string" ? headerValue : String(headerValue)
|
|
432
|
+
);
|
|
433
|
+
} catch {
|
|
434
|
+
throw new PaymentVerificationError(
|
|
435
|
+
"Invalid payment requirement header: failed to parse JSON"
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
} else if (response.data && typeof response.data === "object") {
|
|
439
|
+
requirementData = response.data;
|
|
440
|
+
} else {
|
|
441
|
+
throw new PaymentVerificationError(
|
|
442
|
+
"No payment requirement found in response headers or body"
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const result = PaymentRequirementSchema.safeParse(requirementData);
|
|
446
|
+
if (!result.success) {
|
|
447
|
+
throw new PaymentVerificationError(
|
|
448
|
+
`Invalid payment requirement: ${result.error.message}`,
|
|
449
|
+
{ errors: result.error.errors }
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
return result.data;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Add payment headers to request options
|
|
456
|
+
*/
|
|
457
|
+
addPaymentHeaders(options, payment) {
|
|
458
|
+
const headers = {
|
|
459
|
+
...options.headers,
|
|
460
|
+
[X402_HEADERS.PAYMENT_SIGNATURE]: payment.signature,
|
|
461
|
+
[X402_HEADERS.PAYMENT_PAYLOAD]: JSON.stringify(payment.payload)
|
|
462
|
+
};
|
|
463
|
+
return {
|
|
464
|
+
...options,
|
|
465
|
+
headers
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
function createX402Client(config) {
|
|
470
|
+
return new X402Client(config);
|
|
471
|
+
}
|
|
472
|
+
function createX402ClientFromEnv(config) {
|
|
473
|
+
const privateKey = process.env["X402_PRIVATE_KEY"];
|
|
474
|
+
if (!privateKey) {
|
|
475
|
+
console.warn("X402_PRIVATE_KEY not found in environment. Client will be read-only.");
|
|
476
|
+
}
|
|
477
|
+
return new X402Client({
|
|
478
|
+
...config,
|
|
479
|
+
privateKey
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
async function verifyPaymentSignatureLocal(payment, expectedRecipient, chainId) {
|
|
483
|
+
try {
|
|
484
|
+
const { payload, signature, signer } = payment;
|
|
485
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
486
|
+
if (payload.deadline < now) {
|
|
487
|
+
return {
|
|
488
|
+
valid: false,
|
|
489
|
+
error: `Payment expired at ${new Date(payload.deadline * 1e3).toISOString()}`
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
if (payload.chainId !== chainId) {
|
|
493
|
+
return {
|
|
494
|
+
valid: false,
|
|
495
|
+
error: `Chain ID mismatch: expected ${chainId}, got ${payload.chainId}`
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (payload.payTo.toLowerCase() !== expectedRecipient.toLowerCase()) {
|
|
499
|
+
return {
|
|
500
|
+
valid: false,
|
|
501
|
+
error: `Recipient mismatch: expected ${expectedRecipient}, got ${payload.payTo}`
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const domain = {
|
|
505
|
+
name: X402_DOMAIN_NAME,
|
|
506
|
+
version: X402_VERSION,
|
|
507
|
+
chainId
|
|
508
|
+
};
|
|
509
|
+
const isValid = await viem.verifyTypedData({
|
|
510
|
+
address: signer,
|
|
511
|
+
domain,
|
|
512
|
+
types: PAYMENT_TYPES,
|
|
513
|
+
primaryType: "Payment",
|
|
514
|
+
message: {
|
|
515
|
+
amount: BigInt(payload.amount),
|
|
516
|
+
token: payload.token,
|
|
517
|
+
chainId: BigInt(payload.chainId),
|
|
518
|
+
payTo: payload.payTo,
|
|
519
|
+
payer: payload.payer,
|
|
520
|
+
deadline: BigInt(payload.deadline),
|
|
521
|
+
nonce: payload.nonce,
|
|
522
|
+
resource: payload.resource ?? ""
|
|
523
|
+
},
|
|
524
|
+
signature
|
|
525
|
+
});
|
|
526
|
+
if (!isValid) {
|
|
527
|
+
return {
|
|
528
|
+
valid: false,
|
|
529
|
+
error: "Signature verification failed"
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
valid: true,
|
|
534
|
+
signer,
|
|
535
|
+
payload
|
|
536
|
+
};
|
|
537
|
+
} catch (error) {
|
|
538
|
+
return {
|
|
539
|
+
valid: false,
|
|
540
|
+
error: error instanceof Error ? error.message : "Unknown verification error"
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async function verifyPaymentWithFacilitator(payment, facilitatorUrl, networkId) {
|
|
545
|
+
try {
|
|
546
|
+
const request = {
|
|
547
|
+
signature: payment.signature,
|
|
548
|
+
payload: payment.payload,
|
|
549
|
+
networkId
|
|
550
|
+
};
|
|
551
|
+
const response = await axios__default.default.post(
|
|
552
|
+
`${facilitatorUrl}/verify`,
|
|
553
|
+
request,
|
|
554
|
+
{
|
|
555
|
+
timeout: 1e4,
|
|
556
|
+
headers: { "Content-Type": "application/json" }
|
|
557
|
+
}
|
|
558
|
+
);
|
|
559
|
+
if (response.data.valid) {
|
|
560
|
+
return {
|
|
561
|
+
valid: true,
|
|
562
|
+
signer: response.data.signer,
|
|
563
|
+
payload: payment.payload
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
valid: false,
|
|
568
|
+
error: response.data.error ?? "Facilitator rejected payment"
|
|
569
|
+
};
|
|
570
|
+
} catch (error) {
|
|
571
|
+
return {
|
|
572
|
+
valid: false,
|
|
573
|
+
error: error instanceof Error ? `Facilitator error: ${error.message}` : "Facilitator verification failed"
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function x402Middleware(config) {
|
|
578
|
+
const {
|
|
579
|
+
recipientAddress,
|
|
580
|
+
amount,
|
|
581
|
+
tokenAddress = BSC_USDT.address,
|
|
582
|
+
facilitatorUrl,
|
|
583
|
+
description,
|
|
584
|
+
networkId = BSC_CAIP_ID,
|
|
585
|
+
deadlineDuration = 300,
|
|
586
|
+
nonceGenerator = generateNonce,
|
|
587
|
+
verifyPayment: customVerify,
|
|
588
|
+
excludeRoutes = [],
|
|
589
|
+
onError
|
|
590
|
+
} = config;
|
|
591
|
+
const chainId = extractChainId(networkId);
|
|
592
|
+
return async (req, res, next) => {
|
|
593
|
+
try {
|
|
594
|
+
const path = req.path;
|
|
595
|
+
if (excludeRoutes.some((route) => path.startsWith(route))) {
|
|
596
|
+
next();
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const signatureHeader = req.headers[X402_HEADERS.PAYMENT_SIGNATURE] || req.headers[X402_HEADERS.PAYMENT_SIGNATURE.toLowerCase()];
|
|
600
|
+
const payloadHeader = req.headers[X402_HEADERS.PAYMENT_PAYLOAD] || req.headers[X402_HEADERS.PAYMENT_PAYLOAD.toLowerCase()];
|
|
601
|
+
if (!signatureHeader) {
|
|
602
|
+
const requirement = {
|
|
603
|
+
amount,
|
|
604
|
+
token: tokenAddress,
|
|
605
|
+
network_id: networkId,
|
|
606
|
+
pay_to: recipientAddress,
|
|
607
|
+
description,
|
|
608
|
+
resource: req.originalUrl,
|
|
609
|
+
expires_at: calculateDeadline(deadlineDuration),
|
|
610
|
+
nonce: nonceGenerator(),
|
|
611
|
+
version: X402_VERSION
|
|
612
|
+
};
|
|
613
|
+
res.status(402);
|
|
614
|
+
res.setHeader(X402_HEADERS.PAYMENT_REQUIRED, JSON.stringify(requirement));
|
|
615
|
+
res.json({
|
|
616
|
+
error: "Payment Required",
|
|
617
|
+
requirement
|
|
618
|
+
});
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
let payload;
|
|
622
|
+
try {
|
|
623
|
+
if (!payloadHeader) {
|
|
624
|
+
throw new Error("Missing payment payload header");
|
|
625
|
+
}
|
|
626
|
+
const parsedPayload = JSON.parse(
|
|
627
|
+
typeof payloadHeader === "string" ? payloadHeader : String(payloadHeader)
|
|
628
|
+
);
|
|
629
|
+
const result = PaymentPayloadSchema.safeParse(parsedPayload);
|
|
630
|
+
if (!result.success) {
|
|
631
|
+
throw new Error(`Invalid payload: ${result.error.message}`);
|
|
632
|
+
}
|
|
633
|
+
payload = result.data;
|
|
634
|
+
} catch (error) {
|
|
635
|
+
res.status(400).json({
|
|
636
|
+
error: "Invalid Payment",
|
|
637
|
+
message: error instanceof Error ? error.message : "Failed to parse payment payload"
|
|
638
|
+
});
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const signedPayment = {
|
|
642
|
+
payload,
|
|
643
|
+
signature: typeof signatureHeader === "string" ? signatureHeader : String(signatureHeader),
|
|
644
|
+
signer: payload.payer
|
|
645
|
+
};
|
|
646
|
+
let verificationResult;
|
|
647
|
+
if (facilitatorUrl) {
|
|
648
|
+
verificationResult = await verifyPaymentWithFacilitator(
|
|
649
|
+
signedPayment,
|
|
650
|
+
facilitatorUrl,
|
|
651
|
+
networkId
|
|
652
|
+
);
|
|
653
|
+
} else {
|
|
654
|
+
verificationResult = await verifyPaymentSignatureLocal(
|
|
655
|
+
signedPayment,
|
|
656
|
+
recipientAddress,
|
|
657
|
+
chainId
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
if (!verificationResult.valid) {
|
|
661
|
+
res.status(402).json({
|
|
662
|
+
error: "Payment Verification Failed",
|
|
663
|
+
message: verificationResult.error
|
|
664
|
+
});
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (customVerify) {
|
|
668
|
+
const customValid = await customVerify(signedPayment, req);
|
|
669
|
+
if (!customValid) {
|
|
670
|
+
res.status(402).json({
|
|
671
|
+
error: "Payment Rejected",
|
|
672
|
+
message: "Custom verification failed"
|
|
673
|
+
});
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (BigInt(payload.amount) < BigInt(amount)) {
|
|
678
|
+
res.status(402).json({
|
|
679
|
+
error: "Insufficient Payment",
|
|
680
|
+
message: `Expected ${amount}, received ${payload.amount}`
|
|
681
|
+
});
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
req.x402 = {
|
|
685
|
+
payment: signedPayment,
|
|
686
|
+
verified: true,
|
|
687
|
+
signer: verificationResult.signer
|
|
688
|
+
};
|
|
689
|
+
next();
|
|
690
|
+
} catch (error) {
|
|
691
|
+
if (onError && error instanceof Error) {
|
|
692
|
+
onError(error, req, res);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
console.error("[x402] Middleware error:", error);
|
|
696
|
+
res.status(500).json({
|
|
697
|
+
error: "Internal Server Error",
|
|
698
|
+
message: "Payment processing failed"
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function createPaymentRequirement(config) {
|
|
704
|
+
const {
|
|
705
|
+
recipientAddress,
|
|
706
|
+
amount,
|
|
707
|
+
tokenAddress = BSC_USDT.address,
|
|
708
|
+
description,
|
|
709
|
+
networkId = BSC_CAIP_ID,
|
|
710
|
+
resource,
|
|
711
|
+
deadline,
|
|
712
|
+
nonce
|
|
713
|
+
} = config;
|
|
714
|
+
return {
|
|
715
|
+
amount,
|
|
716
|
+
token: tokenAddress,
|
|
717
|
+
network_id: networkId,
|
|
718
|
+
pay_to: recipientAddress,
|
|
719
|
+
description,
|
|
720
|
+
resource,
|
|
721
|
+
expires_at: deadline ?? calculateDeadline(300),
|
|
722
|
+
nonce: nonce ?? generateNonce(),
|
|
723
|
+
version: X402_VERSION
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
async function verifyPayment(payment, recipientAddress, networkId = BSC_CAIP_ID, facilitatorUrl) {
|
|
727
|
+
const chainId = extractChainId(networkId);
|
|
728
|
+
if (facilitatorUrl) {
|
|
729
|
+
return verifyPaymentWithFacilitator(payment, facilitatorUrl, networkId);
|
|
730
|
+
}
|
|
731
|
+
return verifyPaymentSignatureLocal(payment, recipientAddress, chainId);
|
|
732
|
+
}
|
|
733
|
+
function parsePaymentFromRequest(req) {
|
|
734
|
+
try {
|
|
735
|
+
const signatureHeader = req.headers[X402_HEADERS.PAYMENT_SIGNATURE] || req.headers[X402_HEADERS.PAYMENT_SIGNATURE.toLowerCase()];
|
|
736
|
+
const payloadHeader = req.headers[X402_HEADERS.PAYMENT_PAYLOAD] || req.headers[X402_HEADERS.PAYMENT_PAYLOAD.toLowerCase()];
|
|
737
|
+
if (!signatureHeader || !payloadHeader) {
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
const payload = JSON.parse(
|
|
741
|
+
typeof payloadHeader === "string" ? payloadHeader : String(payloadHeader)
|
|
742
|
+
);
|
|
743
|
+
const result = SignedPaymentSchema.safeParse({
|
|
744
|
+
payload,
|
|
745
|
+
signature: typeof signatureHeader === "string" ? signatureHeader : String(signatureHeader),
|
|
746
|
+
signer: payload.payer
|
|
747
|
+
});
|
|
748
|
+
return result.success ? result.data : null;
|
|
749
|
+
} catch {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
exports.BSC_BLOCK_EXPLORER = BSC_BLOCK_EXPLORER;
|
|
755
|
+
exports.BSC_BUSD = BSC_BUSD;
|
|
756
|
+
exports.BSC_CAIP_ID = BSC_CAIP_ID;
|
|
757
|
+
exports.BSC_CHAIN_ID = BSC_CHAIN_ID;
|
|
758
|
+
exports.BSC_DEFAULT_RPC = BSC_DEFAULT_RPC;
|
|
759
|
+
exports.BSC_DEFAULT_TOKEN = BSC_DEFAULT_TOKEN;
|
|
760
|
+
exports.BSC_NETWORK_CONFIG = BSC_NETWORK_CONFIG;
|
|
761
|
+
exports.BSC_RPC_URLS = BSC_RPC_URLS;
|
|
762
|
+
exports.BSC_TOKENS = BSC_TOKENS;
|
|
763
|
+
exports.BSC_TOKEN_BY_ADDRESS = BSC_TOKEN_BY_ADDRESS;
|
|
764
|
+
exports.BSC_USDC = BSC_USDC;
|
|
765
|
+
exports.BSC_USDT = BSC_USDT;
|
|
766
|
+
exports.BSC_WBNB = BSC_WBNB;
|
|
767
|
+
exports.PAYMENT_TYPES = PAYMENT_TYPES;
|
|
768
|
+
exports.PaymentExpiredError = PaymentExpiredError;
|
|
769
|
+
exports.PaymentPayloadSchema = PaymentPayloadSchema;
|
|
770
|
+
exports.PaymentRequiredError = PaymentRequiredError;
|
|
771
|
+
exports.PaymentRequirementSchema = PaymentRequirementSchema;
|
|
772
|
+
exports.PaymentVerificationError = PaymentVerificationError;
|
|
773
|
+
exports.SUPPORTED_NETWORKS = SUPPORTED_NETWORKS;
|
|
774
|
+
exports.SignedPaymentSchema = SignedPaymentSchema;
|
|
775
|
+
exports.UnsupportedNetworkError = UnsupportedNetworkError;
|
|
776
|
+
exports.X402Client = X402Client;
|
|
777
|
+
exports.X402Error = X402Error;
|
|
778
|
+
exports.X402_DOMAIN_NAME = X402_DOMAIN_NAME;
|
|
779
|
+
exports.X402_HEADERS = X402_HEADERS;
|
|
780
|
+
exports.X402_VERSION = X402_VERSION;
|
|
781
|
+
exports.calculateDeadline = calculateDeadline;
|
|
782
|
+
exports.createCaipId = createCaipId;
|
|
783
|
+
exports.createPaymentRequirement = createPaymentRequirement;
|
|
784
|
+
exports.createX402Client = createX402Client;
|
|
785
|
+
exports.createX402ClientFromEnv = createX402ClientFromEnv;
|
|
786
|
+
exports.extractChainId = extractChainId;
|
|
787
|
+
exports.formatTokenAmount = formatTokenAmount;
|
|
788
|
+
exports.generateNonce = generateNonce;
|
|
789
|
+
exports.getAddressUrl = getAddressUrl;
|
|
790
|
+
exports.getNetworkConfig = getNetworkConfig;
|
|
791
|
+
exports.getSupportedNetworkIds = getSupportedNetworkIds;
|
|
792
|
+
exports.getTokenByAddress = getTokenByAddress;
|
|
793
|
+
exports.getTokenBySymbol = getTokenBySymbol;
|
|
794
|
+
exports.getTokenUrl = getTokenUrl;
|
|
795
|
+
exports.getTxUrl = getTxUrl;
|
|
796
|
+
exports.isNetworkSupported = isNetworkSupported;
|
|
797
|
+
exports.isTokenSupported = isTokenSupported;
|
|
798
|
+
exports.parsePaymentFromRequest = parsePaymentFromRequest;
|
|
799
|
+
exports.parseTokenAmount = parseTokenAmount;
|
|
800
|
+
exports.verifyPayment = verifyPayment;
|
|
801
|
+
exports.x402Middleware = x402Middleware;
|
|
802
|
+
//# sourceMappingURL=index.cjs.map
|
|
803
|
+
//# sourceMappingURL=index.cjs.map
|