payment-kit 1.21.16 → 1.22.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.
Files changed (65) hide show
  1. package/api/src/index.ts +3 -1
  2. package/api/src/integrations/blocklet/user.ts +2 -2
  3. package/api/src/integrations/ethereum/token.ts +4 -5
  4. package/api/src/integrations/stripe/handlers/invoice.ts +31 -26
  5. package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -1
  6. package/api/src/integrations/stripe/handlers/setup-intent.ts +231 -0
  7. package/api/src/integrations/stripe/handlers/subscription.ts +31 -9
  8. package/api/src/integrations/stripe/resource.ts +30 -1
  9. package/api/src/integrations/stripe/setup.ts +1 -1
  10. package/api/src/libs/auth.ts +7 -6
  11. package/api/src/libs/env.ts +1 -1
  12. package/api/src/libs/notification/template/subscription-trial-will-end.ts +1 -0
  13. package/api/src/libs/notification/template/subscription-will-renew.ts +1 -1
  14. package/api/src/libs/payment.ts +11 -6
  15. package/api/src/libs/refund.ts +1 -1
  16. package/api/src/libs/remote-signer.ts +93 -0
  17. package/api/src/libs/security.ts +1 -1
  18. package/api/src/libs/subscription.ts +4 -7
  19. package/api/src/libs/util.ts +18 -1
  20. package/api/src/libs/vendor-util/adapters/didnames-adapter.ts +17 -9
  21. package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +11 -6
  22. package/api/src/queues/payment.ts +2 -2
  23. package/api/src/queues/payout.ts +1 -1
  24. package/api/src/queues/refund.ts +2 -2
  25. package/api/src/queues/subscription.ts +1 -1
  26. package/api/src/queues/usage-record.ts +1 -1
  27. package/api/src/queues/vendors/status-check.ts +1 -1
  28. package/api/src/queues/webhook.ts +1 -1
  29. package/api/src/routes/auto-recharge-configs.ts +1 -1
  30. package/api/src/routes/checkout-sessions.ts +4 -6
  31. package/api/src/routes/connect/change-payer.ts +148 -0
  32. package/api/src/routes/connect/collect-batch.ts +1 -1
  33. package/api/src/routes/connect/collect.ts +1 -1
  34. package/api/src/routes/connect/pay.ts +1 -1
  35. package/api/src/routes/connect/recharge-account.ts +1 -1
  36. package/api/src/routes/connect/recharge.ts +1 -1
  37. package/api/src/routes/connect/shared.ts +62 -23
  38. package/api/src/routes/customers.ts +1 -1
  39. package/api/src/routes/integrations/stripe.ts +1 -1
  40. package/api/src/routes/invoices.ts +141 -2
  41. package/api/src/routes/meter-events.ts +9 -12
  42. package/api/src/routes/payment-currencies.ts +1 -1
  43. package/api/src/routes/payment-intents.ts +2 -2
  44. package/api/src/routes/payment-links.ts +2 -1
  45. package/api/src/routes/payouts.ts +1 -1
  46. package/api/src/routes/products.ts +1 -0
  47. package/api/src/routes/subscriptions.ts +130 -3
  48. package/api/src/store/models/types.ts +1 -1
  49. package/api/tests/setup.ts +11 -0
  50. package/api/third.d.ts +0 -2
  51. package/blocklet.yml +1 -1
  52. package/jest.config.js +2 -2
  53. package/package.json +26 -26
  54. package/src/components/invoice/table.tsx +2 -2
  55. package/src/components/invoice-pdf/template.tsx +30 -0
  56. package/src/components/subscription/payment-method-info.tsx +222 -0
  57. package/src/global.css +4 -0
  58. package/src/libs/util.ts +1 -1
  59. package/src/locales/en.tsx +13 -0
  60. package/src/locales/zh.tsx +13 -0
  61. package/src/pages/admin/billing/invoices/detail.tsx +5 -3
  62. package/src/pages/admin/billing/subscriptions/detail.tsx +16 -0
  63. package/src/pages/admin/overview.tsx +14 -14
  64. package/src/pages/customer/invoice/detail.tsx +59 -17
  65. package/src/pages/customer/subscription/detail.tsx +21 -2
