@atxp/client 0.9.0 → 0.9.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.cjs CHANGED
@@ -295,25 +295,153 @@ class OAuthClient extends common.OAuthResourceClient {
295
295
  }
296
296
  }
297
297
 
298
- class InsufficientFundsError extends Error {
298
+ /**
299
+ * Base class for all ATXP payment errors with structured error codes and actionable guidance
300
+ */
301
+ class ATXPPaymentError extends Error {
302
+ constructor(message, context) {
303
+ super(message);
304
+ this.context = context;
305
+ this.name = this.constructor.name;
306
+ }
307
+ }
308
+ /**
309
+ * Thrown when the user's wallet has insufficient funds for a payment
310
+ */
311
+ class InsufficientFundsError extends ATXPPaymentError {
299
312
  constructor(currency, required, available, network) {
313
+ const shortfall = available ? required.minus(available).toString() : required.toString();
300
314
  const availableText = available ? `, Available: ${available}` : '';
301
315
  const networkText = network ? ` on ${network}` : '';
302
316
  super(`Payment failed due to insufficient ${currency} funds${networkText}. ` +
303
317
  `Required: ${required}${availableText}. ` +
304
- `Please ensure your account has adequate balance before retrying.`);
318
+ `Please ensure your account has adequate balance before retrying.`, { currency, required: required.toString(), available: available?.toString(), network, shortfall });
305
319
  this.currency = currency;
306
320
  this.required = required;
307
321
  this.available = available;
308
322
  this.network = network;
309
- this.name = 'InsufficientFundsError';
323
+ this.code = 'INSUFFICIENT_FUNDS';
324
+ this.retryable = true;
325
+ this.actionableMessage = available
326
+ ? `Add at least ${shortfall} ${currency} to your ${network} wallet and try again.`
327
+ : `Ensure your ${network} wallet has at least ${required} ${currency} and try again.`;
328
+ }
329
+ }
330
+ /**
331
+ * Thrown when a blockchain transaction is reverted
332
+ */
333
+ class TransactionRevertedError extends ATXPPaymentError {
334
+ constructor(transactionHash, network, revertReason) {
335
+ super(`Transaction ${transactionHash} reverted on ${network}${revertReason ? `: ${revertReason}` : ''}`, { transactionHash, network, revertReason });
336
+ this.transactionHash = transactionHash;
337
+ this.network = network;
338
+ this.revertReason = revertReason;
339
+ this.code = 'TRANSACTION_REVERTED';
340
+ this.retryable = false;
341
+ // Provide specific guidance based on revert reason
342
+ if (revertReason?.toLowerCase().includes('allowance')) {
343
+ this.actionableMessage = 'Approve token spending before making the payment. You may need to increase the token allowance.';
344
+ }
345
+ else if (revertReason?.toLowerCase().includes('balance')) {
346
+ this.actionableMessage = 'Ensure your wallet has sufficient token balance and native token for gas fees.';
347
+ }
348
+ else {
349
+ this.actionableMessage = 'The transaction was rejected by the blockchain. Check the transaction details on a block explorer and verify your wallet settings.';
350
+ }
351
+ }
352
+ }
353
+ /**
354
+ * Thrown when an unsupported currency is requested
355
+ */
356
+ class UnsupportedCurrencyError extends ATXPPaymentError {
357
+ constructor(currency, network, supportedCurrencies) {
358
+ super(`Currency ${currency} is not supported on ${network}`, { currency, network, supportedCurrencies });
359
+ this.currency = currency;
360
+ this.network = network;
361
+ this.supportedCurrencies = supportedCurrencies;
362
+ this.code = 'UNSUPPORTED_CURRENCY';
363
+ this.retryable = false;
364
+ this.actionableMessage = `Please use one of the supported currencies: ${supportedCurrencies.join(', ')}`;
365
+ }
366
+ }
367
+ /**
368
+ * Thrown when gas estimation fails for a transaction
369
+ */
370
+ class GasEstimationError extends ATXPPaymentError {
371
+ constructor(network, reason) {
372
+ super(`Failed to estimate gas on ${network}${reason ? `: ${reason}` : ''}`, { network, reason });
373
+ this.network = network;
374
+ this.reason = reason;
375
+ this.code = 'GAS_ESTIMATION_FAILED';
376
+ this.retryable = true;
377
+ this.actionableMessage = 'Unable to estimate gas for this transaction. Ensure you have sufficient funds for both the payment amount and gas fees, then try again.';
378
+ }
379
+ }
380
+ /**
381
+ * Thrown when RPC/network connectivity fails
382
+ */
383
+ class RpcError extends ATXPPaymentError {
384
+ constructor(network, rpcUrl, originalError) {
385
+ super(`RPC call failed on ${network}${rpcUrl ? ` (${rpcUrl})` : ''}`, { network, rpcUrl, originalError: originalError?.message });
386
+ this.network = network;
387
+ this.rpcUrl = rpcUrl;
388
+ this.originalError = originalError;
389
+ this.code = 'RPC_ERROR';
390
+ this.retryable = true;
391
+ this.actionableMessage = 'Unable to connect to the blockchain network. Please check your internet connection and try again.';
392
+ }
393
+ }
394
+ /**
395
+ * Thrown when the user rejects a transaction in their wallet
396
+ */
397
+ class UserRejectedError extends ATXPPaymentError {
398
+ constructor(network) {
399
+ super(`User rejected transaction on ${network}`, { network });
400
+ this.network = network;
401
+ this.code = 'USER_REJECTED';
402
+ this.retryable = true;
403
+ this.actionableMessage = 'You cancelled the transaction. To complete the payment, please approve the transaction in your wallet.';
404
+ }
405
+ }
406
+ /**
407
+ * Thrown when the payment server returns an error
408
+ */
409
+ class PaymentServerError extends ATXPPaymentError {
410
+ constructor(statusCode, endpoint, serverMessage, errorCode, details) {
411
+ super(`Payment server returned ${statusCode} from ${endpoint}${serverMessage ? `: ${serverMessage}` : ''}`, { statusCode, endpoint, serverMessage, errorCode, details });
412
+ this.statusCode = statusCode;
413
+ this.endpoint = endpoint;
414
+ this.serverMessage = serverMessage;
415
+ this.details = details;
416
+ this.retryable = true;
417
+ this.actionableMessage = 'The payment server encountered an error. Please try again in a few moments.';
418
+ this.code = errorCode || 'PAYMENT_SERVER_ERROR';
310
419
  }
311
420
  }
