@atxp/client 0.5.1 → 0.6.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/dist/index.cjs CHANGED
@@ -318,18 +318,39 @@ class InsufficientFundsError extends Error {
318
318
  }
319
319
  class PaymentNetworkError extends Error {
320
320
  constructor(message, originalError) {
321
- super(`Payment failed due to network error: ${message}. Please check your network connection and try again.`);
321
+ super(`Payment failed due to network error: ${message}`);
322
322
  this.originalError = originalError;
323
323
  this.name = 'PaymentNetworkError';
324
324
  }
325
325
  }
326
326
 
327
+ /**
328
+ * Creates an ATXP fetch wrapper that handles OAuth authentication and payments.
329
+ * This follows the wrapper pattern for fetch functions.
330
+ *
331
+ * @param config - The client configuration
332
+ * @returns A wrapped fetch function that handles ATXP protocol
333
+ */
327
334
  function atxpFetch(config) {
328
- const fetcher = new ATXPFetcher(config);
335
+ const fetcher = new ATXPFetcher({
336
+ accountId: config.account.accountId,
337
+ db: config.oAuthDb,
338
+ paymentMakers: config.account.paymentMakers,
339
+ fetchFn: config.fetchFn,
340
+ sideChannelFetch: config.oAuthChannelFetch,
341
+ allowInsecureRequests: config.allowHttp,
342
+ allowedAuthorizationServers: config.allowedAuthorizationServers,
343
+ approvePayment: config.approvePayment,
344
+ logger: config.logger,
345
+ onAuthorize: config.onAuthorize,
346
+ onAuthorizeFailure: config.onAuthorizeFailure,
347
+ onPayment: config.onPayment,
348
+ onPaymentFailure: config.onPaymentFailure
349
+ });
329
350
  return fetcher.fetch;
330
351
  }
