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