312
- class PaymentNetworkError extends Error {
313
- constructor(message, originalError) {
314
- super(`Payment failed due to network error: ${message}`);
421
+ /**
422
+ * Thrown when a payment request has expired
423
+ */
424
+ class PaymentExpiredError extends ATXPPaymentError {
425
+ constructor(paymentRequestId, expiresAt) {
426
+ super(`Payment request ${paymentRequestId} has expired`, { paymentRequestId, expiresAt: expiresAt?.toISOString() });
427
+ this.paymentRequestId = paymentRequestId;
428
+ this.expiresAt = expiresAt;
429
+ this.code = 'PAYMENT_EXPIRED';
430
+ this.retryable = false;
431
+ this.actionableMessage = 'This payment request has expired. Please make a new request to the service.';
432
+ }
433
+ }
434
+ /**
435
+ * Generic network error for backward compatibility and uncategorized errors
436
+ */
437
+ class PaymentNetworkError extends ATXPPaymentError {
438
+ constructor(network, message, originalError) {
439
+ super(`Payment failed on ${network} network: ${message}`, { network, originalError: originalError?.message });
440
+ this.network = network;
315
441
  this.originalError = originalError;
316
- this.name = 'PaymentNetworkError';
442
+ this.code = 'NETWORK_ERROR';
443
+ this.retryable = true;
444
+ this.actionableMessage = 'A network error occurred during payment processing. Please try again.';
317
445
  }
318
446
  }
319
447
 
@@ -338,27 +466,41 @@ function atxpFetch(config) {
338
466
  onAuthorize: config.onAuthorize,
339
467
  onAuthorizeFailure: config.onAuthorizeFailure,
340
468
  onPayment: config.onPayment,
341
- onPaymentFailure: config.onPaymentFailure
469
+ onPaymentFailure: config.onPaymentFailure,
470
+ onPaymentAttemptFailed: config.onPaymentAttemptFailed
342
471
  });
343
472
  return fetcher.fetch;
344
473
  }
