@voyage_ai/v402-web-ts 0.1.0 → 0.1.1
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 +639 -1
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +585 -223
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +585 -223
- package/dist/index.mjs.map +1 -1
- package/dist/react/components/PaymentButton.tsx +12 -5
- package/dist/react/components/WalletConnect.tsx +47 -20
- package/dist/react/hooks/useWallet.ts +35 -8
- package/dist/react/index.d.mts +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +653 -33
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +655 -35
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/store/walletStore.ts +29 -0
- package/dist/react/styles/inline-styles.ts +227 -0
- package/dist/react/styles.css +8 -1
- package/package.json +8 -6
package/dist/index.js
CHANGED
|
@@ -56,7 +56,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
56
56
|
var import_types3 = require("x402/types");
|
|
57
57
|
|
|
58
58
|
// src/types/common.ts
|
|
59
|
-
var PROD_BACK_URL = "https://v402.onvoyage.ai/api";
|
|
59
|
+
var PROD_BACK_URL = "https://v402.onvoyage.ai/api/pay";
|
|
60
60
|
|
|
61
61
|
// src/types/svm.ts
|
|
62
62
|
var import_zod = require("zod");
|
|
@@ -134,166 +134,63 @@ function getChainId(network) {
|
|
|
134
134
|
// src/services/svm/payment-header.ts
|
|
135
135
|
var import_web3 = require("@solana/web3.js");
|
|
136
136
|
var import_spl_token = require("@solana/spl-token");
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
throw new Error("Missing facilitator feePayer in payment requirements (extra.feePayer).");
|
|
143
|
-
}
|
|
144
|
-
const feePayerPubkey = new import_web3.PublicKey(feePayer);
|
|
145
|
-
const walletAddress = wallet?.publicKey?.toString() || wallet?.address;
|
|
146
|
-
if (!walletAddress) {
|
|
147
|
-
throw new Error("Missing connected Solana wallet address or publicKey");
|
|
148
|
-
}
|
|
149
|
-
const userPubkey = new import_web3.PublicKey(walletAddress);
|
|
150
|
-
if (!paymentRequirements?.payTo) {
|
|
151
|
-
throw new Error("Missing payTo in payment requirements");
|
|
152
|
-
}
|
|
153
|
-
const destination = new import_web3.PublicKey(paymentRequirements.payTo);
|
|
154
|
-
const instructions = [];
|
|
155
|
-
instructions.push(
|
|
156
|
-
import_web3.ComputeBudgetProgram.setComputeUnitLimit({
|
|
157
|
-
units: 7e3
|
|
158
|
-
// Sufficient for SPL token transfer
|
|
159
|
-
})
|
|
160
|
-
);
|
|
161
|
-
instructions.push(
|
|
162
|
-
import_web3.ComputeBudgetProgram.setComputeUnitPrice({
|
|
163
|
-
microLamports: 1
|
|
164
|
-
// Minimal price
|
|
165
|
-
})
|
|
166
|
-
);
|
|
167
|
-
if (!paymentRequirements.asset) {
|
|
168
|
-
throw new Error("Missing token mint for SPL transfer");
|
|
137
|
+
|
|
138
|
+
// src/utils/wallet.ts
|
|
139
|
+
function isWalletInstalled(networkType) {
|
|
140
|
+
if (typeof window === "undefined") {
|
|
141
|
+
return false;
|
|
169
142
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
programId
|
|
179
|
-
);
|
|
180
|
-
const destinationAta = await (0, import_spl_token.getAssociatedTokenAddress)(
|
|
181
|
-
mintPubkey,
|
|
182
|
-
destination,
|
|
183
|
-
false,
|
|
184
|
-
programId
|
|
185
|
-
);
|
|
186
|
-
const sourceAtaInfo = await connection.getAccountInfo(sourceAta, "confirmed");
|
|
187
|
-
if (!sourceAtaInfo) {
|
|
188
|
-
throw new Error(
|
|
189
|
-
`User does not have an Associated Token Account for ${paymentRequirements.asset}. Please create one first or ensure you have the required token.`
|
|
190
|
-
);
|
|
143
|
+
switch (networkType) {
|
|
144
|
+
case "evm" /* EVM */:
|
|
145
|
+
return !!window.ethereum;
|
|
146
|
+
case "solana" /* SOLANA */:
|
|
147
|
+
case "svm" /* SVM */:
|
|
148
|
+
return !!window.solana || !!window.phantom;
|
|
149
|
+
default:
|
|
150
|
+
return false;
|
|
191
151
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
);
|
|
152
|
+
}
|
|
153
|
+
function getWalletProvider(networkType) {
|
|
154
|
+
if (typeof window === "undefined") {
|
|
155
|
+
return null;
|
|
197
156
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
mint.decimals,
|
|
207
|
-
[],
|
|
208
|
-
programId
|
|
209
|
-
)
|
|
210
|
-
);
|
|
211
|
-
const { blockhash } = await connection.getLatestBlockhash("confirmed");
|
|
212
|
-
const message = new import_web3.TransactionMessage({
|
|
213
|
-
payerKey: feePayerPubkey,
|
|
214
|
-
recentBlockhash: blockhash,
|
|
215
|
-
instructions
|
|
216
|
-
}).compileToV0Message();
|
|
217
|
-
const transaction = new import_web3.VersionedTransaction(message);
|
|
218
|
-
if (typeof wallet?.signTransaction !== "function") {
|
|
219
|
-
throw new Error("Connected wallet does not support signTransaction");
|
|
157
|
+
switch (networkType) {
|
|
158
|
+
case "evm" /* EVM */:
|
|
159
|
+
return window.ethereum;
|
|
160
|
+
case "solana" /* SOLANA */:
|
|
161
|
+
case "svm" /* SVM */:
|
|
162
|
+
return window.solana || window.phantom;
|
|
163
|
+
default:
|
|
164
|
+
return null;
|
|
220
165
|
}
|
|
221
|
-
const userSignedTx = await wallet.signTransaction(transaction);
|
|
222
|
-
const serializedTransaction = Buffer.from(userSignedTx.serialize()).toString("base64");
|
|
223
|
-
const paymentPayload = {
|
|
224
|
-
x402Version,
|
|
225
|
-
scheme: paymentRequirements.scheme,
|
|
226
|
-
network: paymentRequirements.network,
|
|
227
|
-
payload: {
|
|
228
|
-
transaction: serializedTransaction
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
const paymentHeader = Buffer.from(JSON.stringify(paymentPayload)).toString("base64");
|
|
232
|
-
return paymentHeader;
|
|
233
166
|
}
|
|
234
|
-
function
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return "https://api.mainnet-beta.solana.com";
|
|
238
|
-
} else if (normalized === "solana-devnet") {
|
|
239
|
-
return "https://api.devnet.solana.com";
|
|
167
|
+
function formatAddress(address) {
|
|
168
|
+
if (!address || address.length < 10) {
|
|
169
|
+
return address;
|
|
240
170
|
}
|
|
241
|
-
|
|
171
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
242
172
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
return initialResponse;
|
|
253
|
-
}
|
|
254
|
-
const rawResponse = await initialResponse.json();
|
|
255
|
-
const x402Version = rawResponse.x402Version;
|
|
256
|
-
const parsedPaymentRequirements = rawResponse.accepts || [];
|
|
257
|
-
const selectedRequirements = parsedPaymentRequirements.find(
|
|
258
|
-
(req) => req.scheme === "exact" && SolanaNetworkSchema.safeParse(req.network.toLowerCase()).success
|
|
259
|
-
);
|
|
260
|
-
if (!selectedRequirements) {
|
|
261
|
-
console.error(
|
|
262
|
-
"\u274C No suitable Solana payment requirements found. Available networks:",
|
|
263
|
-
parsedPaymentRequirements.map((req) => req.network)
|
|
264
|
-
);
|
|
265
|
-
throw new Error("No suitable Solana payment requirements found");
|
|
266
|
-
}
|
|
267
|
-
if (maxPaymentAmount && maxPaymentAmount > BigInt(0)) {
|
|
268
|
-
if (BigInt(selectedRequirements.maxAmountRequired) > maxPaymentAmount) {
|
|
269
|
-
throw new Error(
|
|
270
|
-
`Payment amount ${selectedRequirements.maxAmountRequired} exceeds maximum allowed ${maxPaymentAmount}`
|
|
271
|
-
);
|
|
272
|
-
}
|
|
173
|
+
function getWalletInstallUrl(networkType) {
|
|
174
|
+
switch (networkType) {
|
|
175
|
+
case "evm" /* EVM */:
|
|
176
|
+
return "https://metamask.io/download/";
|
|
177
|
+
case "solana" /* SOLANA */:
|
|
178
|
+
case "svm" /* SVM */:
|
|
179
|
+
return "https://phantom.app/download";
|
|
180
|
+
default:
|
|
181
|
+
return "#";
|
|
273
182
|
}
|
|
274
|
-
const effectiveRpcUrl = rpcUrl || getDefaultSolanaRpcUrl(network);
|
|
275
|
-
const paymentHeader = await createSvmPaymentHeader({
|
|
276
|
-
wallet,
|
|
277
|
-
paymentRequirements: selectedRequirements,
|
|
278
|
-
x402Version,
|
|
279
|
-
rpcUrl: effectiveRpcUrl
|
|
280
|
-
});
|
|
281
|
-
const newInit = {
|
|
282
|
-
...requestInit,
|
|
283
|
-
method: requestInit?.method || "POST",
|
|
284
|
-
headers: {
|
|
285
|
-
...requestInit?.headers || {},
|
|
286
|
-
"X-PAYMENT": paymentHeader,
|
|
287
|
-
"Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE"
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
return await fetch(endpoint, newInit);
|
|
291
183
|
}
|
|
292
|
-
function
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
184
|
+
function getWalletDisplayName(networkType) {
|
|
185
|
+
switch (networkType) {
|
|
186
|
+
case "evm" /* EVM */:
|
|
187
|
+
return "MetaMask";
|
|
188
|
+
case "solana" /* SOLANA */:
|
|
189
|
+
case "svm" /* SVM */:
|
|
190
|
+
return "Phantom";
|
|
191
|
+
default:
|
|
192
|
+
return "Unknown Wallet";
|
|
193
|
+
}
|
|
297
194
|
}
|
|
298
195
|
|
|
299
196
|
// src/services/evm/payment-header.ts
|
|
@@ -306,6 +203,34 @@ async function createEvmPaymentHeader(params) {
|
|
|
306
203
|
if (!paymentRequirements?.asset) {
|
|
307
204
|
throw new Error("Missing asset (token contract) in payment requirements");
|
|
308
205
|
}
|
|
206
|
+
if (wallet.getChainId) {
|
|
207
|
+
try {
|
|
208
|
+
const currentChainIdHex = await wallet.getChainId();
|
|
209
|
+
const currentChainId = parseInt(currentChainIdHex, 16);
|
|
210
|
+
if (currentChainId !== chainId) {
|
|
211
|
+
const networkNames = {
|
|
212
|
+
1: "Ethereum Mainnet",
|
|
213
|
+
11155111: "Sepolia Testnet",
|
|
214
|
+
8453: "Base Mainnet",
|
|
215
|
+
84532: "Base Sepolia Testnet",
|
|
216
|
+
137: "Polygon Mainnet",
|
|
217
|
+
42161: "Arbitrum One",
|
|
218
|
+
10: "Optimism Mainnet"
|
|
219
|
+
};
|
|
220
|
+
const currentNetworkName = networkNames[currentChainId] || `Chain ${currentChainId}`;
|
|
221
|
+
const targetNetworkName = networkNames[chainId] || `Chain ${chainId}`;
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Network mismatch: Your wallet is connected to ${currentNetworkName}, but payment requires ${targetNetworkName}. Please switch your wallet to the correct network.`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
console.log(`\u2705 Chain ID verified: ${chainId}`);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error.message.includes("Network mismatch")) {
|
|
229
|
+
throw wrapPaymentError(error);
|
|
230
|
+
}
|
|
231
|
+
console.warn("Could not verify chainId:", error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
309
234
|
const now = Math.floor(Date.now() / 1e3);
|
|
310
235
|
const nonceBytes = import_ethers.ethers.randomBytes(32);
|
|
311
236
|
const nonceBytes32 = import_ethers.ethers.hexlify(nonceBytes);
|
|
@@ -334,7 +259,14 @@ async function createEvmPaymentHeader(params) {
|
|
|
334
259
|
validBefore: String(now + (paymentRequirements.maxTimeoutSeconds || 3600)),
|
|
335
260
|
nonce: nonceBytes32
|
|
336
261
|
};
|
|
337
|
-
|
|
262
|
+
let signature;
|
|
263
|
+
try {
|
|
264
|
+
signature = await wallet.signTypedData(domain, types, authorization);
|
|
265
|
+
console.log("\u2705 Signature created successfully");
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.error("\u274C Failed to create signature:", error);
|
|
268
|
+
throw wrapPaymentError(error);
|
|
269
|
+
}
|
|
338
270
|
const headerPayload = {
|
|
339
271
|
x402_version: x402Version,
|
|
340
272
|
x402Version,
|
|
@@ -385,6 +317,26 @@ async function handleEvmPayment(endpoint, config, requestInit) {
|
|
|
385
317
|
return initialResponse;
|
|
386
318
|
}
|
|
387
319
|
const rawResponse = await initialResponse.json();
|
|
320
|
+
const IGNORED_ERRORS = [
|
|
321
|
+
"X-PAYMENT header is required",
|
|
322
|
+
"missing X-PAYMENT header",
|
|
323
|
+
"payment_required"
|
|
324
|
+
];
|
|
325
|
+
if (rawResponse.error && !IGNORED_ERRORS.includes(rawResponse.error)) {
|
|
326
|
+
console.error(`\u274C Payment verification failed: ${rawResponse.error}`);
|
|
327
|
+
const ERROR_MESSAGES = {
|
|
328
|
+
"insufficient_funds": "Insufficient balance to complete this payment",
|
|
329
|
+
"invalid_signature": "Invalid payment signature",
|
|
330
|
+
"expired": "Payment authorization has expired",
|
|
331
|
+
"already_used": "This payment has already been used",
|
|
332
|
+
"network_mismatch": "Payment network does not match",
|
|
333
|
+
"invalid_payment": "Invalid payment data",
|
|
334
|
+
"verification_failed": "Payment verification failed"
|
|
335
|
+
};
|
|
336
|
+
const errorMessage = ERROR_MESSAGES[rawResponse.error] || `Payment failed: ${rawResponse.error}`;
|
|
337
|
+
const error = new Error(errorMessage);
|
|
338
|
+
throw wrapPaymentError(error);
|
|
339
|
+
}
|
|
388
340
|
const x402Version = rawResponse.x402Version;
|
|
389
341
|
const parsedPaymentRequirements = rawResponse.accepts || [];
|
|
390
342
|
const selectedRequirements = parsedPaymentRequirements.find(
|
|
@@ -405,19 +357,81 @@ async function handleEvmPayment(endpoint, config, requestInit) {
|
|
|
405
357
|
}
|
|
406
358
|
}
|
|
407
359
|
const targetChainId = getChainIdFromNetwork(selectedRequirements.network);
|
|
408
|
-
|
|
360
|
+
let currentChainId;
|
|
361
|
+
if (wallet.getChainId) {
|
|
409
362
|
try {
|
|
363
|
+
const chainIdHex = await wallet.getChainId();
|
|
364
|
+
currentChainId = parseInt(chainIdHex, 16);
|
|
365
|
+
console.log(`\u{1F4CD} Current wallet chain: ${currentChainId}`);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
console.warn("\u26A0\uFE0F Failed to get current chainId:", error);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const networkNames = {
|
|
371
|
+
1: "Ethereum Mainnet",
|
|
372
|
+
11155111: "Sepolia Testnet",
|
|
373
|
+
8453: "Base Mainnet",
|
|
374
|
+
84532: "Base Sepolia Testnet",
|
|
375
|
+
137: "Polygon Mainnet",
|
|
376
|
+
42161: "Arbitrum One",
|
|
377
|
+
10: "Optimism Mainnet"
|
|
378
|
+
};
|
|
379
|
+
if (currentChainId && currentChainId !== targetChainId) {
|
|
380
|
+
if (!wallet.switchChain) {
|
|
381
|
+
const currentNetworkName = networkNames[currentChainId] || `Chain ${currentChainId}`;
|
|
382
|
+
const targetNetworkName = networkNames[targetChainId] || selectedRequirements.network;
|
|
383
|
+
const error = new Error(
|
|
384
|
+
`Network mismatch: Your wallet is connected to ${currentNetworkName}, but payment requires ${targetNetworkName}. Please switch to ${targetNetworkName} manually in your wallet.`
|
|
385
|
+
);
|
|
386
|
+
throw wrapPaymentError(error);
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
console.log(`\u{1F504} Switching to chain ${targetChainId}...`);
|
|
410
390
|
await wallet.switchChain(`0x${targetChainId.toString(16)}`);
|
|
391
|
+
console.log(`\u2705 Successfully switched to chain ${targetChainId}`);
|
|
411
392
|
} catch (error) {
|
|
412
|
-
console.
|
|
393
|
+
console.error("\u274C Failed to switch chain:", error);
|
|
394
|
+
const targetNetworkName = networkNames[targetChainId] || selectedRequirements.network;
|
|
395
|
+
const wrappedError = wrapPaymentError(error);
|
|
396
|
+
let finalError;
|
|
397
|
+
if (wrappedError.code === "USER_REJECTED" /* USER_REJECTED */) {
|
|
398
|
+
finalError = new PaymentOperationError({
|
|
399
|
+
code: wrappedError.code,
|
|
400
|
+
message: wrappedError.message,
|
|
401
|
+
userMessage: `You rejected the network switch request. Please switch to ${targetNetworkName} manually.`,
|
|
402
|
+
originalError: wrappedError.originalError
|
|
403
|
+
});
|
|
404
|
+
} else {
|
|
405
|
+
finalError = new PaymentOperationError({
|
|
406
|
+
code: "NETWORK_SWITCH_FAILED" /* NETWORK_SWITCH_FAILED */,
|
|
407
|
+
message: wrappedError.message,
|
|
408
|
+
userMessage: `Failed to switch to ${targetNetworkName}. Please switch manually in your wallet.`,
|
|
409
|
+
originalError: wrappedError.originalError
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
throw finalError;
|
|
413
|
+
}
|
|
414
|
+
} else if (wallet.switchChain && !currentChainId) {
|
|
415
|
+
try {
|
|
416
|
+
console.log(`\u{1F504} Attempting to switch to chain ${targetChainId}...`);
|
|
417
|
+
await wallet.switchChain(`0x${targetChainId.toString(16)}`);
|
|
418
|
+
console.log(`\u2705 Switch attempted successfully`);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
console.warn("\u26A0\uFE0F Failed to switch chain (best effort):", error);
|
|
413
421
|
}
|
|
414
422
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
423
|
+
let paymentHeader;
|
|
424
|
+
try {
|
|
425
|
+
paymentHeader = await createEvmPaymentHeader({
|
|
426
|
+
wallet,
|
|
427
|
+
paymentRequirements: selectedRequirements,
|
|
428
|
+
x402Version,
|
|
429
|
+
chainId: targetChainId
|
|
430
|
+
});
|
|
431
|
+
} catch (error) {
|
|
432
|
+
console.error("\u274C Failed to create payment header:", error);
|
|
433
|
+
throw wrapPaymentError(error);
|
|
434
|
+
}
|
|
421
435
|
const newInit = {
|
|
422
436
|
...requestInit,
|
|
423
437
|
method: requestInit?.method || "POST",
|
|
@@ -427,7 +441,38 @@ async function handleEvmPayment(endpoint, config, requestInit) {
|
|
|
427
441
|
"Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE"
|
|
428
442
|
}
|
|
429
443
|
};
|
|
430
|
-
|
|
444
|
+
const retryResponse = await fetch(endpoint, newInit);
|
|
445
|
+
if (retryResponse.status === 402) {
|
|
446
|
+
try {
|
|
447
|
+
const retryData = await retryResponse.json();
|
|
448
|
+
const IGNORED_ERRORS2 = [
|
|
449
|
+
"X-PAYMENT header is required",
|
|
450
|
+
"missing X-PAYMENT header",
|
|
451
|
+
"payment_required"
|
|
452
|
+
];
|
|
453
|
+
if (retryData.error && !IGNORED_ERRORS2.includes(retryData.error)) {
|
|
454
|
+
console.error(`\u274C Payment verification failed: ${retryData.error}`);
|
|
455
|
+
const ERROR_MESSAGES = {
|
|
456
|
+
"insufficient_funds": "Insufficient balance to complete this payment",
|
|
457
|
+
"invalid_signature": "Invalid payment signature",
|
|
458
|
+
"expired": "Payment authorization has expired",
|
|
459
|
+
"already_used": "This payment has already been used",
|
|
460
|
+
"network_mismatch": "Payment network does not match",
|
|
461
|
+
"invalid_payment": "Invalid payment data",
|
|
462
|
+
"verification_failed": "Payment verification failed"
|
|
463
|
+
};
|
|
464
|
+
const errorMessage = ERROR_MESSAGES[retryData.error] || `Payment failed: ${retryData.error}`;
|
|
465
|
+
const error = new Error(errorMessage);
|
|
466
|
+
throw wrapPaymentError(error);
|
|
467
|
+
}
|
|
468
|
+
} catch (error) {
|
|
469
|
+
if (error instanceof PaymentOperationError) {
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
console.warn("\u26A0\uFE0F Could not parse retry 402 response:", error);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return retryResponse;
|
|
431
476
|
}
|
|
432
477
|
function createEvmPaymentFetch(config) {
|
|
433
478
|
return async (input, init) => {
|
|
@@ -436,64 +481,6 @@ function createEvmPaymentFetch(config) {
|
|
|
436
481
|
};
|
|
437
482
|
}
|
|
438
483
|
|
|
439
|
-
// src/utils/wallet.ts
|
|
440
|
-
function isWalletInstalled(networkType) {
|
|
441
|
-
if (typeof window === "undefined") {
|
|
442
|
-
return false;
|
|
443
|
-
}
|
|
444
|
-
switch (networkType) {
|
|
445
|
-
case "evm" /* EVM */:
|
|
446
|
-
return !!window.ethereum;
|
|
447
|
-
case "solana" /* SOLANA */:
|
|
448
|
-
case "svm" /* SVM */:
|
|
449
|
-
return !!window.solana || !!window.phantom;
|
|
450
|
-
default:
|
|
451
|
-
return false;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
function getWalletProvider(networkType) {
|
|
455
|
-
if (typeof window === "undefined") {
|
|
456
|
-
return null;
|
|
457
|
-
}
|
|
458
|
-
switch (networkType) {
|
|
459
|
-
case "evm" /* EVM */:
|
|
460
|
-
return window.ethereum;
|
|
461
|
-
case "solana" /* SOLANA */:
|
|
462
|
-
case "svm" /* SVM */:
|
|
463
|
-
return window.solana || window.phantom;
|
|
464
|
-
default:
|
|
465
|
-
return null;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
function formatAddress(address) {
|
|
469
|
-
if (!address || address.length < 10) {
|
|
470
|
-
return address;
|
|
471
|
-
}
|
|
472
|
-
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
473
|
-
}
|
|
474
|
-
function getWalletInstallUrl(networkType) {
|
|
475
|
-
switch (networkType) {
|
|
476
|
-
case "evm" /* EVM */:
|
|
477
|
-
return "https://metamask.io/download/";
|
|
478
|
-
case "solana" /* SOLANA */:
|
|
479
|
-
case "svm" /* SVM */:
|
|
480
|
-
return "https://phantom.app/download";
|
|
481
|
-
default:
|
|
482
|
-
return "#";
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
function getWalletDisplayName(networkType) {
|
|
486
|
-
switch (networkType) {
|
|
487
|
-
case "evm" /* EVM */:
|
|
488
|
-
return "MetaMask";
|
|
489
|
-
case "solana" /* SOLANA */:
|
|
490
|
-
case "svm" /* SVM */:
|
|
491
|
-
return "Phantom";
|
|
492
|
-
default:
|
|
493
|
-
return "Unknown Wallet";
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
484
|
// src/utils/payment-helpers.ts
|
|
498
485
|
var import_ethers2 = require("ethers");
|
|
499
486
|
async function makePayment(networkType, merchantId, endpoint = PROD_BACK_URL) {
|
|
@@ -509,7 +496,8 @@ async function makePayment(networkType, merchantId, endpoint = PROD_BACK_URL) {
|
|
|
509
496
|
}
|
|
510
497
|
response = await handleSvmPayment(endpoint, {
|
|
511
498
|
wallet: solana,
|
|
512
|
-
network: "solana
|
|
499
|
+
network: "solana"
|
|
500
|
+
// Will use backend's network configuration
|
|
513
501
|
});
|
|
514
502
|
} else if (networkType === "evm" /* EVM */) {
|
|
515
503
|
if (!window.ethereum) {
|
|
@@ -521,12 +509,24 @@ async function makePayment(networkType, merchantId, endpoint = PROD_BACK_URL) {
|
|
|
521
509
|
address: await signer.getAddress(),
|
|
522
510
|
signTypedData: async (domain, types, message) => {
|
|
523
511
|
return await signer.signTypedData(domain, types, message);
|
|
512
|
+
},
|
|
513
|
+
// Get current chain ID from wallet
|
|
514
|
+
getChainId: async () => {
|
|
515
|
+
const network = await provider.getNetwork();
|
|
516
|
+
return `0x${network.chainId.toString(16)}`;
|
|
517
|
+
},
|
|
518
|
+
// Switch to a different chain
|
|
519
|
+
switchChain: async (chainId) => {
|
|
520
|
+
await window.ethereum.request({
|
|
521
|
+
method: "wallet_switchEthereumChain",
|
|
522
|
+
params: [{ chainId }]
|
|
523
|
+
});
|
|
524
524
|
}
|
|
525
525
|
};
|
|
526
|
-
const network = endpoint.includes("sepolia") ? "base-sepolia" : "base";
|
|
527
526
|
response = await handleEvmPayment(endpoint, {
|
|
528
527
|
wallet,
|
|
529
|
-
network
|
|
528
|
+
network: "base"
|
|
529
|
+
// Will use backend's network configuration
|
|
530
530
|
});
|
|
531
531
|
} else {
|
|
532
532
|
throw new Error(`\u4E0D\u652F\u6301\u7684\u7F51\u7EDC\u7C7B\u578B: ${networkType}`);
|
|
@@ -601,6 +601,368 @@ function fromAtomicUnits(atomicUnits, decimals) {
|
|
|
601
601
|
function is402Response(response) {
|
|
602
602
|
return response && typeof response === "object" && "x402Version" in response && "accepts" in response && Array.isArray(response.accepts);
|
|
603
603
|
}
|
|
604
|
+
|
|
605
|
+
// src/utils/payment-error-handler.ts
|
|
606
|
+
function parsePaymentError(error) {
|
|
607
|
+
if (!error) {
|
|
608
|
+
return {
|
|
609
|
+
code: "UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
|
|
610
|
+
message: "Unknown error occurred",
|
|
611
|
+
userMessage: "An unknown error occurred. Please try again.",
|
|
612
|
+
originalError: error
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
const errorMessage = error.message || error.toString();
|
|
616
|
+
const errorCode = error.code;
|
|
617
|
+
if (errorCode === 4001 || errorCode === "ACTION_REJECTED" || errorMessage.includes("User rejected") || errorMessage.includes("user rejected") || errorMessage.includes("User denied") || errorMessage.includes("user denied") || errorMessage.includes("ethers-user-denied")) {
|
|
618
|
+
return {
|
|
619
|
+
code: "USER_REJECTED" /* USER_REJECTED */,
|
|
620
|
+
message: "User rejected the transaction",
|
|
621
|
+
userMessage: "You rejected the signature request. Please try again if you want to proceed.",
|
|
622
|
+
originalError: error
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
if (errorMessage.includes("chainId") && (errorMessage.includes("must match") || errorMessage.includes("does not match"))) {
|
|
626
|
+
const match = errorMessage.match(/chainId.*?"(\d+)".*?active.*?"(\d+)"/i) || errorMessage.match(/chain (\d+).*?chain (\d+)/i);
|
|
627
|
+
if (match) {
|
|
628
|
+
const [, requestedChain, activeChain] = match;
|
|
629
|
+
return {
|
|
630
|
+
code: "CHAIN_ID_MISMATCH" /* CHAIN_ID_MISMATCH */,
|
|
631
|
+
message: `Network mismatch (wallet is on different chain): Requested ${requestedChain}, but wallet is on ${activeChain}`,
|
|
632
|
+
userMessage: `Your wallet is on the wrong network. Please switch to the correct network and try again.`,
|
|
633
|
+
originalError: error
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
code: "CHAIN_ID_MISMATCH" /* CHAIN_ID_MISMATCH */,
|
|
638
|
+
message: "Network mismatch (wallet selected network does not match)",
|
|
639
|
+
userMessage: "Your wallet is on the wrong network. Please switch to the correct network.",
|
|
640
|
+
originalError: error
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
if (errorMessage.includes("Network mismatch") || errorMessage.includes("Wrong network") || errorMessage.includes("Incorrect network")) {
|
|
644
|
+
return {
|
|
645
|
+
code: "NETWORK_MISMATCH" /* NETWORK_MISMATCH */,
|
|
646
|
+
message: errorMessage,
|
|
647
|
+
userMessage: "Please switch your wallet to the correct network.",
|
|
648
|
+
originalError: error
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
if (errorMessage.includes("locked") || errorMessage.includes("Wallet is locked")) {
|
|
652
|
+
return {
|
|
653
|
+
code: "WALLET_LOCKED" /* WALLET_LOCKED */,
|
|
654
|
+
message: "Wallet is locked",
|
|
655
|
+
userMessage: "Please unlock your wallet and try again.",
|
|
656
|
+
originalError: error
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
if (errorMessage.includes("insufficient") && (errorMessage.includes("balance") || errorMessage.includes("funds"))) {
|
|
660
|
+
return {
|
|
661
|
+
code: "INSUFFICIENT_BALANCE" /* INSUFFICIENT_BALANCE */,
|
|
662
|
+
message: "Insufficient balance",
|
|
663
|
+
userMessage: "You don't have enough balance to complete this payment.",
|
|
664
|
+
originalError: error
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
if (errorMessage.includes("Failed to switch") || errorMessage.includes("switch chain")) {
|
|
668
|
+
return {
|
|
669
|
+
code: "NETWORK_SWITCH_FAILED" /* NETWORK_SWITCH_FAILED */,
|
|
670
|
+
message: errorMessage,
|
|
671
|
+
userMessage: "Failed to switch network. Please switch manually in your wallet.",
|
|
672
|
+
originalError: error
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
if (errorMessage.includes("not connected") || errorMessage.includes("No wallet") || errorMessage.includes("Connect wallet")) {
|
|
676
|
+
return {
|
|
677
|
+
code: "WALLET_NOT_CONNECTED" /* WALLET_NOT_CONNECTED */,
|
|
678
|
+
message: "Wallet not connected",
|
|
679
|
+
userMessage: "Please connect your wallet first.",
|
|
680
|
+
originalError: error
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
if (errorMessage.includes("No suitable") || errorMessage.includes("payment requirements") || errorMessage.includes("Missing payTo") || errorMessage.includes("Missing asset")) {
|
|
684
|
+
return {
|
|
685
|
+
code: "INVALID_PAYMENT_REQUIREMENTS" /* INVALID_PAYMENT_REQUIREMENTS */,
|
|
686
|
+
message: errorMessage,
|
|
687
|
+
userMessage: "Invalid payment configuration. Please contact support.",
|
|
688
|
+
originalError: error
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
if (errorMessage.includes("exceeds maximum")) {
|
|
692
|
+
return {
|
|
693
|
+
code: "AMOUNT_EXCEEDED" /* AMOUNT_EXCEEDED */,
|
|
694
|
+
message: errorMessage,
|
|
695
|
+
userMessage: "Payment amount exceeds the maximum allowed.",
|
|
696
|
+
originalError: error
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
if (errorMessage.includes("signature") || errorMessage.includes("sign") || errorCode === "UNKNOWN_ERROR") {
|
|
700
|
+
return {
|
|
701
|
+
code: "SIGNATURE_FAILED" /* SIGNATURE_FAILED */,
|
|
702
|
+
message: errorMessage,
|
|
703
|
+
userMessage: "Failed to sign the transaction. Please try again.",
|
|
704
|
+
originalError: error
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
code: "UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
|
|
709
|
+
message: errorMessage,
|
|
710
|
+
userMessage: "An unexpected error occurred. Please try again or contact support.",
|
|
711
|
+
originalError: error
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
var PaymentOperationError = class _PaymentOperationError extends Error {
|
|
715
|
+
constructor(paymentError) {
|
|
716
|
+
super(paymentError.message);
|
|
717
|
+
this.name = "PaymentOperationError";
|
|
718
|
+
this.code = paymentError.code;
|
|
719
|
+
this.userMessage = paymentError.userMessage;
|
|
720
|
+
this.originalError = paymentError.originalError;
|
|
721
|
+
if (Error.captureStackTrace) {
|
|
722
|
+
Error.captureStackTrace(this, _PaymentOperationError);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Get a formatted error message for logging
|
|
727
|
+
*/
|
|
728
|
+
toLogString() {
|
|
729
|
+
return `[${this.code}] ${this.message} | User Message: ${this.userMessage}`;
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
function wrapPaymentError(error) {
|
|
733
|
+
const parsedError = parsePaymentError(error);
|
|
734
|
+
return new PaymentOperationError(parsedError);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/services/svm/payment-header.ts
|
|
738
|
+
async function createSvmPaymentHeader(params) {
|
|
739
|
+
const { wallet, paymentRequirements, x402Version, rpcUrl } = params;
|
|
740
|
+
const connection = new import_web3.Connection(rpcUrl, "confirmed");
|
|
741
|
+
const feePayer = paymentRequirements?.extra?.feePayer;
|
|
742
|
+
if (typeof feePayer !== "string" || !feePayer) {
|
|
743
|
+
throw new Error("Missing facilitator feePayer in payment requirements (extra.feePayer).");
|
|
744
|
+
}
|
|
745
|
+
const feePayerPubkey = new import_web3.PublicKey(feePayer);
|
|
746
|
+
const walletAddress = wallet?.publicKey?.toString() || wallet?.address;
|
|
747
|
+
if (!walletAddress) {
|
|
748
|
+
throw new Error("Missing connected Solana wallet address or publicKey");
|
|
749
|
+
}
|
|
750
|
+
const userPubkey = new import_web3.PublicKey(walletAddress);
|
|
751
|
+
if (!paymentRequirements?.payTo) {
|
|
752
|
+
throw new Error("Missing payTo in payment requirements");
|
|
753
|
+
}
|
|
754
|
+
const destination = new import_web3.PublicKey(paymentRequirements.payTo);
|
|
755
|
+
const instructions = [];
|
|
756
|
+
instructions.push(
|
|
757
|
+
import_web3.ComputeBudgetProgram.setComputeUnitLimit({
|
|
758
|
+
units: 7e3
|
|
759
|
+
// Sufficient for SPL token transfer
|
|
760
|
+
})
|
|
761
|
+
);
|
|
762
|
+
instructions.push(
|
|
763
|
+
import_web3.ComputeBudgetProgram.setComputeUnitPrice({
|
|
764
|
+
microLamports: 1
|
|
765
|
+
// Minimal price
|
|
766
|
+
})
|
|
767
|
+
);
|
|
768
|
+
if (!paymentRequirements.asset) {
|
|
769
|
+
throw new Error("Missing token mint for SPL transfer");
|
|
770
|
+
}
|
|
771
|
+
const mintPubkey = new import_web3.PublicKey(paymentRequirements.asset);
|
|
772
|
+
const mintInfo = await connection.getAccountInfo(mintPubkey, "confirmed");
|
|
773
|
+
const programId = mintInfo?.owner?.toBase58() === import_spl_token.TOKEN_2022_PROGRAM_ID.toBase58() ? import_spl_token.TOKEN_2022_PROGRAM_ID : import_spl_token.TOKEN_PROGRAM_ID;
|
|
774
|
+
const mint = await (0, import_spl_token.getMint)(connection, mintPubkey, void 0, programId);
|
|
775
|
+
const sourceAta = await (0, import_spl_token.getAssociatedTokenAddress)(
|
|
776
|
+
mintPubkey,
|
|
777
|
+
userPubkey,
|
|
778
|
+
false,
|
|
779
|
+
programId
|
|
780
|
+
);
|
|
781
|
+
const destinationAta = await (0, import_spl_token.getAssociatedTokenAddress)(
|
|
782
|
+
mintPubkey,
|
|
783
|
+
destination,
|
|
784
|
+
false,
|
|
785
|
+
programId
|
|
786
|
+
);
|
|
787
|
+
const sourceAtaInfo = await connection.getAccountInfo(sourceAta, "confirmed");
|
|
788
|
+
if (!sourceAtaInfo) {
|
|
789
|
+
throw new Error(
|
|
790
|
+
`User does not have an Associated Token Account for ${paymentRequirements.asset}. Please create one first or ensure you have the required token.`
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
const destAtaInfo = await connection.getAccountInfo(destinationAta, "confirmed");
|
|
794
|
+
if (!destAtaInfo) {
|
|
795
|
+
throw new Error(
|
|
796
|
+
`Destination does not have an Associated Token Account for ${paymentRequirements.asset}. The receiver must create their token account before receiving payments.`
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
const amount = BigInt(paymentRequirements.maxAmountRequired);
|
|
800
|
+
instructions.push(
|
|
801
|
+
(0, import_spl_token.createTransferCheckedInstruction)(
|
|
802
|
+
sourceAta,
|
|
803
|
+
mintPubkey,
|
|
804
|
+
destinationAta,
|
|
805
|
+
userPubkey,
|
|
806
|
+
amount,
|
|
807
|
+
mint.decimals,
|
|
808
|
+
[],
|
|
809
|
+
programId
|
|
810
|
+
)
|
|
811
|
+
);
|
|
812
|
+
const { blockhash } = await connection.getLatestBlockhash("confirmed");
|
|
813
|
+
const message = new import_web3.TransactionMessage({
|
|
814
|
+
payerKey: feePayerPubkey,
|
|
815
|
+
recentBlockhash: blockhash,
|
|
816
|
+
instructions
|
|
817
|
+
}).compileToV0Message();
|
|
818
|
+
const transaction = new import_web3.VersionedTransaction(message);
|
|
819
|
+
if (typeof wallet?.signTransaction !== "function") {
|
|
820
|
+
throw new Error("Connected wallet does not support signTransaction");
|
|
821
|
+
}
|
|
822
|
+
let userSignedTx;
|
|
823
|
+
try {
|
|
824
|
+
userSignedTx = await wallet.signTransaction(transaction);
|
|
825
|
+
console.log("\u2705 Transaction signed successfully");
|
|
826
|
+
} catch (error) {
|
|
827
|
+
console.error("\u274C Failed to sign transaction:", error);
|
|
828
|
+
throw wrapPaymentError(error);
|
|
829
|
+
}
|
|
830
|
+
const serializedTransaction = Buffer.from(userSignedTx.serialize()).toString("base64");
|
|
831
|
+
const paymentPayload = {
|
|
832
|
+
x402Version,
|
|
833
|
+
scheme: paymentRequirements.scheme,
|
|
834
|
+
network: paymentRequirements.network,
|
|
835
|
+
payload: {
|
|
836
|
+
transaction: serializedTransaction
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
const paymentHeader = Buffer.from(JSON.stringify(paymentPayload)).toString("base64");
|
|
840
|
+
return paymentHeader;
|
|
841
|
+
}
|
|
842
|
+
function getDefaultSolanaRpcUrl(network) {
|
|
843
|
+
const normalized = network.toLowerCase();
|
|
844
|
+
if (normalized === "solana" || normalized === "solana-mainnet") {
|
|
845
|
+
return "https://cathee-fu8ezd-fast-mainnet.helius-rpc.com";
|
|
846
|
+
} else if (normalized === "solana-devnet") {
|
|
847
|
+
return "https://api.devnet.solana.com";
|
|
848
|
+
}
|
|
849
|
+
throw new Error(`Unsupported Solana network: ${network}`);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// src/services/svm/payment-handler.ts
|
|
853
|
+
async function handleSvmPayment(endpoint, config, requestInit) {
|
|
854
|
+
const { wallet, network, rpcUrl, maxPaymentAmount } = config;
|
|
855
|
+
const initialResponse = await fetch(endpoint, {
|
|
856
|
+
...requestInit,
|
|
857
|
+
method: requestInit?.method || "POST"
|
|
858
|
+
});
|
|
859
|
+
if (initialResponse.status !== 402) {
|
|
860
|
+
return initialResponse;
|
|
861
|
+
}
|
|
862
|
+
const rawResponse = await initialResponse.json();
|
|
863
|
+
const IGNORED_ERRORS = [
|
|
864
|
+
"X-PAYMENT header is required",
|
|
865
|
+
"missing X-PAYMENT header",
|
|
866
|
+
"payment_required"
|
|
867
|
+
];
|
|
868
|
+
if (rawResponse.error && !IGNORED_ERRORS.includes(rawResponse.error)) {
|
|
869
|
+
console.error(`\u274C Payment verification failed: ${rawResponse.error}`);
|
|
870
|
+
const ERROR_MESSAGES = {
|
|
871
|
+
"insufficient_funds": "Insufficient balance to complete this payment",
|
|
872
|
+
"invalid_signature": "Invalid payment signature",
|
|
873
|
+
"expired": "Payment authorization has expired",
|
|
874
|
+
"already_used": "This payment has already been used",
|
|
875
|
+
"network_mismatch": "Payment network does not match",
|
|
876
|
+
"invalid_payment": "Invalid payment data",
|
|
877
|
+
"verification_failed": "Payment verification failed"
|
|
878
|
+
};
|
|
879
|
+
const errorMessage = ERROR_MESSAGES[rawResponse.error] || `Payment failed: ${rawResponse.error}`;
|
|
880
|
+
const error = new Error(errorMessage);
|
|
881
|
+
throw wrapPaymentError(error);
|
|
882
|
+
}
|
|
883
|
+
const x402Version = rawResponse.x402Version;
|
|
884
|
+
const parsedPaymentRequirements = rawResponse.accepts || [];
|
|
885
|
+
const selectedRequirements = parsedPaymentRequirements.find(
|
|
886
|
+
(req) => req.scheme === "exact" && SolanaNetworkSchema.safeParse(req.network.toLowerCase()).success
|
|
887
|
+
);
|
|
888
|
+
if (!selectedRequirements) {
|
|
889
|
+
console.error(
|
|
890
|
+
"\u274C No suitable Solana payment requirements found. Available networks:",
|
|
891
|
+
parsedPaymentRequirements.map((req) => req.network)
|
|
892
|
+
);
|
|
893
|
+
throw new Error("No suitable Solana payment requirements found");
|
|
894
|
+
}
|
|
895
|
+
if (maxPaymentAmount && maxPaymentAmount > BigInt(0)) {
|
|
896
|
+
if (BigInt(selectedRequirements.maxAmountRequired) > maxPaymentAmount) {
|
|
897
|
+
throw new Error(
|
|
898
|
+
`Payment amount ${selectedRequirements.maxAmountRequired} exceeds maximum allowed ${maxPaymentAmount}`
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const effectiveRpcUrl = rpcUrl || getDefaultSolanaRpcUrl(selectedRequirements.network);
|
|
903
|
+
console.log(`\u{1F4CD} Using Solana RPC: ${effectiveRpcUrl.substring(0, 40)}...`);
|
|
904
|
+
console.log(`\u{1F4CD} Network from backend: ${selectedRequirements.network}`);
|
|
905
|
+
let paymentHeader;
|
|
906
|
+
try {
|
|
907
|
+
paymentHeader = await createSvmPaymentHeader({
|
|
908
|
+
wallet,
|
|
909
|
+
paymentRequirements: selectedRequirements,
|
|
910
|
+
x402Version,
|
|
911
|
+
rpcUrl: effectiveRpcUrl
|
|
912
|
+
});
|
|
913
|
+
console.log("\u2705 Payment header created successfully");
|
|
914
|
+
} catch (error) {
|
|
915
|
+
console.error("\u274C Failed to create payment header:", error);
|
|
916
|
+
throw wrapPaymentError(error);
|
|
917
|
+
}
|
|
918
|
+
const newInit = {
|
|
919
|
+
...requestInit,
|
|
920
|
+
method: requestInit?.method || "POST",
|
|
921
|
+
headers: {
|
|
922
|
+
...requestInit?.headers || {},
|
|
923
|
+
"X-PAYMENT": paymentHeader,
|
|
924
|
+
"Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE"
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
const retryResponse = await fetch(endpoint, newInit);
|
|
928
|
+
if (retryResponse.status === 402) {
|
|
929
|
+
try {
|
|
930
|
+
const retryData = await retryResponse.json();
|
|
931
|
+
const IGNORED_ERRORS2 = [
|
|
932
|
+
"X-PAYMENT header is required",
|
|
933
|
+
"missing X-PAYMENT header",
|
|
934
|
+
"payment_required"
|
|
935
|
+
];
|
|
936
|
+
if (retryData.error && !IGNORED_ERRORS2.includes(retryData.error)) {
|
|
937
|
+
console.error(`\u274C Payment verification failed: ${retryData.error}`);
|
|
938
|
+
const ERROR_MESSAGES = {
|
|
939
|
+
"insufficient_funds": "Insufficient balance to complete this payment",
|
|
940
|
+
"invalid_signature": "Invalid payment signature",
|
|
941
|
+
"expired": "Payment authorization has expired",
|
|
942
|
+
"already_used": "This payment has already been used",
|
|
943
|
+
"network_mismatch": "Payment network does not match",
|
|
944
|
+
"invalid_payment": "Invalid payment data",
|
|
945
|
+
"verification_failed": "Payment verification failed"
|
|
946
|
+
};
|
|
947
|
+
const errorMessage = ERROR_MESSAGES[retryData.error] || `Payment failed: ${retryData.error}`;
|
|
948
|
+
const error = new Error(errorMessage);
|
|
949
|
+
throw wrapPaymentError(error);
|
|
950
|
+
}
|
|
951
|
+
} catch (error) {
|
|
952
|
+
if (error instanceof PaymentOperationError) {
|
|
953
|
+
throw error;
|
|
954
|
+
}
|
|
955
|
+
console.warn("\u26A0\uFE0F Could not parse retry 402 response:", error);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return retryResponse;
|
|
959
|
+
}
|
|
960
|
+
function createSvmPaymentFetch(config) {
|
|
961
|
+
return async (input, init) => {
|
|
962
|
+
const endpoint = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
963
|
+
return handleSvmPayment(endpoint, config, init);
|
|
964
|
+
};
|
|
965
|
+
}
|
|
604
966
|
// Annotate the CommonJS export names for ESM import in node:
|
|
605
967
|
0 && (module.exports = {
|
|
606
968
|
EVM_NETWORK_CONFIGS,
|