@@ -0,0 +1,93 @@
1
+ import { ethers, JsonRpcProvider, TransactionRequest } from 'ethers';
2
+ import type { WalletObject } from '@ocap/wallet';
3
+
4
+ /**
5
+ * Remote Signer that uses remote signing service for Ethereum transactions
6
+ * Implements required methods from AbstractSigner
7
+ */
8
+ export class RemoteSigner extends ethers.AbstractSigner {
9
+ private wallet: WalletObject;
10
+
11
+ constructor(wallet: WalletObject, provider?: JsonRpcProvider) {
12
+ super(provider);
13
+ this.wallet = wallet;
14
+ }
15
+
16
+ /**
17
+ * Returns the address of the signer
18
+ * Required by AbstractSigner
19
+ */
20
+ getAddress(): Promise<string> {
21
+ return Promise.resolve(this.wallet.address);
22
+ }
23
+
24
+ /**
25
+ * Signs a transaction and returns the signed serialized transaction
26
+ * Required by AbstractSigner
27
+ */
28
+ async signTransaction(transaction: TransactionRequest): Promise<string> {
29
+ // Populate transaction fields (nonce, gasLimit, chainId, etc.)
30
+ const populatedTx = await this.populateTransaction(transaction);
31
+
32
+ // Remove 'from' field as it's not allowed in unsigned transactions
33
+ // The 'from' address is derived from the signature
34
+ const { from, ...txWithoutFrom } = populatedTx;
35
+
36
+ // Serialize unsigned transaction
37
+ const unsignedTx = ethers.Transaction.from(txWithoutFrom);
38
+ const serialized = unsignedTx.unsignedSerialized;
39
+
40
+ // Hash and sign via remote service using signETH (Ethereum-specific signing)
41
+ const hash = ethers.keccak256(serialized);
42
+ const signature = await this.wallet.signETH(hash, false); // hashBeforeSign=false because we already hashed
43
+
44
+ // Attach signature and return signed transaction
45
+ unsignedTx.signature = ethers.Signature.from(signature);
46
+ return unsignedTx.serialized;
47
+ }
48
+
49
+ /**
50
+ * Signs a message and returns the signature
51
+ * Required for message signing operations
52
+ */
53
+ async signMessage(message: string | Uint8Array): Promise<string> {
54
+ // Convert message to bytes if it's a string
55
+ const messageBytes = typeof message === 'string' ? ethers.toUtf8Bytes(message) : message;
56
+
57
+ // Calculate the Ethereum signed message hash
58
+ const messageHash = ethers.hashMessage(messageBytes);
59
+
60
+ // Sign via remote service using signETH (Ethereum-specific signing)
61
+ const signature = await this.wallet.signETH(messageHash, false); // hashBeforeSign=false because hashMessage already hashed
62
+
63
+ // Return signature as hex string
64
+ return ethers.hexlify(signature);
65
+ }
66
+
67
+ /**
68
+ * Signs typed data (EIP-712)
69
+ * Optional but useful for signing structured data
70
+ */
71
+ async signTypedData(
72
+ domain: ethers.TypedDataDomain,
73
+ types: Record<string, ethers.TypedDataField[]>,
74
+ value: Record<string, any>
75
+ ): Promise<string> {
76
+ // Calculate the EIP-712 hash
77
+ const hash = ethers.TypedDataEncoder.hash(domain, types, value);
78
+
79
+ // Sign via remote service using signETH (Ethereum-specific signing)
80
+ const signature = await this.wallet.signETH(hash, false); // hashBeforeSign=false because hash is already computed
81
+
82
+ // Return signature as hex string
83
+ return ethers.hexlify(signature);
84
+ }
85
+
86
+ /**
87
+ * Connects the signer to a new provider
88
+ * Required by AbstractSigner
89
+ */
90
+ connect(provider: JsonRpcProvider): RemoteSigner {
91
+ return new RemoteSigner(this.wallet, provider);
92
+ }
93
+ }
@@ -1,6 +1,6 @@
1
1
  import { auth } from '@blocklet/sdk/lib/middlewares';
2
2
  import { getVerifyData, verify } from '@blocklet/sdk/lib/util/verify-sign';