345
474
  class ATXPFetcher {
346
475
  constructor(config) {
347
- this.defaultPaymentFailureHandler = async ({ payment, error }) => {
476
+ this.defaultPaymentFailureHandler = async (context) => {
477
+ const { payment, error, attemptedNetworks, retryable } = context;
478
+ const recoveryHint = common.getErrorRecoveryHint(error);
479
+ this.logger.info(`PAYMENT FAILED: ${recoveryHint.title}`);
480
+ this.logger.info(`Description: ${recoveryHint.description}`);
481
+ if (attemptedNetworks.length > 0) {
482
+ this.logger.info(`Attempted networks: ${attemptedNetworks.join(', ')}`);
483
+ }
484
+ this.logger.info(`Account: ${payment.accountId}`);
485
+ // Log actionable guidance
486
+ if (recoveryHint.actions.length > 0) {
487
+ this.logger.info(`What to do:`);
488
+ recoveryHint.actions.forEach((action, index) => {
489
+ this.logger.info(` ${index + 1}. ${action}`);
490
+ });
491
+ }
492
+ if (retryable) {
493
+ this.logger.info(`This payment can be retried.`);
494
+ }
495
+ // Log additional context for specific error types
348
496
  if (error instanceof InsufficientFundsError) {
349
- const networkText = error.network ? ` on ${error.network}` : '';
350
- this.logger.info(`PAYMENT FAILED: Insufficient ${error.currency} funds${networkText}`);
351
497
  this.logger.info(`Required: ${error.required} ${error.currency}`);
352
498
  if (error.available) {
353
499
  this.logger.info(`Available: ${error.available} ${error.currency}`);
354
500
  }
355
- this.logger.info(`Account: ${payment.accountId}`);
356
501
  }
357
- else if (error instanceof PaymentNetworkError) {
358
- this.logger.info(`PAYMENT FAILED: Network error: ${error.message}`);
359
- }
360
- else {
361
- this.logger.info(`PAYMENT FAILED: ${error.message}`);
502
+ else if (error instanceof ATXPPaymentError && error.context) {
503
+ this.logger.debug(`Error context: ${JSON.stringify(error.context)}`);
362
504
  }
363
505
  };
364
506
  this.handleMultiDestinationPayment = async (paymentRequest, paymentRequestUrl, paymentRequestId) => {
@@ -403,9 +545,11 @@ class ATXPFetcher {
403
545
  this.logger.info(`ATXP: payment request denied by callback function`);
404
546
  return false;
405
547
  }
406
- // Try each payment maker in order
548
+ // Try each payment maker in order, tracking attempts
407
549
  let lastPaymentError = null;
408
550
  let paymentAttempted = false;
551
+ const attemptedNetworks = [];
552
+ const failureReasons = new Map();
409
553
  for (const paymentMaker of this.account.paymentMakers) {
410
554
  try {
411
555
  // Pass all destinations to payment maker - it will filter and pick the one it can handle
@@ -417,7 +561,11 @@ class ATXPFetcher {
417
561
  paymentAttempted = true;
418
562
  // Payment was successful
419
563
  this.logger.info(`ATXP: made payment of ${firstDest.amount.toString()} ${firstDest.currency} on ${result.chain}: ${result.transactionId}`);
420
- await this.onPayment({ payment: prospectivePayment });
564
+ await this.onPayment({
565
+ payment: prospectivePayment,
566
+ transactionHash: result.transactionId,
567
+ network: result.chain
568
+ });
421
569
  // Submit payment to the server
422
570
  const jwt = await paymentMaker.generateJWT({ paymentRequestId, codeChallenge: '', accountId: this.account.accountId });
423
571
  const response = await this.sideChannelFetch(paymentRequestUrl.toString(), {
@@ -445,13 +593,41 @@ class ATXPFetcher {
445
593
  const typedError = error;
446
594
  paymentAttempted = true;
447
595
  lastPaymentError = typedError;
448
- this.logger.warn(`ATXP: payment maker failed: ${typedError.message}`);
449
- await this.onPaymentFailure({ payment: prospectivePayment, error: typedError });
596
+ // Extract network from error context if available
597
+ let network = 'unknown';
598
+ if (typedError instanceof ATXPPaymentError && typedError.context?.network) {
599
+ network = typeof typedError.context.network === 'string' ? typedError.context.network : 'unknown';
600
+ }
601
+ attemptedNetworks.push(network);
602
+ failureReasons.set(network, typedError);
603
+ this.logger.warn(`ATXP: payment maker failed on ${network}: ${typedError.message}`);
604
+ // Call optional per-attempt failure callback
605
+ if (this.onPaymentAttemptFailed) {
606
+ const remainingMakers = this.account.paymentMakers.length - (attemptedNetworks.length);
607
+ const remainingNetworks = remainingMakers > 0 ? ['next available'] : [];
608
+ await this.onPaymentAttemptFailed({
609
+ network,
610
+ error: typedError,
611
+ remainingNetworks
612
+ });
613
+ }
450
614
  // Continue to next payment maker
451
615
  }
452
616
  }
453
- // If payment was attempted but all failed, rethrow the last error
617
+ // If payment was attempted but all failed, create full context and call onPaymentFailure
454
618
  if (paymentAttempted && lastPaymentError) {
619
+ const isRetryable = lastPaymentError instanceof ATXPPaymentError
620
+ ? lastPaymentError.retryable
621
+ : true; // Default to retryable for unknown errors
622
+ const failureContext = {
623
+ payment: prospectivePayment,
624
+ error: lastPaymentError,
625
+ attemptedNetworks,
626
+ failureReasons,
627
+ retryable: isRetryable,
628
+ timestamp: new Date()
629
+ };
630
+ await this.onPaymentFailure(failureContext);
455
631
  throw lastPaymentError;
456
632
  }
457
633
  this.logger.info(`ATXP: no payment maker could handle these destinations`);
@@ -699,7 +875,7 @@ class ATXPFetcher {
699
875
  throw error;
700
876
  }
701
877
  };
702
- const { account, db, destinationMakers, fetchFn = fetch, sideChannelFetch = fetchFn, strict = true, allowInsecureRequests = process.env.NODE_ENV === 'development', allowedAuthorizationServers = [common.DEFAULT_AUTHORIZATION_SERVER], approvePayment = async () => true, logger = new common.ConsoleLogger(), onAuthorize = async () => { }, onAuthorizeFailure = async () => { }, onPayment = async () => { }, onPaymentFailure = async () => { } } = config;
878
+ const { account, db, destinationMakers, fetchFn = fetch, sideChannelFetch = fetchFn, strict = true, allowInsecureRequests = process.env.NODE_ENV === 'development', allowedAuthorizationServers = [common.DEFAULT_AUTHORIZATION_SERVER], approvePayment = async () => true, logger = new common.ConsoleLogger(), onAuthorize = async () => { }, onAuthorizeFailure = async () => { }, onPayment = async () => { }, onPaymentFailure, onPaymentAttemptFailed } = config;
703
879
  // Use React Native safe fetch if in React Native environment
704
880
  const safeFetchFn = common.getIsReactNative() ? common.createReactNativeSafeFetch(fetchFn) : fetchFn;
705
881
  const safeSideChannelFetch = common.getIsReactNative() ? common.createReactNativeSafeFetch(sideChannelFetch) : sideChannelFetch;
@@ -728,6 +904,7 @@ class ATXPFetcher {
728
904
  this.onAuthorizeFailure = onAuthorizeFailure;
729
905
  this.onPayment = onPayment;
730
906
  this.onPaymentFailure = onPaymentFailure || this.defaultPaymentFailureHandler;
907
+ this.onPaymentAttemptFailed = onPaymentAttemptFailed;
731
908
  }
732
909
  }
733
910
 
@@ -16024,7 +16201,8 @@ const DEFAULT_CLIENT_CONFIG = {
16024
16201
  onAuthorize: async () => { },
16025
16202
  onAuthorizeFailure: async () => { },
16026
16203
  onPayment: async () => { },
16027
- onPaymentFailure: async () => { }
16204
+ onPaymentFailure: async () => { },
16205
+ onPaymentAttemptFailed: async () => { }
16028
16206
  };
16029
16207
  function buildClientConfig(args) {
16030
16208
  // Use fetchFn for oAuthChannelFetch if the latter isn't explicitly set
@@ -16338,18 +16516,26 @@ Object.defineProperty(exports, "ATXPAccount", {
16338
16516
  });
16339
16517
  exports.ATXPDestinationMaker = ATXPDestinationMaker;
16340
16518
  exports.ATXPLocalAccount = ATXPLocalAccount;
16519
+ exports.ATXPPaymentError = ATXPPaymentError;
16341
16520
  exports.DEFAULT_CLIENT_CONFIG = DEFAULT_CLIENT_CONFIG;
16521
+ exports.GasEstimationError = GasEstimationError;
16342
16522
  exports.InsufficientFundsError = InsufficientFundsError;
16343
16523
  exports.OAuthAuthenticationRequiredError = OAuthAuthenticationRequiredError;
16344
16524
  exports.OAuthClient = OAuthClient;
16345
16525
  exports.POLYGON_AMOY = POLYGON_AMOY;
16346
16526
  exports.POLYGON_MAINNET = POLYGON_MAINNET;
16347
16527
  exports.PassthroughDestinationMaker = PassthroughDestinationMaker;
16528
+ exports.PaymentExpiredError = PaymentExpiredError;
16348
16529
  exports.PaymentNetworkError = PaymentNetworkError;
16530
+ exports.PaymentServerError = PaymentServerError;
16531
+ exports.RpcError = RpcError;
16532
+ exports.TransactionRevertedError = TransactionRevertedError;
16349
16533
  exports.USDC_CONTRACT_ADDRESS_POLYGON_AMOY = USDC_CONTRACT_ADDRESS_POLYGON_AMOY;
16350
16534
  exports.USDC_CONTRACT_ADDRESS_POLYGON_MAINNET = USDC_CONTRACT_ADDRESS_POLYGON_MAINNET;
16351
16535
  exports.USDC_CONTRACT_ADDRESS_WORLD_MAINNET = USDC_CONTRACT_ADDRESS_WORLD_MAINNET;
16352
16536
  exports.USDC_CONTRACT_ADDRESS_WORLD_SEPOLIA = USDC_CONTRACT_ADDRESS_WORLD_SEPOLIA;
16537
+ exports.UnsupportedCurrencyError = UnsupportedCurrencyError;
16538
+ exports.UserRejectedError = UserRejectedError;
16353
16539
  exports.WORLD_CHAIN_MAINNET = WORLD_CHAIN_MAINNET;
16354
16540
  exports.WORLD_CHAIN_SEPOLIA = WORLD_CHAIN_SEPOLIA;
16355
16541
  exports.atxpClient = atxpClient;