@atxp/client 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/atxpAccount.d.ts +20 -0
- package/dist/atxpAccount.d.ts.map +1 -0
- package/dist/atxpAccount.js +82 -19
- package/dist/atxpAccount.js.map +1 -1
- package/dist/atxpClient.d.ts +12 -0
- package/dist/atxpClient.d.ts.map +1 -0
- package/dist/atxpClient.js +18 -2
- package/dist/atxpClient.js.map +1 -1
- package/dist/atxpFetcher.d.ts +77 -0
- package/dist/atxpFetcher.d.ts.map +1 -0
- package/dist/atxpFetcher.js +95 -140
- package/dist/atxpFetcher.js.map +1 -1
- package/dist/atxpLocalAccount.d.ts +50 -0
- package/dist/atxpLocalAccount.d.ts.map +1 -0
- package/dist/baseAccount.d.ts +20 -0
- package/dist/baseAccount.d.ts.map +1 -0
- package/dist/baseAccount.js +15 -4
- package/dist/baseAccount.js.map +1 -1
- package/dist/baseConstants.d.ts +10 -0
- package/dist/baseConstants.d.ts.map +1 -0
- package/dist/basePaymentMaker.d.ts +23 -0
- package/dist/basePaymentMaker.d.ts.map +1 -0
- package/dist/basePaymentMaker.js +23 -3
- package/dist/basePaymentMaker.js.map +1 -1
- package/dist/clientTestHelpers.d.ts +6 -0
- package/dist/clientTestHelpers.d.ts.map +1 -0
- package/dist/destinationMakers/atxpDestinationMaker.d.ts +15 -0
- package/dist/destinationMakers/atxpDestinationMaker.d.ts.map +1 -0
- package/dist/destinationMakers/atxpDestinationMaker.js +128 -0
- package/dist/destinationMakers/atxpDestinationMaker.js.map +1 -0
- package/dist/destinationMakers/index.d.ts +9 -0
- package/dist/destinationMakers/index.d.ts.map +1 -0
- package/dist/destinationMakers/index.js +42 -0
- package/dist/destinationMakers/index.js.map +1 -0
- package/dist/destinationMakers/passthroughDestinationMaker.d.ts +8 -0
- package/dist/destinationMakers/passthroughDestinationMaker.d.ts.map +1 -0
- package/dist/destinationMakers/passthroughDestinationMaker.js +27 -0
- package/dist/destinationMakers/passthroughDestinationMaker.js.map +1 -0
- package/dist/index.cjs +793 -454
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +111 -36
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +788 -456
- package/dist/index.js.map +1 -1
- package/dist/oAuth.d.ts +44 -0
- package/dist/oAuth.d.ts.map +1 -0
- package/dist/polygonConstants.d.ts +46 -0
- package/dist/polygonConstants.d.ts.map +1 -0
- package/dist/polygonConstants.js +54 -0
- package/dist/polygonConstants.js.map +1 -0
- package/dist/setup.expo.d.ts +2 -0
- package/dist/setup.expo.d.ts.map +1 -0
- package/dist/solanaAccount.d.ts +13 -0
- package/dist/solanaAccount.d.ts.map +1 -0
- package/dist/solanaAccount.js +16 -4
- package/dist/solanaAccount.js.map +1 -1
- package/dist/solanaPaymentMaker.d.ts +25 -0
- package/dist/solanaPaymentMaker.d.ts.map +1 -0
- package/dist/solanaPaymentMaker.js +27 -5
- package/dist/solanaPaymentMaker.js.map +1 -1
- package/dist/types.d.ts +63 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js.map +1 -1
- package/dist/worldConstants.d.ts +53 -0
- package/dist/worldConstants.d.ts.map +1 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { crypto as crypto$1, OAuthResourceClient, ConsoleLogger, PAYMENT_REQUIRED_ERROR_CODE, isSSEResponse, parseMcpMessages, parsePaymentRequests, paymentRequiredError, DEFAULT_AUTHORIZATION_SERVER, getIsReactNative, createReactNativeSafeFetch, MemoryOAuthDb, generateJWT } from '@atxp/common';
|
|
2
|
-
import BigNumber$1, { BigNumber } from 'bignumber.js';
|
|
1
|
+
import { crypto as crypto$1, OAuthResourceClient, ConsoleLogger, PAYMENT_REQUIRED_ERROR_CODE, isSSEResponse, parseMcpMessages, parsePaymentRequests, paymentRequiredError, DEFAULT_AUTHORIZATION_SERVER, getIsReactNative, createReactNativeSafeFetch, isEnumValue, ChainEnum, CurrencyEnum, NetworkEnum, assertNever, DEFAULT_ATXP_ACCOUNTS_SERVER, MemoryOAuthDb, generateJWT } from '@atxp/common';
|
|
3
2
|
import * as oauth from 'oauth4webapi';
|
|
3
|
+
import BigNumber$1, { BigNumber } from 'bignumber.js';
|
|
4
4
|
import { PublicKey, ComputeBudgetProgram, sendAndConfirmTransaction, Connection, Keypair } from '@solana/web3.js';
|
|
5
5
|
import { ValidateTransferError as ValidateTransferError$1, createTransfer } from '@solana/pay';
|
|
6
6
|
import { getAssociatedTokenAddress, getAccount } from '@solana/spl-token';
|
|
@@ -312,9 +312,9 @@ class PaymentNetworkError extends Error {
|
|
|
312
312
|
*/
|
|
313
313
|
function atxpFetch(config) {
|
|
314
314
|
const fetcher = new ATXPFetcher({
|
|
315
|
-
|
|
315
|
+
account: config.account,
|
|
316
316
|
db: config.oAuthDb,
|
|
317
|
-
|
|
317
|
+
destinationMakers: config.destinationMakers,
|
|
318
318
|
fetchFn: config.fetchFn,
|
|
319
319
|
sideChannelFetch: config.oAuthChannelFetch,
|
|
320
320
|
allowInsecureRequests: config.allowHttp,
|
|
@@ -332,7 +332,8 @@ class ATXPFetcher {
|
|
|
332
332
|
constructor(config) {
|
|
333
333
|
this.defaultPaymentFailureHandler = async ({ payment, error }) => {
|
|
334
334
|
if (error instanceof InsufficientFundsError) {
|
|
335
|
-
|
|
335
|
+
const networkText = error.network ? ` on ${error.network}` : '';
|
|
336
|
+
this.logger.info(`PAYMENT FAILED: Insufficient ${error.currency} funds${networkText}`);
|
|
336
337
|
this.logger.info(`Required: ${error.required} ${error.currency}`);
|
|
337
338
|
if (error.available) {
|
|
338
339
|
this.logger.info(`Available: ${error.available} ${error.currency}`);
|
|
@@ -340,7 +341,7 @@ class ATXPFetcher {
|
|
|
340
341
|
this.logger.info(`Account: ${payment.accountId}`);
|
|
341
342
|
}
|
|
342
343
|
else if (error instanceof PaymentNetworkError) {
|
|
343
|
-
this.logger.info(`PAYMENT FAILED: Network error
|
|
344
|
+
this.logger.info(`PAYMENT FAILED: Network error: ${error.message}`);
|
|
344
345
|
}
|
|
345
346
|
else {
|
|
346
347
|
this.logger.info(`PAYMENT FAILED: ${error.message}`);
|
|
@@ -350,35 +351,61 @@ class ATXPFetcher {
|
|
|
350
351
|
if (!paymentRequestData.destinations || paymentRequestData.destinations.length === 0) {
|
|
351
352
|
return false;
|
|
352
353
|
}
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
354
|
+
// Get sources from the account
|
|
355
|
+
const sources = await this.account.getSources();
|
|
356
|
+
// Apply destination mappers to transform destinations
|
|
357
|
+
// Convert PaymentRequestDestination[] to Destination[] for mapper compatibility
|
|
358
|
+
const mappedDestinations = [];
|
|
359
|
+
for (const option of paymentRequestData.destinations) {
|
|
360
|
+
const destinationMaker = this.destinationMakers.get(option.network);
|
|
361
|
+
if (!destinationMaker) {
|
|
362
|
+
this.logger.debug(`ATXP: destination maker for network '${option.network}' not available, trying next destination`);
|
|
358
363
|
continue;
|
|
359
364
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
};
|
|
371
|
-
if (!await this.approvePayment(prospectivePayment)) {
|
|
372
|
-
this.logger.info(`ATXP: payment request denied by callback function for destination on ${dest.network}`);
|
|
373
|
-
continue;
|
|
365
|
+
mappedDestinations.push(...(await destinationMaker.makeDestinations(option, this.logger, paymentRequestId, sources)));
|
|
366
|
+
}
|
|
367
|
+
if (mappedDestinations.length === 0) {
|
|
368
|
+
this.logger.info(`ATXP: no destinations found after mapping`);
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
// Validate amounts are not negative
|
|
372
|
+
for (const dest of mappedDestinations) {
|
|
373
|
+
if (dest.amount.isLessThan(0)) {
|
|
374
|
+
throw new Error(`ATXP: payment amount cannot be negative: ${dest.amount.toString()} ${dest.currency}`);
|
|
374
375
|
}
|
|
375
|
-
|
|
376
|
+
}
|
|
377
|
+
// Create prospective payment for approval (using first destination for display)
|
|
378
|
+
const firstDest = mappedDestinations[0];
|
|
379
|
+
const prospectivePayment = {
|
|
380
|
+
accountId: this.account.accountId,
|
|
381
|
+
resourceUrl: paymentRequestData.resource?.toString() ?? '',
|
|
382
|
+
resourceName: paymentRequestData.resourceName ?? '',
|
|
383
|
+
currency: firstDest.currency,
|
|
384
|
+
amount: firstDest.amount,
|
|
385
|
+
iss: paymentRequestData.iss ?? '',
|
|
386
|
+
};
|
|
387
|
+
// Ask for approval once for all payment attempts
|
|
388
|
+
if (!await this.approvePayment(prospectivePayment)) {
|
|
389
|
+
this.logger.info(`ATXP: payment request denied by callback function`);
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
// Try each payment maker in order
|
|
393
|
+
let lastPaymentError = null;
|
|
394
|
+
let paymentAttempted = false;
|
|
395
|
+
for (const paymentMaker of this.account.paymentMakers) {
|
|
376
396
|
try {
|
|
377
|
-
|
|
378
|
-
|
|
397
|
+
// Pass all destinations to payment maker - it will filter and pick the one it can handle
|
|
398
|
+
const result = await paymentMaker.makePayment(mappedDestinations, paymentRequestData.iss, paymentRequestId);
|
|
399
|
+
if (result === null) {
|
|
400
|
+
this.logger.debug(`ATXP: payment maker cannot handle these destinations, trying next`);
|
|
401
|
+
continue; // Try next payment maker
|
|
402
|
+
}
|
|
403
|
+
paymentAttempted = true;
|
|
404
|
+
// Payment was successful
|
|
405
|
+
this.logger.info(`ATXP: made payment of ${firstDest.amount.toString()} ${firstDest.currency} on ${result.chain}: ${result.transactionId}`);
|
|
379
406
|
await this.onPayment({ payment: prospectivePayment });
|
|
380
407
|
// Submit payment to the server
|
|
381
|
-
const jwt = await paymentMaker.generateJWT({ paymentRequestId, codeChallenge: '' });
|
|
408
|
+
const jwt = await paymentMaker.generateJWT({ paymentRequestId, codeChallenge: '', accountId: this.account.accountId });
|
|
382
409
|
const response = await this.sideChannelFetch(paymentRequestUrl.toString(), {
|
|
383
410
|
method: 'PUT',
|
|
384
411
|
headers: {
|
|
@@ -386,9 +413,10 @@ class ATXPFetcher {
|
|
|
386
413
|
'Content-Type': 'application/json'
|
|
387
414
|
},
|
|
388
415
|
body: JSON.stringify({
|
|
389
|
-
transactionId:
|
|
390
|
-
|
|
391
|
-
|
|
416
|
+
transactionId: result.transactionId,
|
|
417
|
+
...(result.transactionSubId ? { transactionSubId: result.transactionSubId } : {}),
|
|
418
|
+
chain: result.chain,
|
|
419
|
+
currency: result.currency
|
|
392
420
|
})
|
|
393
421
|
});
|
|
394
422
|
this.logger.debug(`ATXP: payment was ${response.ok ? 'successfully' : 'not successfully'} PUT to ${paymentRequestUrl} : status ${response.status} ${response.statusText}`);
|
|
@@ -401,13 +429,18 @@ class ATXPFetcher {
|
|
|
401
429
|
}
|
|
402
430
|
catch (error) {
|
|
403
431
|
const typedError = error;
|
|
404
|
-
|
|
432
|
+
paymentAttempted = true;
|
|
433
|
+
lastPaymentError = typedError;
|
|
434
|
+
this.logger.warn(`ATXP: payment maker failed: ${typedError.message}`);
|
|
405
435
|
await this.onPaymentFailure({ payment: prospectivePayment, error: typedError });
|
|
406
|
-
//
|
|
407
|
-
continue;
|
|
436
|
+
// Continue to next payment maker
|
|
408
437
|
}
|
|
409
438
|
}
|
|
410
|
-
|
|
439
|
+
// If payment was attempted but all failed, rethrow the last error
|
|
440
|
+
if (paymentAttempted && lastPaymentError) {
|
|
441
|
+
throw lastPaymentError;
|
|
442
|
+
}
|
|
443
|
+
this.logger.info(`ATXP: no payment maker could handle these destinations`);
|
|
411
444
|
return false;
|
|
412
445
|
};
|
|
413
446
|
this.handlePaymentRequestError = async (paymentRequestError) => {
|
|
@@ -434,94 +467,8 @@ class ATXPFetcher {
|
|
|
434
467
|
if (paymentRequestData.destinations && paymentRequestData.destinations.length > 0) {
|
|
435
468
|
return this.handleMultiDestinationPayment(paymentRequestData, paymentRequestUrl, paymentRequestId);
|
|
436
469
|
}
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
if (!requestedNetwork) {
|
|
440
|
-
throw new Error(`Payment network not provided`);
|
|
441
|
-
}
|
|
442
|
-
const destination = paymentRequestData.destination;
|
|
443
|
-
if (!destination) {
|
|
444
|
-
throw new Error(`destination not provided`);
|
|
445
|
-
}
|
|
446
|
-
let amount = new BigNumber(0);
|
|
447
|
-
if (!paymentRequestData.amount) {
|
|
448
|
-
throw new Error(`amount not provided`);
|
|
449
|
-
}
|
|
450
|
-
try {
|
|
451
|
-
amount = new BigNumber(paymentRequestData.amount);
|
|
452
|
-
}
|
|
453
|
-
catch {
|
|
454
|
-
throw new Error(`Invalid amount ${paymentRequestData.amount}`);
|
|
455
|
-
}
|
|
456
|
-
if (amount.lte(0)) {
|
|
457
|
-
throw new Error(`Invalid amount ${paymentRequestData.amount}`);
|
|
458
|
-
}
|
|
459
|
-
const currency = paymentRequestData.currency;
|
|
460
|
-
if (!currency) {
|
|
461
|
-
throw new Error(`Currency not provided`);
|
|
462
|
-
}
|
|
463
|
-
const paymentMaker = this.paymentMakers.get(requestedNetwork);
|
|
464
|
-
if (!paymentMaker) {
|
|
465
|
-
this.logger.info(`ATXP: payment network '${requestedNetwork}' not set up for this client (available networks: ${Array.from(this.paymentMakers.keys()).join(', ')})`);
|
|
466
|
-
return false;
|
|
467
|
-
}
|
|
468
|
-
const prospectivePayment = {
|
|
469
|
-
accountId: this.accountId,
|
|
470
|
-
resourceUrl: paymentRequestData.resource?.toString() ?? '',
|
|
471
|
-
resourceName: paymentRequestData.resourceName ?? '',
|
|
472
|
-
network: requestedNetwork,
|
|
473
|
-
currency,
|
|
474
|
-
amount,
|
|
475
|
-
iss: paymentRequestData.iss ?? '',
|
|
476
|
-
};
|
|
477
|
-
if (!await this.approvePayment(prospectivePayment)) {
|
|
478
|
-
this.logger.info(`ATXP: payment request denied by callback function`);
|
|
479
|
-
return false;
|
|
480
|
-
}
|
|
481
|
-
let paymentId;
|
|
482
|
-
try {
|
|
483
|
-
paymentId = await paymentMaker.makePayment(amount, currency, destination, paymentRequestData.iss);
|
|
484
|
-
this.logger.info(`ATXP: made payment of ${amount} ${currency} on ${requestedNetwork}: ${paymentId}`);
|
|
485
|
-
// Call onPayment callback after successful payment
|
|
486
|
-
await this.onPayment({ payment: prospectivePayment });
|
|
487
|
-
}
|
|
488
|
-
catch (paymentError) {
|
|
489
|
-
// Call onPaymentFailure callback if payment fails
|
|
490
|
-
await this.onPaymentFailure({
|
|
491
|
-
payment: prospectivePayment,
|
|
492
|
-
error: paymentError
|
|
493
|
-
});
|
|
494
|
-
throw paymentError;
|
|
495
|
-
}
|
|
496
|
-
const jwt = await paymentMaker.generateJWT({ paymentRequestId, codeChallenge: '' });
|
|
497
|
-
// Make a fetch call to the authorization URL with the payment ID
|
|
498
|
-
// redirect=false is a hack
|
|
499
|
-
// The OAuth spec calls for the authorization url to return with a redirect, but fetch
|
|
500
|
-
// on mobile will automatically follow the redirect (it doesn't support the redirect=manual option)
|
|
501
|
-
// We want the redirect URL so we can extract the code from it, not the contents of the
|
|
502
|
-
// redirect URL (which might not even exist for agentic ATXP clients)
|
|
503
|
-
// So ATXP servers are set up to instead return a 200 with the redirect URL in the body
|
|
504
|
-
// if we pass redirect=false.
|
|
505
|
-
// TODO: Remove the redirect=false hack once we have a way to handle the redirect on mobile
|
|
506
|
-
const response = await this.sideChannelFetch(paymentRequestUrl.toString(), {
|
|
507
|
-
method: 'PUT',
|
|
508
|
-
headers: {
|
|
509
|
-
'Authorization': `Bearer ${jwt}`,
|
|
510
|
-
'Content-Type': 'application/json'
|
|
511
|
-
},
|
|
512
|
-
body: JSON.stringify({
|
|
513
|
-
transactionId: paymentId,
|
|
514
|
-
network: requestedNetwork,
|
|
515
|
-
currency: currency
|
|
516
|
-
})
|
|
517
|
-
});
|
|
518
|
-
this.logger.debug(`ATXP: payment was ${response.ok ? 'successfully' : 'not successfully'} PUT to ${paymentRequestUrl} : status ${response.status} ${response.statusText}`);
|
|
519
|
-
if (!response.ok) {
|
|
520
|
-
const msg = `ATXP: payment to ${paymentRequestUrl} failed: HTTP ${response.status} ${await response.text()}`;
|
|
521
|
-
this.logger.info(msg);
|
|
522
|
-
throw new Error(msg);
|
|
523
|
-
}
|
|
524
|
-
return true;
|
|
470
|
+
// Payment request doesn't have destinations - this shouldn't happen with new SDK
|
|
471
|
+
throw new Error(`ATXP: payment request does not contain destinations array`);
|
|
525
472
|
};
|
|
526
473
|
this.getPaymentRequestData = async (paymentRequestUrl) => {
|
|
527
474
|
const prRequest = await this.sideChannelFetch(paymentRequestUrl);
|
|
@@ -529,6 +476,14 @@ class ATXPFetcher {
|
|
|
529
476
|
throw new Error(`ATXP: GET ${paymentRequestUrl} failed: ${prRequest.status} ${prRequest.statusText}`);
|
|
530
477
|
}
|
|
531
478
|
const paymentRequest = await prRequest.json();
|
|
479
|
+
// Parse amount strings to BigNumber objects
|
|
480
|
+
if (paymentRequest.destinations) {
|
|
481
|
+
for (const dest of paymentRequest.destinations) {
|
|
482
|
+
if (typeof dest.amount === 'string' || typeof dest.amount === 'number') {
|
|
483
|
+
dest.amount = new BigNumber(dest.amount);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
532
487
|
return paymentRequest;
|
|
533
488
|
};
|
|
534
489
|
this.isAllowedAuthServer = (url) => {
|
|
@@ -542,15 +497,15 @@ class ATXPFetcher {
|
|
|
542
497
|
throw new Error(`Code challenge not provided`);
|
|
543
498
|
}
|
|
544
499
|
if (!paymentMaker) {
|
|
545
|
-
const
|
|
546
|
-
throw new Error(`Payment maker is null/undefined. Available payment
|
|
500
|
+
const paymentMakerCount = this.account.paymentMakers.length;
|
|
501
|
+
throw new Error(`Payment maker is null/undefined. Available payment maker count: ${paymentMakerCount}. This usually indicates a payment maker object was not properly instantiated.`);
|
|
547
502
|
}
|
|
548
503
|
// TypeScript should prevent this, but add runtime check for edge cases (untyped JS, version mismatches, etc.)
|
|
549
504
|
if (!paymentMaker.generateJWT) {
|
|
550
|
-
const
|
|
551
|
-
throw new Error(`Payment maker is missing generateJWT method. Available payment
|
|
505
|
+
const paymentMakerCount = this.account.paymentMakers.length;
|
|
506
|
+
throw new Error(`Payment maker is missing generateJWT method. Available payment maker count: ${paymentMakerCount}. This indicates the payment maker object does not implement the PaymentMaker interface. If using TypeScript, ensure your payment maker properly implements the PaymentMaker interface.`);
|
|
552
507
|
}
|
|
553
|
-
const authToken = await paymentMaker.generateJWT({ paymentRequestId: '', codeChallenge: codeChallenge });
|
|
508
|
+
const authToken = await paymentMaker.generateJWT({ paymentRequestId: '', codeChallenge: codeChallenge, accountId: this.account.accountId });
|
|
554
509
|
// Make a fetch call to the authorization URL with the payment ID
|
|
555
510
|
// redirect=false is a hack
|
|
556
511
|
// The OAuth spec calls for the authorization url to return with a redirect, but fetch
|
|
@@ -596,11 +551,11 @@ class ATXPFetcher {
|
|
|
596
551
|
throw new Error(`Expected redirect response from authorization URL, got ${response.status}`);
|
|
597
552
|
};
|
|
598
553
|
this.authToService = async (error) => {
|
|
599
|
-
// TODO: We need to generalize this - we can't assume that there's a single paymentMaker for the auth flow.
|
|
600
|
-
if (this.paymentMakers.
|
|
554
|
+
// TODO: We need to generalize this - we can't assume that there's a single paymentMaker for the auth flow.
|
|
555
|
+
if (this.account.paymentMakers.length > 1) {
|
|
601
556
|
throw new Error(`ATXP: multiple payment makers found - cannot determine which one to use for auth`);
|
|
602
557
|
}
|
|
603
|
-
const paymentMaker =
|
|
558
|
+
const paymentMaker = this.account.paymentMakers[0];
|
|
604
559
|
if (paymentMaker) {
|
|
605
560
|
// We can do the full OAuth flow - we'll generate a signed JWT and call /authorize on the
|
|
606
561
|
// AS to get a code, then exchange the code for an access token
|
|
@@ -615,14 +570,14 @@ class ATXPFetcher {
|
|
|
615
570
|
// Call onAuthorize callback after successful authorization
|
|
616
571
|
await this.onAuthorize({
|
|
617
572
|
authorizationServer: authorizationUrl.origin,
|
|
618
|
-
userId: this.accountId
|
|
573
|
+
userId: this.account.accountId
|
|
619
574
|
});
|
|
620
575
|
}
|
|
621
576
|
catch (authError) {
|
|
622
577
|
// Call onAuthorizeFailure callback if authorization fails
|
|
623
578
|
await this.onAuthorizeFailure({
|
|
624
579
|
authorizationServer: authorizationUrl.origin,
|
|
625
|
-
userId: this.accountId,
|
|
580
|
+
userId: this.account.accountId,
|
|
626
581
|
error: authError
|
|
627
582
|
});
|
|
628
583
|
throw authError;
|
|
@@ -633,13 +588,13 @@ class ATXPFetcher {
|
|
|
633
588
|
// If we do, we'll use it to auth to the downstream resource
|
|
634
589
|
// (In pass-through scenarios, the atxpServer() middleware stores the incoming
|
|
635
590
|
// token in the DB under the '' resource URL).
|
|
636
|
-
const existingToken = await this.db.getAccessToken(this.accountId, '');
|
|
591
|
+
const existingToken = await this.db.getAccessToken(this.account.accountId, '');
|
|
637
592
|
if (!existingToken) {
|
|
638
593
|
this.logger.info(`ATXP: no token found for the current server - we can't exchange a token if we don't have one`);
|
|
639
594
|
throw error;
|
|
640
595
|
}
|
|
641
596
|
const newToken = await this.exchangeToken(existingToken, error.resourceServerUrl);
|
|
642
|
-
this.db.saveAccessToken(this.accountId, error.resourceServerUrl, newToken);
|
|
597
|
+
this.db.saveAccessToken(this.account.accountId, error.resourceServerUrl, newToken);
|
|
643
598
|
}
|
|
644
599
|
};
|
|
645
600
|
this.exchangeToken = async (myToken, newResourceUrl) => {
|
|
@@ -730,15 +685,15 @@ class ATXPFetcher {
|
|
|
730
685
|
throw error;
|
|
731
686
|
}
|
|
732
687
|
};
|
|
733
|
-
const {
|
|
688
|
+
const { account, db, destinationMakers, fetchFn = fetch, sideChannelFetch = fetchFn, strict = true, allowInsecureRequests = process.env.NODE_ENV === 'development', allowedAuthorizationServers = [DEFAULT_AUTHORIZATION_SERVER], approvePayment = async () => true, logger = new ConsoleLogger(), onAuthorize = async () => { }, onAuthorizeFailure = async () => { }, onPayment = async () => { }, onPaymentFailure = async () => { } } = config;
|
|
734
689
|
// Use React Native safe fetch if in React Native environment
|
|
735
690
|
const safeFetchFn = getIsReactNative() ? createReactNativeSafeFetch(fetchFn) : fetchFn;
|
|
736
691
|
const safeSideChannelFetch = getIsReactNative() ? createReactNativeSafeFetch(sideChannelFetch) : sideChannelFetch;
|
|
737
|
-
// ATXPClient should never actually use the callback url - instead of redirecting the user to
|
|
692
|
+
// ATXPClient should never actually use the callback url - instead of redirecting the user to
|
|
738
693
|
// an authorization url which redirects back to the callback url, ATXPClient posts the payment
|
|
739
694
|
// directly to the authorization server, then does the token exchange itself
|
|
740
695
|
this.oauthClient = new OAuthClient({
|
|
741
|
-
userId: accountId,
|
|
696
|
+
userId: account.accountId,
|
|
742
697
|
db,
|
|
743
698
|
callbackUrl: 'http://localhost:3000/unused-dummy-atxp-callback',
|
|
744
699
|
isPublic: false,
|
|
@@ -748,10 +703,10 @@ class ATXPFetcher {
|
|
|
748
703
|
allowInsecureRequests,
|
|
749
704
|
logger: logger
|
|
750
705
|
});
|
|
751
|
-
this.
|
|
706
|
+
this.account = account;
|
|
707
|
+
this.destinationMakers = destinationMakers;
|
|
752
708
|
this.sideChannelFetch = safeSideChannelFetch;
|
|
753
709
|
this.db = db;
|
|
754
|
-
this.accountId = accountId;
|
|
755
710
|
this.allowedAuthorizationServers = allowedAuthorizationServers;
|
|
756
711
|
this.approvePayment = approvePayment;
|
|
757
712
|
this.logger = logger;
|
|
@@ -15844,312 +15799,186 @@ class StreamableHTTPClientTransport {
|
|
|
15844
15799
|
}
|
|
15845
15800
|
}
|
|
15846
15801
|
|
|
15847
|
-
|
|
15848
|
-
|
|
15849
|
-
|
|
15850
|
-
|
|
15851
|
-
|
|
15852
|
-
|
|
15853
|
-
|
|
15854
|
-
|
|
15855
|
-
|
|
15856
|
-
|
|
15857
|
-
|
|
15858
|
-
approvePayment: async (_p) => true,
|
|
15859
|
-
fetchFn: getFetch(),
|
|
15860
|
-
oAuthChannelFetch: getFetch(),
|
|
15861
|
-
allowHttp: false, // may be overridden in buildClientConfig by process.env.NODE_ENV
|
|
15862
|
-
clientInfo: {
|
|
15863
|
-
name: 'ATXPClient',
|
|
15864
|
-
version: '0.0.1'
|
|
15865
|
-
},
|
|
15866
|
-
clientOptions: {
|
|
15867
|
-
capabilities: {}
|
|
15868
|
-
},
|
|
15869
|
-
onAuthorize: async () => { },
|
|
15870
|
-
onAuthorizeFailure: async () => { },
|
|
15871
|
-
onPayment: async () => { },
|
|
15872
|
-
onPaymentFailure: async () => { }
|
|
15873
|
-
};
|
|
15874
|
-
function buildClientConfig(args) {
|
|
15875
|
-
// Use fetchFn for oAuthChannelFetch if the latter isn't explicitly set
|
|
15876
|
-
if (args.fetchFn && !args.oAuthChannelFetch) {
|
|
15877
|
-
args.oAuthChannelFetch = args.fetchFn;
|
|
15878
|
-
}
|
|
15879
|
-
// Read environment variable at runtime, not module load time
|
|
15880
|
-
const envDefaults = {
|
|
15881
|
-
...DEFAULT_CLIENT_CONFIG,
|
|
15882
|
-
allowHttp: process.env.NODE_ENV === 'development',
|
|
15883
|
-
};
|
|
15884
|
-
const withDefaults = { ...envDefaults, ...args };
|
|
15885
|
-
const logger = withDefaults.logger ?? new ConsoleLogger();
|
|
15886
|
-
const oAuthDb = withDefaults.oAuthDb ?? new MemoryOAuthDb({ logger });
|
|
15887
|
-
const built = { oAuthDb, logger };
|
|
15888
|
-
return Object.freeze({ ...withDefaults, ...built });
|
|
15802
|
+
function isDestinationResponse(obj) {
|
|
15803
|
+
return (typeof obj === 'object' &&
|
|
15804
|
+
obj !== null &&
|
|
15805
|
+
'chain' in obj &&
|
|
15806
|
+
'address' in obj &&
|
|
15807
|
+
'currency' in obj &&
|
|
15808
|
+
'amount' in obj &&
|
|
15809
|
+
typeof obj.chain === 'string' &&
|
|
15810
|
+
typeof obj.address === 'string' &&
|
|
15811
|
+
typeof obj.currency === 'string' &&
|
|
15812
|
+
typeof obj.amount === 'string');
|
|
15889
15813
|
}
|
|
15890
|
-
function
|
|
15891
|
-
|
|
15892
|
-
|
|
15893
|
-
|
|
15894
|
-
|
|
15895
|
-
|
|
15896
|
-
|
|
15897
|
-
async function atxpClient(args) {
|
|
15898
|
-
const config = buildClientConfig(args);
|
|
15899
|
-
const transport = buildStreamableTransport(config);
|
|
15900
|
-
const client = new Client(config.clientInfo, config.clientOptions);
|
|
15901
|
-
await client.connect(transport);
|
|
15902
|
-
return client;
|
|
15814
|
+
function isDestinationsApiResponse(obj) {
|
|
15815
|
+
return (typeof obj === 'object' &&
|
|
15816
|
+
obj !== null &&
|
|
15817
|
+
'destinations' in obj &&
|
|
15818
|
+
'paymentRequestId' in obj &&
|
|
15819
|
+
Array.isArray(obj.destinations) &&
|
|
15820
|
+
typeof obj.paymentRequestId === 'string');
|
|
15903
15821
|
}
|
|
15904
|
-
|
|
15905
|
-
//
|
|
15906
|
-
|
|
15907
|
-
|
|
15908
|
-
|
|
15909
|
-
|
|
15910
|
-
|
|
15911
|
-
|
|
15912
|
-
|
|
15913
|
-
|
|
15914
|
-
|
|
15915
|
-
|
|
15916
|
-
|
|
15917
|
-
|
|
15918
|
-
|
|
15919
|
-
|
|
15920
|
-
|
|
15921
|
-
|
|
15922
|
-
|
|
15923
|
-
|
|
15924
|
-
|
|
15822
|
+
function parseDestinationsResponse(data) {
|
|
15823
|
+
// Validate response structure
|
|
15824
|
+
if (!isDestinationsApiResponse(data)) {
|
|
15825
|
+
throw new Error('Invalid response: expected object with destinations array and paymentRequestId');
|
|
15826
|
+
}
|
|
15827
|
+
// Validate and convert each destination
|
|
15828
|
+
return data.destinations.map((dest, index) => {
|
|
15829
|
+
if (!isDestinationResponse(dest)) {
|
|
15830
|
+
throw new Error(`Invalid destination at index ${index}: missing required fields (chain, address, currency, amount)`);
|
|
15831
|
+
}
|
|
15832
|
+
// Validate chain is a valid Chain enum value
|
|
15833
|
+
if (!isEnumValue(ChainEnum, dest.chain)) {
|
|
15834
|
+
const validChains = Object.values(ChainEnum).join(', ');
|
|
15835
|
+
throw new Error(`Invalid destination at index ${index}: chain "${dest.chain}" is not a valid chain. Valid chains are: ${validChains}`);
|
|
15836
|
+
}
|
|
15837
|
+
// Validate currency is a valid Currency enum value
|
|
15838
|
+
if (!isEnumValue(CurrencyEnum, dest.currency)) {
|
|
15839
|
+
const validCurrencies = Object.values(CurrencyEnum).join(', ');
|
|
15840
|
+
throw new Error(`Invalid destination at index ${index}: currency "${dest.currency}" is not a valid currency. Valid currencies are: ${validCurrencies}`);
|
|
15841
|
+
}
|
|
15842
|
+
// Validate amount is a valid number
|
|
15843
|
+
const amount = new BigNumber(dest.amount);
|
|
15844
|
+
if (amount.isNaN()) {
|
|
15845
|
+
throw new Error(`Invalid destination at index ${index}: amount "${dest.amount}" is not a valid number`);
|
|
15846
|
+
}
|
|
15847
|
+
return {
|
|
15848
|
+
chain: dest.chain,
|
|
15849
|
+
currency: dest.currency,
|
|
15850
|
+
address: dest.address,
|
|
15851
|
+
amount: amount
|
|
15925
15852
|
};
|
|
15926
|
-
|
|
15927
|
-
|
|
15928
|
-
|
|
15853
|
+
});
|
|
15854
|
+
}
|
|
15855
|
+
/**
|
|
15856
|
+
* Destination mapper for ATXP network destinations.
|
|
15857
|
+
* Converts destinations with network='atxp' to actual blockchain network destinations
|
|
15858
|
+
* by resolving the account ID to its associated blockchain addresses.
|
|
15859
|
+
*/
|
|
15860
|
+
class ATXPDestinationMaker {
|
|
15861
|
+
constructor(accountsServiceUrl, fetchFn = fetch) {
|
|
15862
|
+
this.accountsServiceUrl = accountsServiceUrl;
|
|
15863
|
+
this.fetchFn = fetchFn;
|
|
15864
|
+
}
|
|
15865
|
+
async makeDestinations(option, logger, paymentRequestId, sources) {
|
|
15866
|
+
if (option.network !== 'atxp') {
|
|
15867
|
+
return [];
|
|
15868
|
+
}
|
|
15869
|
+
try {
|
|
15870
|
+
// The address field contains the account ID (e.g., atxp_acct_xxx) for atxp options
|
|
15871
|
+
const accountId = option.address;
|
|
15872
|
+
// Always use the destinations endpoint
|
|
15873
|
+
const destinations = await this.getDestinations(accountId, paymentRequestId, option, sources, logger);
|
|
15874
|
+
if (destinations.length === 0) {
|
|
15875
|
+
logger.warn(`ATXPDestinationMaker: No destinations found for account ${accountId}`);
|
|
15929
15876
|
}
|
|
15930
|
-
|
|
15931
|
-
|
|
15932
|
-
try {
|
|
15933
|
-
// Check balance before attempting payment
|
|
15934
|
-
const tokenAccountAddress = await getAssociatedTokenAddress(USDC_MINT, this.source.publicKey);
|
|
15935
|
-
const tokenAccount = await getAccount(this.connection, tokenAccountAddress);
|
|
15936
|
-
const balance = new BigNumber$1(tokenAccount.amount.toString()).dividedBy(10 ** 6); // USDC has 6 decimals
|
|
15937
|
-
if (balance.lt(amount)) {
|
|
15938
|
-
this.logger.warn(`Insufficient ${currency} balance for payment. Required: ${amount}, Available: ${balance}`);
|
|
15939
|
-
throw new InsufficientFundsError(currency, amount, balance, 'solana');
|
|
15940
|
-
}
|
|
15941
|
-
// Increase compute units to handle both memo and token transfer
|
|
15942
|
-
// Memo uses ~6000 CUs, token transfer needs ~6500 CUs
|
|
15943
|
-
const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({
|
|
15944
|
-
units: 50000,
|
|
15945
|
-
});
|
|
15946
|
-
const addPriorityFee = ComputeBudgetProgram.setComputeUnitPrice({
|
|
15947
|
-
microLamports: 20000,
|
|
15948
|
-
});
|
|
15949
|
-
const transaction = await createTransfer(this.connection, this.source.publicKey, {
|
|
15950
|
-
amount: amount,
|
|
15951
|
-
recipient: receiverKey,
|
|
15952
|
-
splToken: USDC_MINT,
|
|
15953
|
-
memo,
|
|
15954
|
-
});
|
|
15955
|
-
transaction.add(modifyComputeUnits);
|
|
15956
|
-
transaction.add(addPriorityFee);
|
|
15957
|
-
const transactionHash = await sendAndConfirmTransaction(this.connection, transaction, [this.source]);
|
|
15958
|
-
return transactionHash;
|
|
15877
|
+
else {
|
|
15878
|
+
logger.debug(`ATXPDestinationMaker: Got ${destinations.length} destinations for account ${accountId}`);
|
|
15959
15879
|
}
|
|
15960
|
-
|
|
15961
|
-
|
|
15962
|
-
|
|
15963
|
-
|
|
15964
|
-
|
|
15965
|
-
|
|
15880
|
+
return destinations;
|
|
15881
|
+
}
|
|
15882
|
+
catch (error) {
|
|
15883
|
+
logger.error(`ATXPDestinationMaker: Failed to make ATXP destinations: ${error}`);
|
|
15884
|
+
throw error;
|
|
15885
|
+
}
|
|
15886
|
+
}
|
|
15887
|
+
async getDestinations(accountId, paymentRequestId, option, sources, logger) {
|
|
15888
|
+
// Strip any network prefix if present
|
|
15889
|
+
const unqualifiedId = accountId.includes(':') ? accountId.split(':')[1] : accountId;
|
|
15890
|
+
const url = `${this.accountsServiceUrl}/account/${unqualifiedId}/destinations`;
|
|
15891
|
+
logger?.debug(`ATXPDestinationMaker: Fetching destinations from ${url}`);
|
|
15892
|
+
try {
|
|
15893
|
+
const requestBody = {
|
|
15894
|
+
paymentRequestId,
|
|
15895
|
+
options: [{
|
|
15896
|
+
network: option.network,
|
|
15897
|
+
currency: option.currency.toString(),
|
|
15898
|
+
address: option.address,
|
|
15899
|
+
amount: option.amount.toString()
|
|
15900
|
+
}],
|
|
15901
|
+
sources: sources
|
|
15902
|
+
};
|
|
15903
|
+
const response = await this.fetchFn(url, {
|
|
15904
|
+
method: 'POST',
|
|
15905
|
+
headers: {
|
|
15906
|
+
'Accept': 'application/json',
|
|
15907
|
+
'Content-Type': 'application/json',
|
|
15908
|
+
},
|
|
15909
|
+
body: JSON.stringify(requestBody),
|
|
15910
|
+
});
|
|
15911
|
+
if (!response.ok) {
|
|
15912
|
+
const text = await response.text();
|
|
15913
|
+
throw new Error(`Failed to fetch destinations: ${response.status} ${response.statusText} - ${text}`);
|
|
15966
15914
|
}
|
|
15967
|
-
|
|
15968
|
-
|
|
15969
|
-
throw new Error('Solana endpoint is required');
|
|
15915
|
+
const data = await response.json();
|
|
15916
|
+
return parseDestinationsResponse(data);
|
|
15970
15917
|
}
|
|
15971
|
-
|
|
15972
|
-
|
|
15918
|
+
catch (error) {
|
|
15919
|
+
logger?.error(`ATXPDestinationMaker: Error fetching destinations: ${error}`);
|
|
15920
|
+
throw error;
|
|
15973
15921
|
}
|
|
15974
|
-
this.connection = new Connection(solanaEndpoint, { commitment: 'confirmed' });
|
|
15975
|
-
this.source = Keypair.fromSecretKey(bs58.decode(sourceSecretKey));
|
|
15976
|
-
this.logger = logger ?? new ConsoleLogger();
|
|
15977
15922
|
}
|
|
15978
15923
|
}
|
|
15979
15924
|
|
|
15980
|
-
|
|
15981
|
-
|
|
15982
|
-
|
|
15983
|
-
* Get USDC contract address for Base chain by chain ID
|
|
15984
|
-
* @param chainId - Chain ID (8453 for mainnet, 84532 for sepolia)
|
|
15985
|
-
* @returns USDC contract address
|
|
15986
|
-
* @throws Error if chain ID is not supported
|
|
15987
|
-
*/
|
|
15988
|
-
const getBaseUSDCAddress = (chainId) => {
|
|
15989
|
-
switch (chainId) {
|
|
15990
|
-
case 8453: // Base mainnet
|
|
15991
|
-
return USDC_CONTRACT_ADDRESS_BASE;
|
|
15992
|
-
case 84532: // Base Sepolia
|
|
15993
|
-
return USDC_CONTRACT_ADDRESS_BASE_SEPOLIA;
|
|
15994
|
-
default:
|
|
15995
|
-
throw new Error(`Unsupported Base Chain ID: ${chainId}. Supported chains: 8453 (mainnet), 84532 (sepolia)`);
|
|
15925
|
+
class PassthroughDestinationMaker {
|
|
15926
|
+
constructor(network) {
|
|
15927
|
+
this.network = network;
|
|
15996
15928
|
}
|
|
15997
|
-
|
|
15929
|
+
async makeDestinations(option, _logger, _paymentRequestId, _sources) {
|
|
15930
|
+
if (option.network !== this.network) {
|
|
15931
|
+
return [];
|
|
15932
|
+
}
|
|
15933
|
+
// Check if option.network is also a Chain by inspecting the ChainEnum values
|
|
15934
|
+
if (Object.values(ChainEnum).includes(option.network)) {
|
|
15935
|
+
// It's a chain, so return a single passthrough destination
|
|
15936
|
+
const destination = {
|
|
15937
|
+
chain: option.network,
|
|
15938
|
+
currency: option.currency,
|
|
15939
|
+
address: option.address,
|
|
15940
|
+
amount: option.amount
|
|
15941
|
+
};
|
|
15942
|
+
return [destination];
|
|
15943
|
+
}
|
|
15944
|
+
return [];
|
|
15945
|
+
}
|
|
15946
|
+
}
|
|
15998
15947
|
|
|
15999
|
-
|
|
16000
|
-
|
|
16001
|
-
//
|
|
16002
|
-
const
|
|
16003
|
-
|
|
16004
|
-
|
|
16005
|
-
|
|
16006
|
-
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
16013
|
-
|
|
16014
|
-
|
|
16015
|
-
|
|
16016
|
-
|
|
16017
|
-
|
|
16018
|
-
|
|
16019
|
-
|
|
16020
|
-
|
|
16021
|
-
|
|
16022
|
-
|
|
16023
|
-
|
|
16024
|
-
|
|
16025
|
-
|
|
16026
|
-
|
|
16027
|
-
|
|
16028
|
-
|
|
16029
|
-
|
|
16030
|
-
|
|
16031
|
-
|
|
16032
|
-
|
|
16033
|
-
}
|
|
16034
|
-
],
|
|
16035
|
-
"payable": false,
|
|
16036
|
-
"stateMutability": "view",
|
|
16037
|
-
"type": "function"
|
|
16038
|
-
}
|
|
16039
|
-
];
|
|
16040
|
-
class BasePaymentMaker {
|
|
16041
|
-
constructor(baseRPCUrl, walletClient, logger) {
|
|
16042
|
-
if (!baseRPCUrl) {
|
|
16043
|
-
throw new Error('baseRPCUrl was empty');
|
|
16044
|
-
}
|
|
16045
|
-
if (!walletClient) {
|
|
16046
|
-
throw new Error('walletClient was empty');
|
|
16047
|
-
}
|
|
16048
|
-
if (!walletClient.account) {
|
|
16049
|
-
throw new Error('walletClient.account was empty');
|
|
16050
|
-
}
|
|
16051
|
-
this.signingClient = walletClient.extend(publicActions);
|
|
16052
|
-
this.logger = logger ?? new ConsoleLogger();
|
|
16053
|
-
}
|
|
16054
|
-
async generateJWT({ paymentRequestId, codeChallenge }) {
|
|
16055
|
-
const headerObj = { alg: 'ES256K' };
|
|
16056
|
-
const payloadObj = {
|
|
16057
|
-
sub: this.signingClient.account.address,
|
|
16058
|
-
iss: 'accounts.atxp.ai',
|
|
16059
|
-
aud: 'https://auth.atxp.ai',
|
|
16060
|
-
iat: Math.floor(Date.now() / 1000),
|
|
16061
|
-
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
|
16062
|
-
...(codeChallenge ? { code_challenge: codeChallenge } : {}),
|
|
16063
|
-
...(paymentRequestId ? { payment_request_id: paymentRequestId } : {}),
|
|
16064
|
-
};
|
|
16065
|
-
const header = toBase64Url(JSON.stringify(headerObj));
|
|
16066
|
-
const payload = toBase64Url(JSON.stringify(payloadObj));
|
|
16067
|
-
const message = `${header}.${payload}`;
|
|
16068
|
-
const messageBytes = typeof Buffer !== 'undefined'
|
|
16069
|
-
? Buffer.from(message, 'utf8')
|
|
16070
|
-
: new TextEncoder().encode(message);
|
|
16071
|
-
const signResult = await this.signingClient.signMessage({
|
|
16072
|
-
account: this.signingClient.account,
|
|
16073
|
-
message: { raw: messageBytes },
|
|
16074
|
-
});
|
|
16075
|
-
// For ES256K, signature is typically 65 bytes (r,s,v)
|
|
16076
|
-
// Server expects the hex signature string (with 0x prefix) to be base64url encoded
|
|
16077
|
-
// This creates: base64url("0x6eb2565...") not base64url(rawBytes)
|
|
16078
|
-
// Pass the hex string directly to toBase64Url which will UTF-8 encode and base64url it
|
|
16079
|
-
const signature = toBase64Url(signResult);
|
|
16080
|
-
const jwt = `${header}.${payload}.${signature}`;
|
|
16081
|
-
this.logger.info(`Generated ES256K JWT: ${jwt}`);
|
|
16082
|
-
return jwt;
|
|
16083
|
-
}
|
|
16084
|
-
async makePayment(amount, currency, receiver) {
|
|
16085
|
-
if (currency.toUpperCase() !== 'USDC') {
|
|
16086
|
-
throw new PaymentNetworkError('Only USDC currency is supported; received ' + currency);
|
|
16087
|
-
}
|
|
16088
|
-
this.logger.info(`Making payment of ${amount} ${currency} to ${receiver} on Base from ${this.signingClient.account.address}`);
|
|
16089
|
-
try {
|
|
16090
|
-
// Check balance before attempting payment
|
|
16091
|
-
const balanceRaw = await this.signingClient.readContract({
|
|
16092
|
-
address: USDC_CONTRACT_ADDRESS_BASE,
|
|
16093
|
-
abi: ERC20_ABI,
|
|
16094
|
-
functionName: 'balanceOf',
|
|
16095
|
-
args: [this.signingClient.account.address],
|
|
16096
|
-
});
|
|
16097
|
-
const balance = new BigNumber(balanceRaw.toString()).dividedBy(10 ** USDC_DECIMALS);
|
|
16098
|
-
if (balance.lt(amount)) {
|
|
16099
|
-
this.logger.warn(`Insufficient ${currency} balance for payment. Required: ${amount}, Available: ${balance}`);
|
|
16100
|
-
throw new InsufficientFundsError(currency, amount, balance, 'base');
|
|
16101
|
-
}
|
|
16102
|
-
// Convert amount to USDC units (6 decimals) as BigInt
|
|
16103
|
-
const amountInUSDCUnits = BigInt(amount.multipliedBy(10 ** USDC_DECIMALS).toFixed(0));
|
|
16104
|
-
const data = encodeFunctionData({
|
|
16105
|
-
abi: ERC20_ABI,
|
|
16106
|
-
functionName: "transfer",
|
|
16107
|
-
args: [receiver, amountInUSDCUnits],
|
|
16108
|
-
});
|
|
16109
|
-
const hash = await this.signingClient.sendTransaction({
|
|
16110
|
-
chain: base,
|
|
16111
|
-
account: this.signingClient.account,
|
|
16112
|
-
to: USDC_CONTRACT_ADDRESS_BASE,
|
|
16113
|
-
data: data,
|
|
16114
|
-
value: parseEther('0'),
|
|
16115
|
-
maxPriorityFeePerGas: parseEther('0.000000001')
|
|
16116
|
-
});
|
|
16117
|
-
// Wait for transaction confirmation with more blocks to ensure propagation
|
|
16118
|
-
this.logger.info(`Waiting for transaction confirmation: ${hash}`);
|
|
16119
|
-
const receipt = await this.signingClient.waitForTransactionReceipt({
|
|
16120
|
-
hash: hash,
|
|
16121
|
-
confirmations: 1
|
|
16122
|
-
});
|
|
16123
|
-
if (receipt.status === 'reverted') {
|
|
16124
|
-
throw new PaymentNetworkError(`Transaction reverted: ${hash}`, new Error('Transaction reverted on chain'));
|
|
16125
|
-
}
|
|
16126
|
-
this.logger.info(`Transaction confirmed: ${hash} in block ${receipt.blockNumber}`);
|
|
16127
|
-
return hash;
|
|
16128
|
-
}
|
|
16129
|
-
catch (error) {
|
|
16130
|
-
if (error instanceof InsufficientFundsError || error instanceof PaymentNetworkError) {
|
|
16131
|
-
throw error;
|
|
16132
|
-
}
|
|
16133
|
-
// Wrap other errors in PaymentNetworkError
|
|
16134
|
-
throw new PaymentNetworkError(`Payment failed on Base network: ${error.message}`, error);
|
|
16135
|
-
}
|
|
16136
|
-
}
|
|
16137
|
-
}
|
|
16138
|
-
|
|
16139
|
-
class SolanaAccount {
|
|
16140
|
-
constructor(solanaEndpoint, sourceSecretKey) {
|
|
16141
|
-
if (!solanaEndpoint) {
|
|
16142
|
-
throw new Error('Solana endpoint is required');
|
|
16143
|
-
}
|
|
16144
|
-
if (!sourceSecretKey) {
|
|
16145
|
-
throw new Error('Source secret key is required');
|
|
16146
|
-
}
|
|
16147
|
-
const source = Keypair.fromSecretKey(bs58.decode(sourceSecretKey));
|
|
16148
|
-
this.accountId = source.publicKey.toBase58();
|
|
16149
|
-
this.paymentMakers = {
|
|
16150
|
-
'solana': new SolanaPaymentMaker(solanaEndpoint, sourceSecretKey),
|
|
16151
|
-
};
|
|
16152
|
-
}
|
|
15948
|
+
function createDestinationMakers(config) {
|
|
15949
|
+
const { atxpAccountsServer, fetchFn = fetch } = config;
|
|
15950
|
+
// Build the map by exhaustively checking all Network values
|
|
15951
|
+
const makers = new Map();
|
|
15952
|
+
for (const network of Object.values(NetworkEnum)) {
|
|
15953
|
+
// Exhaustiveness check using switch with assertNever
|
|
15954
|
+
switch (network) {
|
|
15955
|
+
case NetworkEnum.Solana:
|
|
15956
|
+
makers.set(network, new PassthroughDestinationMaker(network));
|
|
15957
|
+
break;
|
|
15958
|
+
case NetworkEnum.Base:
|
|
15959
|
+
makers.set(network, new PassthroughDestinationMaker(network));
|
|
15960
|
+
break;
|
|
15961
|
+
case NetworkEnum.World:
|
|
15962
|
+
makers.set(network, new PassthroughDestinationMaker(network));
|
|
15963
|
+
break;
|
|
15964
|
+
case NetworkEnum.Polygon:
|
|
15965
|
+
makers.set(network, new PassthroughDestinationMaker(network));
|
|
15966
|
+
break;
|
|
15967
|
+
case NetworkEnum.BaseSepolia:
|
|
15968
|
+
makers.set(network, new PassthroughDestinationMaker(network));
|
|
15969
|
+
break;
|
|
15970
|
+
case NetworkEnum.WorldSepolia:
|
|
15971
|
+
makers.set(network, new PassthroughDestinationMaker(network));
|
|
15972
|
+
break;
|
|
15973
|
+
case NetworkEnum.ATXP:
|
|
15974
|
+
makers.set(network, new ATXPDestinationMaker(atxpAccountsServer, fetchFn));
|
|
15975
|
+
break;
|
|
15976
|
+
default:
|
|
15977
|
+
// This will cause a compilation error if a new Network is added but not handled above
|
|
15978
|
+
assertNever(network);
|
|
15979
|
+
}
|
|
15980
|
+
}
|
|
15981
|
+
return makers;
|
|
16153
15982
|
}
|
|
16154
15983
|
|
|
16155
15984
|
function toBasicAuth$1(token) {
|
|
@@ -16265,6 +16094,9 @@ function parseConnectionString(connectionString) {
|
|
|
16265
16094
|
if (!token) {
|
|
16266
16095
|
throw new Error('ATXPAccount: connection string missing connection token');
|
|
16267
16096
|
}
|
|
16097
|
+
if (!accountId) {
|
|
16098
|
+
throw new Error('ATXPAccount: connection string missing account id');
|
|
16099
|
+
}
|
|
16268
16100
|
return { origin, token, accountId };
|
|
16269
16101
|
}
|
|
16270
16102
|
class ATXPHttpPaymentMaker {
|
|
@@ -16273,8 +16105,33 @@ class ATXPHttpPaymentMaker {
|
|
|
16273
16105
|
this.token = token;
|
|
16274
16106
|
this.fetchFn = fetchFn;
|
|
16275
16107
|
}
|
|
16276
|
-
async
|
|
16277
|
-
//
|
|
16108
|
+
async getSourceAddress(params) {
|
|
16109
|
+
// Call the /address_for_payment endpoint to get the source address for this account
|
|
16110
|
+
const response = await this.fetchFn(`${this.origin}/address_for_payment`, {
|
|
16111
|
+
method: 'POST',
|
|
16112
|
+
headers: {
|
|
16113
|
+
'Authorization': toBasicAuth(this.token),
|
|
16114
|
+
'Content-Type': 'application/json',
|
|
16115
|
+
},
|
|
16116
|
+
body: JSON.stringify({
|
|
16117
|
+
amount: params.amount.toString(),
|
|
16118
|
+
currency: params.currency,
|
|
16119
|
+
receiver: params.receiver,
|
|
16120
|
+
memo: params.memo,
|
|
16121
|
+
}),
|
|
16122
|
+
});
|
|
16123
|
+
if (!response.ok) {
|
|
16124
|
+
const text = await response.text();
|
|
16125
|
+
throw new Error(`ATXPAccount: /address_for_payment failed: ${response.status} ${response.statusText} ${text}`);
|
|
16126
|
+
}
|
|
16127
|
+
const json = await response.json();
|
|
16128
|
+
if (!json?.sourceAddress) {
|
|
16129
|
+
throw new Error('ATXPAccount: /address_for_payment did not return sourceAddress');
|
|
16130
|
+
}
|
|
16131
|
+
return json.sourceAddress;
|
|
16132
|
+
}
|
|
16133
|
+
async makePayment(destinations, memo, paymentRequestId) {
|
|
16134
|
+
// Make a payment via the /pay endpoint with multiple destinations
|
|
16278
16135
|
const response = await this.fetchFn(`${this.origin}/pay`, {
|
|
16279
16136
|
method: 'POST',
|
|
16280
16137
|
headers: {
|
|
@@ -16282,10 +16139,14 @@ class ATXPHttpPaymentMaker {
|
|
|
16282
16139
|
'Content-Type': 'application/json',
|
|
16283
16140
|
},
|
|
16284
16141
|
body: JSON.stringify({
|
|
16285
|
-
|
|
16286
|
-
|
|
16287
|
-
|
|
16142
|
+
destinations: destinations.map(d => ({
|
|
16143
|
+
chain: d.chain,
|
|
16144
|
+
address: d.address,
|
|
16145
|
+
amount: d.amount.toString(),
|
|
16146
|
+
currency: d.currency
|
|
16147
|
+
})),
|
|
16288
16148
|
memo,
|
|
16149
|
+
...(paymentRequestId && { paymentRequestId })
|
|
16289
16150
|
}),
|
|
16290
16151
|
});
|
|
16291
16152
|
if (!response.ok) {
|
|
@@ -16293,10 +16154,22 @@ class ATXPHttpPaymentMaker {
|
|
|
16293
16154
|
throw new Error(`ATXPAccount: /pay failed: ${response.status} ${response.statusText} ${text}`);
|
|
16294
16155
|
}
|
|
16295
16156
|
const json = await response.json();
|
|
16296
|
-
|
|
16297
|
-
|
|
16157
|
+
const transactionId = json.transactionId;
|
|
16158
|
+
if (!transactionId) {
|
|
16159
|
+
throw new Error('ATXPAccount: /pay did not return transactionId or txHash');
|
|
16298
16160
|
}
|
|
16299
|
-
|
|
16161
|
+
if (!json?.chain) {
|
|
16162
|
+
throw new Error('ATXPAccount: /pay did not return chain');
|
|
16163
|
+
}
|
|
16164
|
+
if (!json?.currency) {
|
|
16165
|
+
throw new Error('ATXPAccount: /pay did not return currency');
|
|
16166
|
+
}
|
|
16167
|
+
return {
|
|
16168
|
+
transactionId,
|
|
16169
|
+
...(json.transactionSubId ? { transactionSubId: json.transactionSubId } : {}),
|
|
16170
|
+
chain: json.chain,
|
|
16171
|
+
currency: json.currency
|
|
16172
|
+
};
|
|
16300
16173
|
}
|
|
16301
16174
|
async generateJWT(params) {
|
|
16302
16175
|
const response = await this.fetchFn(`${this.origin}/sign`, {
|
|
@@ -16308,6 +16181,7 @@ class ATXPHttpPaymentMaker {
|
|
|
16308
16181
|
body: JSON.stringify({
|
|
16309
16182
|
paymentRequestId: params.paymentRequestId,
|
|
16310
16183
|
codeChallenge: params.codeChallenge,
|
|
16184
|
+
...(params.accountId ? { accountId: params.accountId } : {}),
|
|
16311
16185
|
}),
|
|
16312
16186
|
});
|
|
16313
16187
|
if (!response.ok) {
|
|
@@ -16325,23 +16199,418 @@ class ATXPAccount {
|
|
|
16325
16199
|
constructor(connectionString, opts) {
|
|
16326
16200
|
const { origin, token, accountId } = parseConnectionString(connectionString);
|
|
16327
16201
|
const fetchFn = opts?.fetchFn ?? fetch;
|
|
16328
|
-
const network = opts?.network ?? 'base';
|
|
16329
16202
|
// Store for use in X402 payment creation
|
|
16330
16203
|
this.origin = origin;
|
|
16331
16204
|
this.token = token;
|
|
16332
16205
|
this.fetchFn = fetchFn;
|
|
16333
|
-
|
|
16334
|
-
|
|
16206
|
+
// Format accountId as network:address
|
|
16207
|
+
// Connection string provides just the atxp_acct_xxx part (no prefix for UI)
|
|
16208
|
+
this.unqualifiedAccountId = accountId;
|
|
16209
|
+
this.accountId = `atxp:${accountId}`;
|
|
16210
|
+
this.paymentMakers = [
|
|
16211
|
+
new ATXPHttpPaymentMaker(origin, token, fetchFn)
|
|
16212
|
+
];
|
|
16213
|
+
}
|
|
16214
|
+
async getSigner() {
|
|
16215
|
+
return ATXPLocalAccount.create(this.origin, this.token, this.fetchFn);
|
|
16216
|
+
}
|
|
16217
|
+
/**
|
|
16218
|
+
* Get sources for this account by calling the accounts service
|
|
16219
|
+
*/
|
|
16220
|
+
async getSources() {
|
|
16221
|
+
// Use the unqualified account ID (without atxp: prefix) for the API call
|
|
16222
|
+
const response = await this.fetchFn(`${this.origin}/account/${this.unqualifiedAccountId}/sources`, {
|
|
16223
|
+
method: 'GET',
|
|
16224
|
+
headers: {
|
|
16225
|
+
'Accept': 'application/json',
|
|
16226
|
+
}
|
|
16227
|
+
});
|
|
16228
|
+
if (!response.ok) {
|
|
16229
|
+
const text = await response.text();
|
|
16230
|
+
throw new Error(`ATXPAccount: /account/${this.unqualifiedAccountId}/sources failed: ${response.status} ${response.statusText} ${text}`);
|
|
16335
16231
|
}
|
|
16336
|
-
|
|
16337
|
-
|
|
16232
|
+
const json = await response.json();
|
|
16233
|
+
// The accounts service returns the sources array directly, not wrapped in an object
|
|
16234
|
+
if (!Array.isArray(json)) {
|
|
16235
|
+
throw new Error(`ATXPAccount: /account/${this.unqualifiedAccountId}/sources did not return sources array`);
|
|
16236
|
+
}
|
|
16237
|
+
return json;
|
|
16238
|
+
}
|
|
16239
|
+
}
|
|
16240
|
+
|
|
16241
|
+
// Detect if we're in a browser environment and bind fetch appropriately
|
|
16242
|
+
const getFetch = () => {
|
|
16243
|
+
if (typeof window !== 'undefined' && typeof window.fetch === 'function') {
|
|
16244
|
+
// In browser, bind fetch to window to avoid "Illegal invocation" errors
|
|
16245
|
+
return fetch.bind(window);
|
|
16246
|
+
}
|
|
16247
|
+
// In Node.js or other environments, use fetch as-is
|
|
16248
|
+
return fetch;
|
|
16249
|
+
};
|
|
16250
|
+
const DEFAULT_CLIENT_CONFIG = {
|
|
16251
|
+
allowedAuthorizationServers: [DEFAULT_AUTHORIZATION_SERVER],
|
|
16252
|
+
atxpAccountsServer: DEFAULT_ATXP_ACCOUNTS_SERVER,
|
|
16253
|
+
approvePayment: async (_p) => true,
|
|
16254
|
+
fetchFn: getFetch(),
|
|
16255
|
+
oAuthChannelFetch: getFetch(),
|
|
16256
|
+
allowHttp: false, // may be overridden in buildClientConfig by process.env.NODE_ENV
|
|
16257
|
+
clientInfo: {
|
|
16258
|
+
name: 'ATXPClient',
|
|
16259
|
+
version: '0.0.1'
|
|
16260
|
+
},
|
|
16261
|
+
clientOptions: {
|
|
16262
|
+
capabilities: {}
|
|
16263
|
+
},
|
|
16264
|
+
onAuthorize: async () => { },
|
|
16265
|
+
onAuthorizeFailure: async () => { },
|
|
16266
|
+
onPayment: async () => { },
|
|
16267
|
+
onPaymentFailure: async () => { }
|
|
16268
|
+
};
|
|
16269
|
+
function buildClientConfig(args) {
|
|
16270
|
+
// Use fetchFn for oAuthChannelFetch if the latter isn't explicitly set
|
|
16271
|
+
if (args.fetchFn && !args.oAuthChannelFetch) {
|
|
16272
|
+
args.oAuthChannelFetch = args.fetchFn;
|
|
16273
|
+
}
|
|
16274
|
+
// Read environment variable at runtime, not module load time
|
|
16275
|
+
const envDefaults = {
|
|
16276
|
+
...DEFAULT_CLIENT_CONFIG,
|
|
16277
|
+
allowHttp: process.env.NODE_ENV === 'development',
|
|
16278
|
+
};
|
|
16279
|
+
const withDefaults = { ...envDefaults, ...args };
|
|
16280
|
+
const logger = withDefaults.logger ?? new ConsoleLogger();
|
|
16281
|
+
const oAuthDb = withDefaults.oAuthDb ?? new MemoryOAuthDb({ logger });
|
|
16282
|
+
const fetchFn = withDefaults.fetchFn;
|
|
16283
|
+
// Build destination makers if not provided
|
|
16284
|
+
let accountsServer = withDefaults.atxpAccountsServer;
|
|
16285
|
+
// QoL hack for unspecified accounts server - if the caller is passing an atxpAccount, then assume the origin for that
|
|
16286
|
+
// is what we should use for the accounts server. In practice, the only option is accounts.atxp.ai,
|
|
16287
|
+
// but this supports staging environment
|
|
16288
|
+
if (args.atxpAccountsServer === undefined && withDefaults.account && withDefaults.account instanceof ATXPAccount) {
|
|
16289
|
+
accountsServer = withDefaults.account.origin;
|
|
16290
|
+
}
|
|
16291
|
+
const destinationMakers = withDefaults.destinationMakers ?? createDestinationMakers({
|
|
16292
|
+
atxpAccountsServer: accountsServer,
|
|
16293
|
+
fetchFn
|
|
16294
|
+
});
|
|
16295
|
+
const built = { oAuthDb, logger, destinationMakers };
|
|
16296
|
+
return Object.freeze({ ...withDefaults, ...built });
|
|
16297
|
+
}
|
|
16298
|
+
function buildStreamableTransport(args) {
|
|
16299
|
+
const config = buildClientConfig(args);
|
|
16300
|
+
// Apply the ATXP wrapper to the fetch function
|
|
16301
|
+
const wrappedFetch = atxpFetch(config);
|
|
16302
|
+
const transport = new StreamableHTTPClientTransport(new URL(args.mcpServer), { fetch: wrappedFetch });
|
|
16303
|
+
return transport;
|
|
16304
|
+
}
|
|
16305
|
+
async function atxpClient(args) {
|
|
16306
|
+
const config = buildClientConfig(args);
|
|
16307
|
+
const transport = buildStreamableTransport(config);
|
|
16308
|
+
const client = new Client(config.clientInfo, config.clientOptions);
|
|
16309
|
+
await client.connect(transport);
|
|
16310
|
+
return client;
|
|
16311
|
+
}
|
|
16312
|
+
|
|
16313
|
+
// this is a global public key for USDC on the solana mainnet
|
|
16314
|
+
const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
|
|
16315
|
+
const ValidateTransferError = ValidateTransferError$1;
|
|
16316
|
+
class SolanaPaymentMaker {
|
|
16317
|
+
constructor(solanaEndpoint, sourceSecretKey, logger) {
|
|
16318
|
+
this.generateJWT = async ({ paymentRequestId, codeChallenge, accountId }) => {
|
|
16319
|
+
// Solana/Web3.js secretKey is 64 bytes:
|
|
16320
|
+
// first 32 bytes are the private scalar, last 32 are the public key.
|
|
16321
|
+
// JWK expects only the 32-byte private scalar for 'd'
|
|
16322
|
+
const jwk = {
|
|
16323
|
+
kty: 'OKP',
|
|
16324
|
+
crv: 'Ed25519',
|
|
16325
|
+
d: Buffer.from(this.source.secretKey.slice(0, 32)).toString('base64url'),
|
|
16326
|
+
x: Buffer.from(this.source.publicKey.toBytes()).toString('base64url'),
|
|
16327
|
+
};
|
|
16328
|
+
const privateKey = await importJWK(jwk, 'EdDSA');
|
|
16329
|
+
if (!(privateKey instanceof CryptoKey)) {
|
|
16330
|
+
throw new Error('Expected CryptoKey from importJWK');
|
|
16331
|
+
}
|
|
16332
|
+
return generateJWT(this.source.publicKey.toBase58(), privateKey, paymentRequestId || '', codeChallenge || '', accountId);
|
|
16333
|
+
};
|
|
16334
|
+
this.makePayment = async (destinations, memo, _paymentRequestId) => {
|
|
16335
|
+
// Filter to solana chain destinations
|
|
16336
|
+
const solanaDestinations = destinations.filter(d => d.chain === 'solana');
|
|
16337
|
+
if (solanaDestinations.length === 0) {
|
|
16338
|
+
this.logger.debug('SolanaPaymentMaker: No solana destinations found, cannot handle payment');
|
|
16339
|
+
return null; // Cannot handle these destinations
|
|
16340
|
+
}
|
|
16341
|
+
// Pick first solana destination
|
|
16342
|
+
const dest = solanaDestinations[0];
|
|
16343
|
+
const amount = dest.amount;
|
|
16344
|
+
const currency = dest.currency;
|
|
16345
|
+
const receiver = dest.address;
|
|
16346
|
+
if (currency.toUpperCase() !== 'USDC') {
|
|
16347
|
+
throw new PaymentNetworkError('Only USDC currency is supported; received ' + currency);
|
|
16348
|
+
}
|
|
16349
|
+
const receiverKey = new PublicKey(receiver);
|
|
16350
|
+
this.logger.info(`Making payment of ${amount} ${currency} to ${receiver} on Solana from ${this.source.publicKey.toBase58()}`);
|
|
16351
|
+
try {
|
|
16352
|
+
// Check balance before attempting payment
|
|
16353
|
+
const tokenAccountAddress = await getAssociatedTokenAddress(USDC_MINT, this.source.publicKey);
|
|
16354
|
+
const tokenAccount = await getAccount(this.connection, tokenAccountAddress);
|
|
16355
|
+
const balance = new BigNumber$1(tokenAccount.amount.toString()).dividedBy(10 ** 6); // USDC has 6 decimals
|
|
16356
|
+
if (balance.lt(amount)) {
|
|
16357
|
+
this.logger.warn(`Insufficient ${currency} balance for payment. Required: ${amount}, Available: ${balance}`);
|
|
16358
|
+
throw new InsufficientFundsError(currency, amount, balance, 'solana');
|
|
16359
|
+
}
|
|
16360
|
+
// Get the destination token account address (this will be the transactionId)
|
|
16361
|
+
const destinationTokenAccount = await getAssociatedTokenAddress(USDC_MINT, receiverKey);
|
|
16362
|
+
// Increase compute units to handle both memo and token transfer
|
|
16363
|
+
// Memo uses ~6000 CUs, token transfer needs ~6500 CUs
|
|
16364
|
+
const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({
|
|
16365
|
+
units: 50000,
|
|
16366
|
+
});
|
|
16367
|
+
const addPriorityFee = ComputeBudgetProgram.setComputeUnitPrice({
|
|
16368
|
+
microLamports: 20000,
|
|
16369
|
+
});
|
|
16370
|
+
const transaction = await createTransfer(this.connection, this.source.publicKey, {
|
|
16371
|
+
amount: amount,
|
|
16372
|
+
recipient: receiverKey,
|
|
16373
|
+
splToken: USDC_MINT,
|
|
16374
|
+
memo,
|
|
16375
|
+
});
|
|
16376
|
+
transaction.add(modifyComputeUnits);
|
|
16377
|
+
transaction.add(addPriorityFee);
|
|
16378
|
+
const transactionSignature = await sendAndConfirmTransaction(this.connection, transaction, [this.source]);
|
|
16379
|
+
// Return transaction signature as transactionId and token account address as transactionSubId
|
|
16380
|
+
return {
|
|
16381
|
+
transactionId: transactionSignature,
|
|
16382
|
+
transactionSubId: destinationTokenAccount.toBase58(),
|
|
16383
|
+
chain: 'solana',
|
|
16384
|
+
currency: 'USDC'
|
|
16385
|
+
};
|
|
16386
|
+
}
|
|
16387
|
+
catch (error) {
|
|
16388
|
+
if (error instanceof InsufficientFundsError || error instanceof PaymentNetworkError) {
|
|
16389
|
+
throw error;
|
|
16390
|
+
}
|
|
16391
|
+
// Wrap other errors in PaymentNetworkError
|
|
16392
|
+
throw new PaymentNetworkError(`Payment failed on Solana network: ${error.message}`, error);
|
|
16393
|
+
}
|
|
16394
|
+
};
|
|
16395
|
+
if (!solanaEndpoint) {
|
|
16396
|
+
throw new Error('Solana endpoint is required');
|
|
16397
|
+
}
|
|
16398
|
+
if (!sourceSecretKey) {
|
|
16399
|
+
throw new Error('Source secret key is required');
|
|
16400
|
+
}
|
|
16401
|
+
this.connection = new Connection(solanaEndpoint, { commitment: 'confirmed' });
|
|
16402
|
+
this.source = Keypair.fromSecretKey(bs58.decode(sourceSecretKey));
|
|
16403
|
+
this.logger = logger ?? new ConsoleLogger();
|
|
16404
|
+
}
|
|
16405
|
+
getSourceAddress(_params) {
|
|
16406
|
+
return this.source.publicKey.toBase58();
|
|
16407
|
+
}
|
|
16408
|
+
}
|
|
16409
|
+
|
|
16410
|
+
const USDC_CONTRACT_ADDRESS_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // USDC on Base mainnet
|
|
16411
|
+
const USDC_CONTRACT_ADDRESS_BASE_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"; // USDC on Base Sepolia testnet
|
|
16412
|
+
/**
|
|
16413
|
+
* Get USDC contract address for Base chain by chain ID
|
|
16414
|
+
* @param chainId - Chain ID (8453 for mainnet, 84532 for sepolia)
|
|
16415
|
+
* @returns USDC contract address
|
|
16416
|
+
* @throws Error if chain ID is not supported
|
|
16417
|
+
*/
|
|
16418
|
+
const getBaseUSDCAddress = (chainId) => {
|
|
16419
|
+
switch (chainId) {
|
|
16420
|
+
case 8453: // Base mainnet
|
|
16421
|
+
return USDC_CONTRACT_ADDRESS_BASE;
|
|
16422
|
+
case 84532: // Base Sepolia
|
|
16423
|
+
return USDC_CONTRACT_ADDRESS_BASE_SEPOLIA;
|
|
16424
|
+
default:
|
|
16425
|
+
throw new Error(`Unsupported Base Chain ID: ${chainId}. Supported chains: 8453 (mainnet), 84532 (sepolia)`);
|
|
16426
|
+
}
|
|
16427
|
+
};
|
|
16428
|
+
|
|
16429
|
+
// Helper function to convert to base64url that works in both Node.js and browsers
|
|
16430
|
+
function toBase64Url(data) {
|
|
16431
|
+
// Convert string to base64
|
|
16432
|
+
const base64 = typeof Buffer !== 'undefined'
|
|
16433
|
+
? Buffer.from(data).toString('base64')
|
|
16434
|
+
: btoa(data);
|
|
16435
|
+
// Convert base64 to base64url
|
|
16436
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
16437
|
+
}
|
|
16438
|
+
const USDC_DECIMALS = 6;
|
|
16439
|
+
const ERC20_ABI = [
|
|
16440
|
+
{
|
|
16441
|
+
constant: false,
|
|
16442
|
+
inputs: [
|
|
16443
|
+
{ name: "_to", type: "address" },
|
|
16444
|
+
{ name: "_value", type: "uint256" },
|
|
16445
|
+
],
|
|
16446
|
+
name: "transfer",
|
|
16447
|
+
outputs: [{ name: "", type: "bool" }],
|
|
16448
|
+
type: "function",
|
|
16449
|
+
},
|
|
16450
|
+
{
|
|
16451
|
+
"constant": true,
|
|
16452
|
+
"inputs": [
|
|
16453
|
+
{
|
|
16454
|
+
"name": "_owner",
|
|
16455
|
+
"type": "address"
|
|
16456
|
+
}
|
|
16457
|
+
],
|
|
16458
|
+
"name": "balanceOf",
|
|
16459
|
+
"outputs": [
|
|
16460
|
+
{
|
|
16461
|
+
"name": "balance",
|
|
16462
|
+
"type": "uint256"
|
|
16463
|
+
}
|
|
16464
|
+
],
|
|
16465
|
+
"payable": false,
|
|
16466
|
+
"stateMutability": "view",
|
|
16467
|
+
"type": "function"
|
|
16468
|
+
}
|
|
16469
|
+
];
|
|
16470
|
+
class BasePaymentMaker {
|
|
16471
|
+
constructor(baseRPCUrl, walletClient, logger) {
|
|
16472
|
+
if (!baseRPCUrl) {
|
|
16473
|
+
throw new Error('baseRPCUrl was empty');
|
|
16474
|
+
}
|
|
16475
|
+
if (!walletClient) {
|
|
16476
|
+
throw new Error('walletClient was empty');
|
|
16477
|
+
}
|
|
16478
|
+
if (!walletClient.account) {
|
|
16479
|
+
throw new Error('walletClient.account was empty');
|
|
16338
16480
|
}
|
|
16339
|
-
this.
|
|
16340
|
-
|
|
16481
|
+
this.signingClient = walletClient.extend(publicActions);
|
|
16482
|
+
this.logger = logger ?? new ConsoleLogger();
|
|
16483
|
+
}
|
|
16484
|
+
getSourceAddress(_params) {
|
|
16485
|
+
return this.signingClient.account.address;
|
|
16486
|
+
}
|
|
16487
|
+
async generateJWT({ paymentRequestId, codeChallenge, accountId }) {
|
|
16488
|
+
const headerObj = { alg: 'ES256K' };
|
|
16489
|
+
const payloadObj = {
|
|
16490
|
+
sub: this.signingClient.account.address,
|
|
16491
|
+
iss: 'accounts.atxp.ai',
|
|
16492
|
+
aud: 'https://auth.atxp.ai',
|
|
16493
|
+
iat: Math.floor(Date.now() / 1000),
|
|
16494
|
+
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
|
16495
|
+
...(codeChallenge ? { code_challenge: codeChallenge } : {}),
|
|
16496
|
+
...(paymentRequestId ? { payment_request_id: paymentRequestId } : {}),
|
|
16497
|
+
...(accountId ? { account_id: accountId } : {}),
|
|
16341
16498
|
};
|
|
16499
|
+
const header = toBase64Url(JSON.stringify(headerObj));
|
|
16500
|
+
const payload = toBase64Url(JSON.stringify(payloadObj));
|
|
16501
|
+
const message = `${header}.${payload}`;
|
|
16502
|
+
const messageBytes = typeof Buffer !== 'undefined'
|
|
16503
|
+
? Buffer.from(message, 'utf8')
|
|
16504
|
+
: new TextEncoder().encode(message);
|
|
16505
|
+
const signResult = await this.signingClient.signMessage({
|
|
16506
|
+
account: this.signingClient.account,
|
|
16507
|
+
message: { raw: messageBytes },
|
|
16508
|
+
});
|
|
16509
|
+
// For ES256K, signature is typically 65 bytes (r,s,v)
|
|
16510
|
+
// Server expects the hex signature string (with 0x prefix) to be base64url encoded
|
|
16511
|
+
// This creates: base64url("0x6eb2565...") not base64url(rawBytes)
|
|
16512
|
+
// Pass the hex string directly to toBase64Url which will UTF-8 encode and base64url it
|
|
16513
|
+
const signature = toBase64Url(signResult);
|
|
16514
|
+
const jwt = `${header}.${payload}.${signature}`;
|
|
16515
|
+
this.logger.info(`Generated ES256K JWT: ${jwt}`);
|
|
16516
|
+
return jwt;
|
|
16342
16517
|
}
|
|
16343
|
-
async
|
|
16344
|
-
|
|
16518
|
+
async makePayment(destinations, _memo, _paymentRequestId) {
|
|
16519
|
+
// Filter to base chain destinations
|
|
16520
|
+
const baseDestinations = destinations.filter(d => d.chain === 'base');
|
|
16521
|
+
if (baseDestinations.length === 0) {
|
|
16522
|
+
this.logger.debug('BasePaymentMaker: No base destinations found, cannot handle payment');
|
|
16523
|
+
return null; // Cannot handle these destinations
|
|
16524
|
+
}
|
|
16525
|
+
// Pick first base destination
|
|
16526
|
+
const dest = baseDestinations[0];
|
|
16527
|
+
const amount = dest.amount;
|
|
16528
|
+
const currency = dest.currency;
|
|
16529
|
+
const receiver = dest.address;
|
|
16530
|
+
if (currency.toUpperCase() !== 'USDC') {
|
|
16531
|
+
throw new PaymentNetworkError('Only USDC currency is supported; received ' + currency);
|
|
16532
|
+
}
|
|
16533
|
+
this.logger.info(`Making payment of ${amount} ${currency} to ${receiver} on Base from ${this.signingClient.account.address}`);
|
|
16534
|
+
try {
|
|
16535
|
+
// Check balance before attempting payment
|
|
16536
|
+
const balanceRaw = await this.signingClient.readContract({
|
|
16537
|
+
address: USDC_CONTRACT_ADDRESS_BASE,
|
|
16538
|
+
abi: ERC20_ABI,
|
|
16539
|
+
functionName: 'balanceOf',
|
|
16540
|
+
args: [this.signingClient.account.address],
|
|
16541
|
+
});
|
|
16542
|
+
const balance = new BigNumber(balanceRaw.toString()).dividedBy(10 ** USDC_DECIMALS);
|
|
16543
|
+
if (balance.lt(amount)) {
|
|
16544
|
+
this.logger.warn(`Insufficient ${currency} balance for payment. Required: ${amount}, Available: ${balance}`);
|
|
16545
|
+
throw new InsufficientFundsError(currency, amount, balance, 'base');
|
|
16546
|
+
}
|
|
16547
|
+
// Convert amount to USDC units (6 decimals) as BigInt
|
|
16548
|
+
const amountInUSDCUnits = BigInt(amount.multipliedBy(10 ** USDC_DECIMALS).toFixed(0));
|
|
16549
|
+
const data = encodeFunctionData({
|
|
16550
|
+
abi: ERC20_ABI,
|
|
16551
|
+
functionName: "transfer",
|
|
16552
|
+
args: [receiver, amountInUSDCUnits],
|
|
16553
|
+
});
|
|
16554
|
+
const hash = await this.signingClient.sendTransaction({
|
|
16555
|
+
chain: base,
|
|
16556
|
+
account: this.signingClient.account,
|
|
16557
|
+
to: USDC_CONTRACT_ADDRESS_BASE,
|
|
16558
|
+
data: data,
|
|
16559
|
+
value: parseEther('0'),
|
|
16560
|
+
maxPriorityFeePerGas: parseEther('0.000000001')
|
|
16561
|
+
});
|
|
16562
|
+
// Wait for transaction confirmation with more blocks to ensure propagation
|
|
16563
|
+
this.logger.info(`Waiting for transaction confirmation: ${hash}`);
|
|
16564
|
+
const receipt = await this.signingClient.waitForTransactionReceipt({
|
|
16565
|
+
hash: hash,
|
|
16566
|
+
confirmations: 1
|
|
16567
|
+
});
|
|
16568
|
+
if (receipt.status === 'reverted') {
|
|
16569
|
+
throw new PaymentNetworkError(`Transaction reverted: ${hash}`, new Error('Transaction reverted on chain'));
|
|
16570
|
+
}
|
|
16571
|
+
this.logger.info(`Transaction confirmed: ${hash} in block ${receipt.blockNumber}`);
|
|
16572
|
+
// Return payment result with chain and currency
|
|
16573
|
+
return {
|
|
16574
|
+
transactionId: hash,
|
|
16575
|
+
chain: 'base',
|
|
16576
|
+
currency: 'USDC'
|
|
16577
|
+
};
|
|
16578
|
+
}
|
|
16579
|
+
catch (error) {
|
|
16580
|
+
if (error instanceof InsufficientFundsError || error instanceof PaymentNetworkError) {
|
|
16581
|
+
throw error;
|
|
16582
|
+
}
|
|
16583
|
+
// Wrap other errors in PaymentNetworkError
|
|
16584
|
+
throw new PaymentNetworkError(`Payment failed on Base network: ${error.message}`, error);
|
|
16585
|
+
}
|
|
16586
|
+
}
|
|
16587
|
+
}
|
|
16588
|
+
|
|
16589
|
+
class SolanaAccount {
|
|
16590
|
+
constructor(solanaEndpoint, sourceSecretKey) {
|
|
16591
|
+
if (!solanaEndpoint) {
|
|
16592
|
+
throw new Error('Solana endpoint is required');
|
|
16593
|
+
}
|
|
16594
|
+
if (!sourceSecretKey) {
|
|
16595
|
+
throw new Error('Source secret key is required');
|
|
16596
|
+
}
|
|
16597
|
+
const source = Keypair.fromSecretKey(bs58.decode(sourceSecretKey));
|
|
16598
|
+
this.sourcePublicKey = source.publicKey.toBase58();
|
|
16599
|
+
// Format accountId as network:address
|
|
16600
|
+
this.accountId = `solana:${this.sourcePublicKey}`;
|
|
16601
|
+
this.paymentMakers = [
|
|
16602
|
+
new SolanaPaymentMaker(solanaEndpoint, sourceSecretKey)
|
|
16603
|
+
];
|
|
16604
|
+
}
|
|
16605
|
+
/**
|
|
16606
|
+
* Get sources for this account
|
|
16607
|
+
*/
|
|
16608
|
+
async getSources() {
|
|
16609
|
+
return [{
|
|
16610
|
+
address: this.sourcePublicKey,
|
|
16611
|
+
chain: 'solana',
|
|
16612
|
+
walletType: 'eoa'
|
|
16613
|
+
}];
|
|
16345
16614
|
}
|
|
16346
16615
|
}
|
|
16347
16616
|
|
|
@@ -16425,6 +16694,58 @@ const getWorldChainUSDCAddress = (chainId) => {
|
|
|
16425
16694
|
}
|
|
16426
16695
|
};
|
|
16427
16696
|
|
|
16697
|
+
const USDC_CONTRACT_ADDRESS_POLYGON_MAINNET = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"; // Native USDC on Polygon mainnet
|
|
16698
|
+
// Polygon Mainnet (Chain ID: 137)
|
|
16699
|
+
const POLYGON_MAINNET = {
|
|
16700
|
+
id: 137,
|
|
16701
|
+
name: 'Polygon',
|
|
16702
|
+
nativeCurrency: { name: 'MATIC', symbol: 'MATIC', decimals: 18 },
|
|
16703
|
+
rpcUrls: {
|
|
16704
|
+
default: { http: ['https://polygon-rpc.com'] }
|
|
16705
|
+
},
|
|
16706
|
+
blockExplorers: {
|
|
16707
|
+
default: { name: 'PolygonScan', url: 'https://polygonscan.com' }
|
|
16708
|
+
}
|
|
16709
|
+
};
|
|
16710
|
+
/**
|
|
16711
|
+
* Get Polygon Mainnet configuration with custom RPC URL (e.g., with API key)
|
|
16712
|
+
* @param rpcUrl - Custom RPC URL, e.g., 'https://polygon-mainnet.g.alchemy.com/v2/YOUR_API_KEY'
|
|
16713
|
+
*/
|
|
16714
|
+
const getPolygonMainnetWithRPC = (rpcUrl) => ({
|
|
16715
|
+
...POLYGON_MAINNET,
|
|
16716
|
+
rpcUrls: {
|
|
16717
|
+
default: { http: [rpcUrl] }
|
|
16718
|
+
}
|
|
16719
|
+
});
|
|
16720
|
+
/**
|
|
16721
|
+
* Get Polygon Chain configuration by chain ID
|
|
16722
|
+
* @param chainId - Chain ID (137 for mainnet)
|
|
16723
|
+
* @returns Polygon Chain configuration
|
|
16724
|
+
* @throws Error if chain ID is not supported
|
|
16725
|
+
*/
|
|
16726
|
+
const getPolygonByChainId = (chainId) => {
|
|
16727
|
+
switch (chainId) {
|
|
16728
|
+
case 137:
|
|
16729
|
+
return POLYGON_MAINNET;
|
|
16730
|
+
default:
|
|
16731
|
+
throw new Error(`Unsupported Polygon Chain ID: ${chainId}. Supported chains: 137 (mainnet)`);
|
|
16732
|
+
}
|
|
16733
|
+
};
|
|
16734
|
+
/**
|
|
16735
|
+
* Get USDC contract address for Polygon by chain ID
|
|
16736
|
+
* @param chainId - Chain ID (137 for mainnet)
|
|
16737
|
+
* @returns USDC contract address
|
|
16738
|
+
* @throws Error if chain ID is not supported
|
|
16739
|
+
*/
|
|
16740
|
+
const getPolygonUSDCAddress = (chainId) => {
|
|
16741
|
+
switch (chainId) {
|
|
16742
|
+
case 137:
|
|
16743
|
+
return USDC_CONTRACT_ADDRESS_POLYGON_MAINNET;
|
|
16744
|
+
default:
|
|
16745
|
+
throw new Error(`Unsupported Polygon Chain ID: ${chainId}. Supported chains: 137 (mainnet)`);
|
|
16746
|
+
}
|
|
16747
|
+
};
|
|
16748
|
+
|
|
16428
16749
|
class BaseAccount {
|
|
16429
16750
|
constructor(baseRPCUrl, sourceSecretKey) {
|
|
16430
16751
|
if (!baseRPCUrl) {
|
|
@@ -16434,15 +16755,16 @@ class BaseAccount {
|
|
|
16434
16755
|
throw new Error('Source secret key is required');
|
|
16435
16756
|
}
|
|
16436
16757
|
this.account = privateKeyToAccount(sourceSecretKey);
|
|
16437
|
-
|
|
16758
|
+
// Format accountId as network:address
|
|
16759
|
+
this.accountId = `base:${this.account.address}`;
|
|
16438
16760
|
this.walletClient = createWalletClient({
|
|
16439
16761
|
account: this.account,
|
|
16440
16762
|
chain: base,
|
|
16441
16763
|
transport: http(baseRPCUrl),
|
|
16442
16764
|
});
|
|
16443
|
-
this.paymentMakers =
|
|
16444
|
-
|
|
16445
|
-
|
|
16765
|
+
this.paymentMakers = [
|
|
16766
|
+
new BasePaymentMaker(baseRPCUrl, this.walletClient)
|
|
16767
|
+
];
|
|
16446
16768
|
}
|
|
16447
16769
|
/**
|
|
16448
16770
|
* Get a signer that can be used with the x402 library
|
|
@@ -16452,7 +16774,17 @@ class BaseAccount {
|
|
|
16452
16774
|
// Return the viem account directly - it implements LocalAccount interface
|
|
16453
16775
|
return this.account;
|
|
16454
16776
|
}
|
|
16777
|
+
/**
|
|
16778
|
+
* Get sources for this account
|
|
16779
|
+
*/
|
|
16780
|
+
async getSources() {
|
|
16781
|
+
return [{
|
|
16782
|
+
address: this.account.address,
|
|
16783
|
+
chain: 'base',
|
|
16784
|
+
walletType: 'eoa'
|
|
16785
|
+
}];
|
|
16786
|
+
}
|
|
16455
16787
|
}
|
|
16456
16788
|
|
|
16457
|
-
export { ATXPAccount, ATXPLocalAccount, BaseAccount, BasePaymentMaker, DEFAULT_CLIENT_CONFIG, InsufficientFundsError, OAuthAuthenticationRequiredError, OAuthClient, PaymentNetworkError, SolanaAccount, SolanaPaymentMaker, USDC_CONTRACT_ADDRESS_BASE, USDC_CONTRACT_ADDRESS_BASE_SEPOLIA, USDC_CONTRACT_ADDRESS_WORLD_MAINNET, USDC_CONTRACT_ADDRESS_WORLD_SEPOLIA, ValidateTransferError, WORLD_CHAIN_MAINNET, WORLD_CHAIN_SEPOLIA, atxpClient, atxpFetch, buildClientConfig, buildStreamableTransport, getBaseUSDCAddress, getWorldChainByChainId, getWorldChainMainnetWithRPC, getWorldChainSepoliaWithRPC, getWorldChainUSDCAddress };
|
|
16789
|
+
export { ATXPAccount, ATXPDestinationMaker, ATXPLocalAccount, BaseAccount, BasePaymentMaker, DEFAULT_CLIENT_CONFIG, InsufficientFundsError, OAuthAuthenticationRequiredError, OAuthClient, POLYGON_MAINNET, PassthroughDestinationMaker, PaymentNetworkError, SolanaAccount, SolanaPaymentMaker, USDC_CONTRACT_ADDRESS_BASE, USDC_CONTRACT_ADDRESS_BASE_SEPOLIA, USDC_CONTRACT_ADDRESS_POLYGON_MAINNET, USDC_CONTRACT_ADDRESS_WORLD_MAINNET, USDC_CONTRACT_ADDRESS_WORLD_SEPOLIA, ValidateTransferError, WORLD_CHAIN_MAINNET, WORLD_CHAIN_SEPOLIA, atxpClient, atxpFetch, buildClientConfig, buildStreamableTransport, getBaseUSDCAddress, getPolygonByChainId, getPolygonMainnetWithRPC, getPolygonUSDCAddress, getWorldChainByChainId, getWorldChainMainnetWithRPC, getWorldChainSepoliaWithRPC, getWorldChainUSDCAddress };
|
|
16458
16790
|
//# sourceMappingURL=index.js.map
|