@voyage_ai/v402-web-ts 0.1.0 → 0.1.2

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