3
- import getWallet from '@blocklet/sdk/lib/wallet';
3
+ import { getWallet } from '@blocklet/sdk/lib/wallet';
4
4
  import type { NextFunction, Request, Response } from 'express';
5
5
  import type { Model } from 'sequelize';
6
6
 
@@ -1043,22 +1043,19 @@ export async function getSubscriptionStakeAmountSetup(
1043
1043
  logger.info('getSubscriptionStakeAmountSetup failed, no inputs', { txHash, info });
1044
1044
  return null;
1045
1045
  }
1046
- const amountRes: { [key: string]: BN } = {};
1046
+ const amountRes: { [key: string]: string } = {};
1047
1047
  // calculate stake amount for each address
1048
1048
  inputs.forEach((input: any) => {
1049
1049
  const { tokens } = input;
1050
1050
  tokens.forEach((token: any) => {
1051
1051
  const { address, value } = token;
1052
1052
  if (amountRes[address]) {
1053
- amountRes[address] = amountRes[address].add(new BN(value));
1053
+ amountRes[address] = new BN(amountRes[address]).add(new BN(value)).toString();
1054
1054
  } else {
1055
- amountRes[address] = new BN(value);
1055
+ amountRes[address] = new BN(value).toString();
1056
1056
  }
1057
1057
  });
1058
1058
  });
1059
- Object.keys(amountRes).forEach((address) => {
1060
- amountRes[address] = amountRes[address].toString();
1061
- });
1062
1059
  logger.info('get subscription stake amount setup success', { txHash, amountRes });
1063
1060
  return amountRes;
1064
1061
  }
@@ -1489,7 +1486,7 @@ export async function slashOverdraftProtectionStake(subscription: Subscription,
1489
1486
  // @ts-ignore
1490
1487
  const { buffer } = await client.encodeSlashStakeTx({ tx: signed });
1491
1488
  // @ts-ignore
1492
- const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, getGasPayerExtra(buffer));
1489
+ const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, await getGasPayerExtra(buffer));
1493
1490
 