331
352
  class ATXPFetcher {
332
- constructor({ accountId, db, paymentMakers, 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 () => { } }) {
353
+ constructor(config) {
333
354
  this.defaultPaymentFailureHandler = async ({ payment, error }) => {
334
355
  if (error instanceof InsufficientFundsError) {
335
356
  this.logger.info(`PAYMENT FAILED: Insufficient ${error.currency} funds on ${payment.network}`);
@@ -346,6 +367,70 @@ class ATXPFetcher {
346
367
  this.logger.info(`PAYMENT FAILED: ${error.message}`);
347
368
  }
348
369
  };
370
+ this.handleMultiDestinationPayment = async (paymentRequestData, paymentRequestUrl, paymentRequestId) => {
371
+ if (!paymentRequestData.destinations || paymentRequestData.destinations.length === 0) {
372
+ return false;
373
+ }
374
+ // Try each destination in order
375
+ for (const dest of paymentRequestData.destinations) {
376
+ const paymentMaker = this.paymentMakers.get(dest.network);
377
+ if (!paymentMaker) {
378
+ this.logger.debug(`ATXP: payment network '${dest.network}' not available, trying next destination`);
379
+ continue;
380
+ }
381
+ // Convert amount to BigNumber since it comes as a string from JSON
382
+ const amount = new BigNumber.BigNumber(dest.amount);
383
+ const prospectivePayment = {
384
+ accountId: this.accountId,
385
+ resourceUrl: paymentRequestData.resource?.toString() ?? '',
386
+ resourceName: paymentRequestData.resourceName ?? '',
387
+ network: dest.network,
388
+ currency: dest.currency,
389
+ amount: amount,
390
+ iss: paymentRequestData.iss ?? '',
391
+ };
392
+ if (!await this.approvePayment(prospectivePayment)) {
393
+ this.logger.info(`ATXP: payment request denied by callback function for destination on ${dest.network}`);
394
+ continue;
395
+ }
396
+ let paymentId;
397
+ try {
398
+ paymentId = await paymentMaker.makePayment(amount, dest.currency, dest.address, paymentRequestData.iss);
399
+ this.logger.info(`ATXP: made payment of ${amount.toString()} ${dest.currency} on ${dest.network}: ${paymentId}`);
400
+ await this.onPayment({ payment: prospectivePayment });
401
+ // Submit payment to the server
402
+ const jwt = await paymentMaker.generateJWT({ paymentRequestId, codeChallenge: '' });
403
+ const response = await this.sideChannelFetch(paymentRequestUrl.toString(), {
404
+ method: 'PUT',
405
+ headers: {
406
+ 'Authorization': `Bearer ${jwt}`,
407
+ 'Content-Type': 'application/json'
408
+ },
409
+ body: JSON.stringify({
410
+ transactionId: paymentId,
411
+ network: dest.network,
412
+ currency: dest.currency
413
+ })
414
+ });
415
+ this.logger.debug(`ATXP: payment was ${response.ok ? 'successfully' : 'not successfully'} PUT to ${paymentRequestUrl} : status ${response.status} ${response.statusText}`);
416
+ if (!response.ok) {
417
+ const msg = `ATXP: payment to ${paymentRequestUrl} failed: HTTP ${response.status} ${await response.text()}`;
418
+ this.logger.info(msg);
419
+ throw new Error(msg);
420
+ }
421
+ return true;
422
+ }
423
+ catch (error) {
424
+ const typedError = error;
425
+ this.logger.warn(`ATXP: payment failed on ${dest.network}: ${typedError.message}`);
426
+ await this.onPaymentFailure({ payment: prospectivePayment, error: typedError });
427
+ // Try next destination
428
+ continue;
429
+ }
430
+ }
431
+ this.logger.info(`ATXP: no suitable payment destination found among ${paymentRequestData.destinations.length} options`);
432
+ return false;
433
+ };
349
434
  this.handlePaymentRequestError = async (paymentRequestError) => {
350
435
  if (paymentRequestError.code !== common.PAYMENT_REQUIRED_ERROR_CODE) {
351
436
  throw new Error(`ATXP: expected payment required error (code ${common.PAYMENT_REQUIRED_ERROR_CODE}); got code ${paymentRequestError.code}`);
@@ -366,6 +451,11 @@ class ATXPFetcher {
366
451
  if (!paymentRequestData) {
367
452
  throw new Error(`ATXP: payment request ${paymentRequestId} not found on server ${paymentRequestUrl}`);
368
453
  }
454
+ // Handle multi-destination format
455
+ if (paymentRequestData.destinations && paymentRequestData.destinations.length > 0) {
456
+ return this.handleMultiDestinationPayment(paymentRequestData, paymentRequestUrl, paymentRequestId);
457
+ }
458
+ // Handle legacy single destination format
369
459
  const requestedNetwork = paymentRequestData.network;
370
460
  if (!requestedNetwork) {
371
461
  throw new Error(`Payment network not provided`);
@@ -442,7 +532,8 @@ class ATXPFetcher {
442
532
  },
443
533
  body: JSON.stringify({
444
534
  transactionId: paymentId,
445
- network: requestedNetwork
535
+ network: requestedNetwork,
536
+ currency: currency
446
537
  })
447
538
  });
448
539
  this.logger.debug(`ATXP: payment was ${response.ok ? 'successfully' : 'not successfully'} PUT to ${paymentRequestUrl} : status ${response.status} ${response.statusText}`);
@@ -661,6 +752,7 @@ class ATXPFetcher {
661
752
  throw error;
662
753
  }
663
754
  };
755
+ const { accountId, db, paymentMakers, 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;
664
756
  // Use React Native safe fetch if in React Native environment
665
757
  const safeFetchFn = common.getIsReactNative() ? common.createReactNativeSafeFetch(fetchFn) : fetchFn;
666
758
  const safeSideChannelFetch = common.getIsReactNative() ? common.createReactNativeSafeFetch(sideChannelFetch) : sideChannelFetch;
@@ -15819,22 +15911,9 @@ function buildClientConfig(args) {
15819
15911
  }
15820
15912
  function buildStreamableTransport(args) {
15821
15913
  const config = buildClientConfig(args);
15822
- const fetcher = new ATXPFetcher({
15823
- accountId: args.account.accountId,
15824
- db: config.oAuthDb,
15825
- paymentMakers: args.account.paymentMakers,
15826
- fetchFn: config.fetchFn,
15827
- sideChannelFetch: config.oAuthChannelFetch,
15828
- allowInsecureRequests: config.allowHttp,
15829
- allowedAuthorizationServers: config.allowedAuthorizationServers,
15830
- approvePayment: config.approvePayment,
15831
- logger: config.logger,
15832
- onAuthorize: config.onAuthorize,
15833
- onAuthorizeFailure: config.onAuthorizeFailure,
15834
- onPayment: config.onPayment,
15835
- onPaymentFailure: config.onPaymentFailure
15836
- });
15837
- const transport = new StreamableHTTPClientTransport(new URL(args.mcpServer), { fetch: fetcher.fetch });
15914
+ // Apply the ATXP wrapper to the fetch function
15915
+ const wrappedFetch = atxpFetch(config);
15916
+ const transport = new StreamableHTTPClientTransport(new URL(args.mcpServer), { fetch: wrappedFetch });
15838
15917
  return transport;
15839
15918
  }
15840
15919
  async function atxpClient(args) {
@@ -15851,7 +15930,7 @@ const ValidateTransferError = pay.ValidateTransferError;
15851
15930
  class SolanaPaymentMaker {
15852
15931
  constructor(solanaEndpoint, sourceSecretKey, logger) {
15853
15932
  this.generateJWT = async ({ paymentRequestId, codeChallenge }) => {
15854
- // Solana/Web3.js secretKey is 64 bytes:
15933
+ // Solana/Web3.js secretKey is 64 bytes:
15855
15934
  // first 32 bytes are the private scalar, last 32 are the public key.
15856
15935
  // JWK expects only the 32-byte private scalar for 'd'
15857
15936
  const jwk = {
@@ -15881,8 +15960,10 @@ class SolanaPaymentMaker {
15881
15960
  this.logger.warn(`Insufficient ${currency} balance for payment. Required: ${amount}, Available: ${balance}`);
15882
15961
  throw new InsufficientFundsError(currency, amount, balance, 'solana');
15883
15962
  }
15963
+ // Increase compute units to handle both memo and token transfer
15964
+ // Memo uses ~6000 CUs, token transfer needs ~6500 CUs
15884
15965
  const modifyComputeUnits = web3_js.ComputeBudgetProgram.setComputeUnitLimit({
15885
- units: 10000,
15966
+ units: 50000,
15886
15967
  });
15887
15968
  const addPriorityFee = web3_js.ComputeBudgetProgram.setComputeUnitPrice({
15888
15969
  microLamports: 20000,
@@ -16076,6 +16157,106 @@ class SolanaAccount {
16076
16157
  }
16077
16158
  }
16078
16159
 
16160
+ function toBasicAuth$1(token) {
16161
+ // Basic auth is base64("username:password"), password is blank
16162
+ const b64 = Buffer.from(`${token}:`).toString('base64');
16163
+ return `Basic ${b64}`;
16164
+ }
16165
+ /**
16166
+ * ATXP implementation of viem's LocalAccount interface.
16167
+ * Delegates signing operations to the accounts-x402 API.
16168
+ * Includes properties needed by x402 library for wallet client compatibility.
16169
+ */
16170
+ class ATXPLocalAccount {
16171
+ constructor(address, origin, token, fetchFn = fetch) {
16172
+ this.address = address;
16173
+ this.origin = origin;
16174
+ this.token = token;
16175
+ this.fetchFn = fetchFn;
16176
+ this.type = 'local';
16177
+ /**
16178
+ * Get public key - required by LocalAccount interface
16179
+ */
16180
+ this.publicKey = '0x0000000000000000000000000000000000000000000000000000000000000000';
16181
+ /**
16182
+ * Source - required by LocalAccount interface (set to 'custom')
16183
+ */
16184
+ this.source = 'custom';
16185
+ // x402 library expects these properties for wallet client compatibility
16186
+ this.account = this; // Self-reference for x402's isSignerWallet check
16187
+ this.chain = { id: 8453 }; // Base mainnet - could make this configurable
16188
+ this.transport = {}; // Empty transport object for x402 compatibility
16189
+ }
16190
+ /**
16191
+ * Fetch the wallet address from the /address endpoint
16192
+ */
16193
+ static async create(origin, token, fetchFn = fetch) {
16194
+ // The /address endpoint uses Basic auth like other authenticated endpoints
16195
+ // For X402, we need the Ethereum/Base address with USDC currency
16196
+ const url = new URL(`${origin}/address`);
16197
+ url.searchParams.set('network', 'base'); // X402 operates on Base
16198
+ url.searchParams.set('currency', 'USDC'); // Always USDC for X402
16199
+ const response = await fetchFn(url.toString(), {
16200
+ method: 'GET',
16201
+ headers: {
16202
+ 'Authorization': toBasicAuth$1(token)
16203
+ }
16204
+ });
16205
+ if (!response.ok) {
16206
+ const errorText = await response.text();
16207
+ throw new Error(`Failed to fetch destination address: ${response.status} ${response.statusText} ${errorText}`);
16208
+ }
16209
+ const data = await response.json();
16210
+ const address = data.address;
16211
+ if (!address) {
16212
+ throw new Error('Address endpoint did not return an address');
16213
+ }
16214
+ // Check that the account is an Ethereum/Base account (required for X402/EVM operations)
16215
+ const network = data.network;
16216
+ if (!network) {
16217
+ throw new Error('Address endpoint did not return a network');
16218
+ }
16219
+ if (network !== 'ethereum' && network !== 'base') {
16220
+ throw new Error(`ATXPLocalAccount requires an Ethereum/Base account, but got ${network} account`);
16221
+ }
16222
+ return new ATXPLocalAccount(address, origin, token, fetchFn);
16223
+ }
16224
+ /**
16225
+ * Sign a typed data structure using EIP-712
16226
+ * This is what x402 library will call for EIP-3009 authorization
16227
+ */
16228
+ async signTypedData(typedData) {
16229
+ const response = await this.fetchFn(`${this.origin}/sign-typed-data`, {
16230
+ method: 'POST',
16231
+ headers: {
16232
+ 'Authorization': toBasicAuth$1(this.token),
16233
+ 'Content-Type': 'application/json',
16234
+ },
16235
+ body: JSON.stringify({
16236
+ typedData
16237
+ })
16238
+ });
16239
+ if (!response.ok) {
16240
+ const errorText = await response.text();
16241
+ throw new Error(`Failed to sign typed data: ${response.status} ${response.statusText} ${errorText}`);
16242
+ }
16243
+ const result = await response.json();
16244
+ return result.signature;
16245
+ }
16246
+ /**
16247
+ * Sign a message - required by LocalAccount interface but not used for X402
16248
+ */
16249
+ async signMessage(_) {
16250
+ throw new Error('Message signing not implemented for ATXP local account');
16251
+ }
16252
+ /**
16253
+ * Sign a transaction - required by LocalAccount interface but not used for X402
16254
+ */
16255
+ async signTransaction(_transaction, _args) {
16256
+ throw new Error('Transaction signing not implemented for ATXP local account');
16257
+ }
16258
+ }
16259
+
16079
16260
  function toBasicAuth(token) {
16080
16261
  // Basic auth is base64("username:password"), password is blank
16081
16262
  const b64 = Buffer.from(`${token}:`).toString('base64');
@@ -16085,10 +16266,11 @@ function parseConnectionString(connectionString) {
16085
16266
  const url = new URL(connectionString);
16086
16267
  const origin = url.origin;
16087
16268
  const token = url.searchParams.get('connection_token') || '';
16269
+ const accountId = url.searchParams.get('account_id');
16088
16270
  if (!token) {
16089
16271
  throw new Error('ATXPAccount: connection string missing connection token');
16090
16272
  }
16091
- return { origin, token };
16273
+ return { origin, token, accountId };
16092
16274
  }
16093
16275
  class ATXPHttpPaymentMaker {
16094
16276
  constructor(origin, token, fetchFn = fetch) {
@@ -16097,19 +16279,19 @@ class ATXPHttpPaymentMaker {
16097
16279
  this.fetchFn = fetchFn;
16098
16280
  }
16099
16281
  async makePayment(amount, currency, receiver, memo) {
16100
- const body = {
16101
- amount: amount.toString(),
16102
- currency,
16103
- receiver,
16104
- memo,
16105
- };
16282
+ // Make a regular payment via the /pay endpoint
16106
16283
  const response = await this.fetchFn(`${this.origin}/pay`, {
16107
16284
  method: 'POST',
16108
16285
  headers: {
16109
16286
  'Authorization': toBasicAuth(this.token),
16110
16287
  'Content-Type': 'application/json',
16111
16288
  },
16112
- body: JSON.stringify(body),
16289
+ body: JSON.stringify({
16290
+ amount: amount.toString(),
16291
+ currency,
16292
+ receiver,
16293
+ memo,
16294
+ }),
16113
16295
  });
16114
16296
  if (!response.ok) {
16115
16297
  const text = await response.text();
@@ -16146,15 +16328,26 @@ class ATXPHttpPaymentMaker {
16146
16328
  }
16147
16329
  class ATXPAccount {
16148
16330
  constructor(connectionString, opts) {
16149
- const { origin, token } = parseConnectionString(connectionString);
16331
+ const { origin, token, accountId } = parseConnectionString(connectionString);
16150
16332
  const fetchFn = opts?.fetchFn ?? fetch;
16151
16333
  const network = opts?.network ?? 'base';
16152
- // Use token as a stable accountId namespace to keep OAuth/ATXP state per-connection
16153
- this.accountId = `atxp:${token}`;
16334
+ // Store for use in X402 payment creation
16335
+ this.origin = origin;
16336
+ this.token = token;
16337
+ this.fetchFn = fetchFn;
16338
+ if (accountId) {
16339
+ this.accountId = `atxp:${accountId}`;
16340
+ }
16341
+ else {
16342
+ this.accountId = `atxp:${common.crypto.randomUUID()}`;
16343
+ }
16154
16344
  this.paymentMakers = {
16155
16345
  [network]: new ATXPHttpPaymentMaker(origin, token, fetchFn),
16156
16346
  };
16157
16347
  }
16348
+ async getSigner() {
16349
+ return ATXPLocalAccount.create(this.origin, this.token, this.fetchFn);
16350
+ }
16158
16351
  }
16159
16352
 
16160
16353
  class BaseAccount {
@@ -16165,21 +16358,29 @@ class BaseAccount {
16165
16358
  if (!sourceSecretKey) {
16166
16359
  throw new Error('Source secret key is required');
16167
16360
  }
16168
- const account = accounts.privateKeyToAccount(sourceSecretKey);
16169
- this.accountId = account.address;
16170
- const walletClient = viem.createWalletClient({
16171
- account: account,
16361
+ this.account = accounts.privateKeyToAccount(sourceSecretKey);
16362
+ this.accountId = this.account.address;
16363
+ this.walletClient = viem.createWalletClient({
16364
+ account: this.account,
16172
16365
  chain: chains.base,
16173
16366
  transport: viem.http(baseRPCUrl),
16174
16367
  });
16175
16368
  this.paymentMakers = {
16176
- 'base': new BasePaymentMaker(baseRPCUrl, walletClient),
16369
+ 'base': new BasePaymentMaker(baseRPCUrl, this.walletClient),
16177
16370
  };
16178
16371
  }
16372
+ /**
16373
+ * Get a signer that can be used with the x402 library
16374
+ * This is only available for EVM-based accounts
16375
+ */
16376
+ getSigner() {
16377
+ // Return the viem account directly - it implements LocalAccount interface
16378
+ return this.account;
16379
+ }
16179
16380
  }
16180
16381
 
16181
16382
  exports.ATXPAccount = ATXPAccount;
16182
- exports.ATXPFetcher = ATXPFetcher;
16383
+ exports.ATXPLocalAccount = ATXPLocalAccount;
16183
16384
  exports.BaseAccount = BaseAccount;
16184
16385
  exports.BasePaymentMaker = BasePaymentMaker;
16185
16386
  exports.DEFAULT_CLIENT_CONFIG = DEFAULT_CLIENT_CONFIG;