1494
1491
  await paymentIntent.update({
1495
1492
  status: 'succeeded',
@@ -2,7 +2,7 @@ import crypto from 'crypto';
2
2
  import { Readable } from 'stream';
3
3
  import { buffer } from 'node:stream/consumers';
4
4
  import { getUrl } from '@blocklet/sdk/lib/component';
5
- import env from '@blocklet/sdk/lib/env';
5
+ import { env } from '@blocklet/sdk/lib/env';
6
6
  import { getWalletDid } from '@blocklet/sdk/lib/did';
7
7
  import { toStakeAddress } from '@arcblock/did-util';
8
8
  import { customAlphabet } from 'nanoid';
@@ -603,3 +603,20 @@ export function formatNumber(
603
603
  const [left, right] = result.split('.');
604
604
  return right ? [left, trimEnd(right, '0')].filter(Boolean).join('.') : left;
605
605
  }
606
+
607
+ export function formatLinkWithLocale(url: string, locale?: string) {
608
+ if (!locale || !url) {
609
+ return url;
610
+ }
611
+ try {
612
+ const urlObj = new URL(url);
613
+ urlObj.searchParams.set('locale', locale);
614
+ return urlObj.toString();
615
+ } catch (error) {
616
+ if (/[?&]locale=[^&]*/.test(url)) {
617
+ return url.replace(/([?&])locale=[^&]*/, `$1locale=${locale}`);
618
+ }
619
+ const separator = url.includes('?') ? '&' : '?';
620
+ return `${url}${separator}locale=${locale}`;
621
+ }
622
+ }
@@ -94,7 +94,8 @@ export class DidnamesAdapter implements VendorAdapter {
94
94
  });
95
95
 
96
96
  // Generate bindDomainCap for this domain
97
- const bindDomainCap = this.generateBindCap({
97
+ // eslint-disable-next-line no-await-in-loop
98
+ const bindDomainCap = await this.generateBindCap({
98
99
  domain,
99
100
  checkoutSessionId: orderData.checkoutSessionId,
100
101
  });
@@ -115,7 +116,8 @@ export class DidnamesAdapter implements VendorAdapter {
115
116
  };
116
117
 
117
118
  try {
118
- const { headers, body } = VendorAuth.signRequestWithHeaders(updatedOrderData);
119
+ // eslint-disable-next-line no-await-in-loop
120
+ const { headers, body } = await VendorAuth.signRequestWithHeaders(updatedOrderData);
119
121
 
120
122
  // eslint-disable-next-line no-await-in-loop
121
123
  const response = await fetch(url, {
@@ -198,7 +200,13 @@ export class DidnamesAdapter implements VendorAdapter {
198
200
  /**
199
201
  * Generate bindDomainCap (binding capability) for domain authorization
200
202
  */
201
- private generateBindCap({ domain, checkoutSessionId }: { domain: string; checkoutSessionId: string }): any {
203
+ private async generateBindCap({
204
+ domain,
205
+ checkoutSessionId,
206
+ }: {
207
+ domain: string;
208
+ checkoutSessionId: string;
209
+ }): Promise<any> {
202
210
  const now = Math.floor(Date.now() / 1000);
203
211
  const expireInMinutes = 30;
204
212
 
@@ -210,7 +218,7 @@ export class DidnamesAdapter implements VendorAdapter {
210
218
  nonce: uuidV4(),
211
219
  };
212
220
 
213
- const signature = toBase58(wallet.sign(stableStringify(cap) || ''));
221
+ const signature = toBase58(await wallet.sign(stableStringify(cap) || ''));
214
222
 
215
223
  return {
216
224
  cap,
@@ -345,7 +353,7 @@ export class DidnamesAdapter implements VendorAdapter {
345
353
  customParams: params.customParams,
346
354
  };
347
355
 
348
- const { headers, body } = VendorAuth.signRequestWithHeaders(returnRequest);
356
+ const { headers, body } = await VendorAuth.signRequestWithHeaders(returnRequest);
349
357
  const url = formatVendorUrl(vendorConfig, '/api/vendor/return');
350
358
  logger.info('submitting domain return to DID Names', {
351
359
  url,
@@ -449,7 +457,7 @@ export class DidnamesAdapter implements VendorAdapter {
449
457
  async getOrder(vendor: ProductVendor, orderId: string): Promise<any> {
450
458
  const url = formatVendorUrl(vendor, `/api/vendor/orders/${orderId}`);
451
459
 
452
- const { headers } = VendorAuth.signRequestWithHeaders({});
460
+ const { headers } = await VendorAuth.signRequestWithHeaders({});
453
461
  const response = await fetch(url, { method: 'GET', headers });
454
462
 
455
463
  if (!response.ok) {
@@ -474,7 +482,7 @@ export class DidnamesAdapter implements VendorAdapter {
474
482
  orderId,
475
483
  });
476
484
 
477
- const { headers } = VendorAuth.signRequestWithHeaders({});
485
+ const { headers } = await VendorAuth.signRequestWithHeaders({});
478
486
  const response = await fetch(url, { method: 'GET', headers });
479
487
 
480
488
  logger.info('didnames order status response', {
@@ -499,9 +507,9 @@ export class DidnamesAdapter implements VendorAdapter {
499
507
  return data;
500
508
  }
501
509
 
502
- connectTest(): Promise<any> {
510
+ async connectTest(): Promise<any> {
503
511
  const url = formatVendorUrl(this.vendorConfig!, '/api/vendor/health');
504
- const { headers } = VendorAuth.signRequestWithHeaders({});
512
+ const { headers } = await VendorAuth.signRequestWithHeaders({});
505
513
  return fetch(url, { headers }).then((res) => res.json());
506
514
  }
507
515
  }
@@ -17,8 +17,13 @@ import {
17
17
  } from './types';
18
18
  import { formatVendorUrl } from './util';
19
19
 
20
- const doRequestVendorData = (vendor: ProductVendor, orderId: string, url: string, options: { shortUrl: boolean }) => {
21
- const { headers } = VendorAuth.signRequestWithHeaders({});
20
+ const doRequestVendorData = async (
21
+ vendor: ProductVendor,
22
+ orderId: string,
23
+ url: string,
24
+ options: { shortUrl: boolean }
25
+ ) => {
26
+ const { headers } = await VendorAuth.signRequestWithHeaders({});
22
27
  const name = vendor?.name;
23
28
  const key = vendor?.vendor_key;
24
29
  const { shortUrl } = options;
@@ -124,7 +129,7 @@ export class LauncherAdapter implements VendorAdapter {
124
129
  };
125
130
  }
126
131
 
127
- const { headers, body } = VendorAuth.signRequestWithHeaders(orderData);
132
+ const { headers, body } = await VendorAuth.signRequestWithHeaders(orderData);
128
133
  const url = formatVendorUrl(vendorConfig, '/api/vendor/deliveries');
129
134
  const response = await fetch(url, {
130
135
  method: 'POST',
@@ -207,7 +212,7 @@ export class LauncherAdapter implements VendorAdapter {
207
212
  });
208
213
 
209
214
  const vendorConfig = await this.getVendorConfig();
210
- const { headers, body } = VendorAuth.signRequestWithHeaders(params);
215
+ const { headers, body } = await VendorAuth.signRequestWithHeaders(params);
211
216
 
212
217
  const response = await fetch(formatVendorUrl(vendorConfig, '/api/vendor/return'), {
213
218
  method: 'POST',
@@ -268,9 +273,9 @@ export class LauncherAdapter implements VendorAdapter {
268
273
  return doRequestVendorData(vendor, orderId, url, options);
269
274
  }
270
275
 
271
- connectTest(): Promise<any> {
276
+ async connectTest(): Promise<any> {
272
277
  const url = formatVendorUrl(this.vendorConfig!, '/api/vendor/health');
273
- const { headers } = VendorAuth.signRequestWithHeaders({});
278
+ const { headers } = await VendorAuth.signRequestWithHeaders({});
274
279
  return fetch(url, { headers }).then((res) => res.json());
275
280
  }
276
281
  }
@@ -754,7 +754,7 @@ const handleStakeSlash = async (
754
754
  // @ts-ignore
755
755
  const { buffer } = await client.encodeSlashStakeTx({ tx: signed });
756
756
  // @ts-ignore
757
- const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, getGasPayerExtra(buffer));
757
+ const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, await getGasPayerExtra(buffer));
758
758
  logger.info('Stake slashing done', {
759
759
  subscription: subscription.id,
760
760
  amount: slashAmount,
@@ -973,7 +973,7 @@ export const handlePayment = async (job: PaymentJob) => {
973
973
  const txHash = await client.sendTransferV2Tx(
974
974
  // @ts-ignore
975
975
  { tx: signed, wallet, delegator: result.delegator },
976
- getGasPayerExtra(buffer)
976
+ await getGasPayerExtra(buffer)
977
977
  );
978
978
  logger.info('PaymentIntent txHash', { txHash });
979
979
  logger.info('PaymentIntent capture done', { id: paymentIntent.id, txHash });
@@ -82,7 +82,7 @@ async function processArcblockPayout(payout: Payout, paymentMethod: PaymentMetho
82
82
  // @ts-ignore
83
83
  const { buffer } = await client.encodeTransferV2Tx({ tx: signed });
84
84
  // @ts-ignore
85
- const txHash = await client.sendTransferV2Tx({ tx: signed, wallet }, getGasPayerExtra(buffer));
85
+ const txHash = await client.sendTransferV2Tx({ tx: signed, wallet }, await getGasPayerExtra(buffer));
86
86
 
87
87
  logger.info('Payout completed', { id: payout.id, txHash });
88
88
 
@@ -167,7 +167,7 @@ const handleRefundJob = async (
167
167
  // @ts-ignore
168
168
  const { buffer } = await client.encodeTransferV2Tx({ tx: signed });
169
169
  // @ts-ignore
170
- const txHash = await client.sendTransferV2Tx({ tx: signed, wallet }, getGasPayerExtra(buffer));
170
+ const txHash = await client.sendTransferV2Tx({ tx: signed, wallet }, await getGasPayerExtra(buffer));
171
171
 
172
172
  logger.info('refund transfer done', { id: refund.id, txHash });
173
173
  await refund.update({
@@ -366,7 +366,7 @@ const handleStakeReturnJob = async (
366
366
  // @ts-ignore
367
367
  const { buffer } = await client.encodeReturnStakeTx({ tx: signed });
368
368
  // @ts-ignore
369
- const txHash = await client.sendReturnStakeTx({ tx: signed, wallet }, getGasPayerExtra(buffer));
369
+ const txHash = await client.sendReturnStakeTx({ tx: signed, wallet }, await getGasPayerExtra(buffer));
370
370
  logger.info('stake return done', { id: refund.id, txHash });
371
371
  await refund.update({
372
372
  status: 'succeeded',
@@ -743,7 +743,7 @@ export const handleStakeSlashAfterCancel = async (subscription: Subscription, fo
743
743
  // @ts-ignore
744
744
  const { buffer } = await client.encodeSlashStakeTx({ tx: signed });
745
745
  // @ts-ignore
746
- const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, getGasPayerExtra(buffer));
746
+ const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, await getGasPayerExtra(buffer));
747
747
  logger.info('Stake slashing done', {
748
748
  subscription: subscription.id,
749
749
  amount: invoice.amount_remaining,
@@ -108,7 +108,7 @@ export const doHandleUsageRecord = async (job: UsageRecordJob) => {
108
108
  rawQuantity,
109
109
  quantity,
110
110
 
111
- unitAmount: fromUnitToToken(unitAmount, currency.decimal),
111
+ unitAmount: fromUnitToToken(unitAmount || '0', currency.decimal),
112
112
  totalAmount: fromUnitToToken(totalAmount.toString(), currency.decimal),
113
113
  threshold: fromUnitToToken(threshold.toString(), currency.decimal),
114
114
  });
@@ -103,7 +103,7 @@ export const handleVendorStatusCheck = async (job: VendorStatusCheckJob) => {
103
103
  '/api/vendor/status',
104
104
  vendor.order_id
105
105
  );
106
- const { headers } = VendorAuth.signRequestWithHeaders({});
106
+ const { headers } = await VendorAuth.signRequestWithHeaders({});
107
107
 
108
108
  const result = await fetch(serverStatusUrl, { headers });
109
109
  const data = await result.json();
@@ -63,7 +63,7 @@ export const handleWebhook = async (job: WebhookJob) => {
63
63
  headers: {
64
64
  'x-app-id': wallet.address,
65
65
  'x-app-pk': wallet.publicKey,
66
- 'x-component-sig': sign(json),
66
+ 'x-component-sig': await sign(json),
67
67
  'x-component-did': process.env.BLOCKLET_COMPONENT_DID as string,
68
68
  },
69
69
  });
@@ -3,7 +3,7 @@ import Joi from 'joi';
3
3
 
4
4
  import { CustomError } from '@blocklet/error';
5
5
  import { Op } from 'sequelize';
6
- import sessionMiddleware from '@blocklet/sdk/lib/middlewares/session';
6
+ import { sessionMiddleware } from '@blocklet/sdk/lib/middlewares/session';
7
7
  import { BN, fromTokenToUnit, fromUnitToToken } from '@ocap/util';
8
8
  import { trimDecimals } from '../libs/math-utils';
9
9
  import {
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable consistent-return */
2
2
  import { isValid } from '@arcblock/did';
3
3
  import { getUrl } from '@blocklet/sdk/lib/component';
4
- import sessionMiddleware from '@blocklet/sdk/lib/middlewares/session';
4
+ import { sessionMiddleware } from '@blocklet/sdk/lib/middlewares/session';
5
5
  import { BN, fromUnitToToken } from '@ocap/util';
6
6
  import { NextFunction, Request, Response, Router } from 'express';
7
7
  import Joi from 'joi';
@@ -390,7 +390,7 @@ export async function calculateAndUpdateAmount(
390
390
  amount,
391
391
  });
392
392
 
393
- if (checkoutSession.mode === 'payment' && amount.total < 0) {
393
+ if (checkoutSession.mode === 'payment' && new BN(amount.total || '0').lt(new BN('0'))) {
394
394
  throw new Error('Payment amount should be greater or equal to 0');
395
395
  }
396
396
 
@@ -2149,10 +2149,6 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
2149
2149
  return res.status(400).json({ error: 'Payment method not found' });
2150
2150
  }
2151
2151
 
2152
- if (paymentMethod.type !== 'arcblock') {
2153
- return res.status(400).json({ error: 'Payment method not supported for fast checkout' });
2154
- }
2155
-
2156
2152
  // Validate checkout session ownership if it was created for a specific customer
2157
2153
  if (checkoutSession.customer_id && checkoutSession.metadata?.createdBy) {
2158
2154
  const createdByDid = checkoutSession.metadata.createdBy;
@@ -2355,6 +2351,8 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
2355
2351
  };
2356
2352
  canFastPay = true;
2357
2353
  }
2354
+ } else if (paymentMethod.type !== 'arcblock') {
2355
+ return res.status(400).json({ error: 'Payment method not supported for fast checkout' });
2358
2356
  }
2359
2357
 
2360
2358
  logger.info('Checkout session submitted successfully', {
@@ -0,0 +1,148 @@
1
+ import { executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
2
+ import type { CallbackArgs } from '../../libs/auth';
3
+ import { getTxMetadata } from '../../libs/util';
4
+ import { type TLineItemExpanded } from '../../store/models';
5
+ import {
6
+ ensurePayerChangeContext,
7
+ executeOcapTransactions,
8
+ getAuthPrincipalClaim,
9
+ getDelegationTxClaim,
10
+ } from './shared';
11
+ import { EVM_CHAIN_TYPES } from '../../libs/constants';
12
+
13
+ export default {
14
+ action: 'change-payer',
15
+ authPrincipal: false,
16
+ persistentDynamicClaims: true,
17
+ claims: {
18
+ authPrincipal: async ({ extraParams }: CallbackArgs) => {
19
+ const { paymentMethod } = await ensurePayerChangeContext(extraParams.subscriptionId);
20
+ return getAuthPrincipalClaim(paymentMethod, 'continue');
21
+ },
22
+ },
23
+ onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
24
+ const { subscriptionId } = extraParams;
25
+ const { subscription, paymentMethod, paymentCurrency, payerAddress } =
26
+ await ensurePayerChangeContext(subscriptionId);
27
+
28
+ if (userDid === payerAddress) {
29
+ throw new Error('The current payer is the same as the new payer, please use another account to change payer');
30
+ }
31
+ const claimsList: any[] = [];
32
+ // @ts-ignore
33
+ const items = subscription!.items as TLineItemExpanded[];
34
+ const trialing = true;
35
+ const billingThreshold = Number(subscription.billing_thresholds?.amount_gte || 0);
36
+
37
+ if (paymentMethod.type === 'arcblock') {
38
+ claimsList.push({
39
+ signature: await getDelegationTxClaim({
40
+ mode: 'delegation',
41
+ userDid,
42
+ userPk,
43
+ nonce: subscription.id,
44
+ data: getTxMetadata({ subscriptionId: subscription.id }),
45
+ paymentCurrency,
46
+ paymentMethod,
47
+ trialing,
48
+ billingThreshold,
49
+ items,
50
+ requiredStake: false,
51
+ }),
52
+ });
53
+ return claimsList;
54
+ }
55
+
56
+ if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
57
+ if (!paymentCurrency.contract) {
58
+ throw new Error(`Payment currency ${paymentMethod.type}:${paymentCurrency.id} does not support subscription`);
59
+ }
60
+
61
+ claimsList.push({
62
+ signature: await getDelegationTxClaim({
63
+ mode: 'subscription',
64
+ userDid,
65
+ userPk,
66
+ nonce: `change-payer-${subscription!.id}`,
67
+ data: getTxMetadata({ subscriptionId: subscription!.id }),
68
+ paymentCurrency,
69
+ paymentMethod,
70
+ trialing,
71
+ billingThreshold,
72
+ items,
73
+ }),
74
+ });
75
+
76
+ return claimsList;
77
+ }
78
+
79
+ throw new Error(`ChangePayer: Payment method ${paymentMethod.type} not supported`);
80
+ },
81
+
82
+ onAuth: async ({ request, userDid, userPk, claims, extraParams, step }: CallbackArgs) => {
83
+ const { subscriptionId } = extraParams;
84
+ const { subscription, paymentMethod, paymentCurrency } = await ensurePayerChangeContext(subscriptionId);
85
+
86
+ const result = request?.context?.store?.result || [];
87
+ result.push({
88
+ step,
89
+ claim: claims?.[0],
90
+ stepRequest: {
91
+ headers: request?.headers,
92
+ },
93
+ });
94
+ const claimsList = result.map((x: any) => x.claim);
95
+
96
+ const afterTxExecution = async (paymentDetails: any) => {
97
+ await subscription?.update({
98
+ payment_settings: {
99
+ payment_method_types: [paymentMethod.type],
100
+ payment_method_options: {
101
+ [paymentMethod.type]: { payer: userDid },
102
+ },
103
+ },
104
+ payment_details: {
105
+ ...subscription.payment_details,
106
+ [paymentMethod.type]: {
107
+ ...(subscription.payment_details?.[paymentMethod.type as keyof typeof subscription.payment_details] || {}),
108
+ type: 'delegate',
109
+ payer: userDid,
110
+ tx_hash: paymentDetails.tx_hash,
111
+ },
112
+ },
113
+ });
114
+ };
115
+
116
+ if (paymentMethod.type === 'arcblock') {
117
+ const requestArray = result
118
+ .map((item: { stepRequest?: Request }) => item.stepRequest)
119
+ .filter(Boolean) as Request[];
120
+ const requestSource = requestArray.length > 0 ? requestArray : request;
121
+
122
+ const paymentDetails = await executeOcapTransactions(
123
+ userDid,
124
+ userPk,
125
+ claimsList,
126
+ paymentMethod,
127
+ requestSource,
128
+ subscription?.id,
129
+ paymentCurrency.contract
130
+ );
131
+
132
+ await afterTxExecution(paymentDetails);
133
+ return { hash: paymentDetails.tx_hash };
134
+ }
135
+
136
+ if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
137
+ const paymentDetails = await executeEvmTransaction('approve', userDid, claimsList, paymentMethod);
138
+ waitForEvmTxConfirm(paymentMethod.getEvmClient(), +paymentDetails.block_height, paymentMethod.confirmation.block)
139
+ .then(async () => {
140
+ await afterTxExecution(paymentDetails);
141
+ })
142
+ .catch(console.error);
143
+ return { hash: paymentDetails.tx_hash };
144
+ }
145
+
146
+ throw new Error(`ChangePayer: Payment method ${paymentMethod.type} not supported`);
147
+ },
148
+ };
@@ -136,7 +136,7 @@ export default {
136
136
  const txHash = await client.sendTransferV3Tx(
137
137
  // @ts-ignore
138
138
  { tx, wallet: fromAddress(userDid) },
139
- getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
139
+ await getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
140
140
  );
141
141
  await afterTxExecution({ tx_hash: txHash, payer: userDid, type: 'transfer' });
142
142
 
@@ -155,7 +155,7 @@ export default {
155
155
  const txHash = await client.sendTransferV3Tx(
156
156
  // @ts-ignore
157
157
  { tx, wallet: fromAddress(userDid) },
158
- getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
158
+ await getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
159
159
  );
160
160
 
161
161
  await afterTxExecution({
@@ -115,7 +115,7 @@ export default {
115
115
  const txHash = await client.sendTransferV3Tx(
116
116
  // @ts-ignore
117
117
  { tx, wallet: fromAddress(userDid) },
118
- getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
118
+ await getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
119
119
  );
120
120
 
121
121
  await paymentIntent.update({
@@ -149,7 +149,7 @@ export default {
149
149
  const txHash = await client.sendTransferV3Tx(
150
150
  // @ts-ignore
151
151
  { tx, wallet: fromAddress(userDid) },
152
- getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
152
+ await getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
153
153
  );
154
154
 
155
155
  await afterTxExecution({
@@ -128,7 +128,7 @@ export default {
128
128
  const txHash = await client.sendTransferV3Tx(
129
129
  // @ts-ignore
130
130
  { tx, wallet: fromAddress(userDid) },
131
- getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
131
+ await getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
132
132
  );
133
133
 
134
134
  logger.info('Recharge successful', {