payment-kit 1.18.15 → 1.18.16

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/api/src/index.ts CHANGED
@@ -39,6 +39,7 @@ import setupHandlers from './routes/connect/setup';
39
39
  import subscribeHandlers from './routes/connect/subscribe';
40
40
  import delegationHandlers from './routes/connect/delegation';
41
41
  import overdraftProtectionHandlers from './routes/connect/overdraft-protection';
42
+ import rechargeAccountHandlers from './routes/connect/recharge-account';
42
43
  import { initialize } from './store/models';
43
44
  import { sequelize } from './store/sequelize';
44
45
  import { initUserHandler } from './integrations/blocklet/user';
@@ -74,6 +75,7 @@ handlers.attach(Object.assign({ app: router }, subscribeHandlers));
74
75
  handlers.attach(Object.assign({ app: router }, changePaymentHandlers));
75
76
  handlers.attach(Object.assign({ app: router }, changePlanHandlers));
76
77
  handlers.attach(Object.assign({ app: router }, rechargeHandlers));
78
+ handlers.attach(Object.assign({ app: router }, rechargeAccountHandlers));
77
79
  handlers.attach(Object.assign({ app: router }, delegationHandlers));
78
80
  handlers.attach(Object.assign({ app: router }, overdraftProtectionHandlers));
79
81
  router.use('/api', routes);
@@ -363,7 +363,7 @@ export async function getStakingInvoices(subscription: Subscription): Promise<In
363
363
 
364
364
  type BaseInvoiceProps = {
365
365
  customer: Customer;
366
- subscription?: Subscription;
366
+ subscription?: Subscription | null;
367
367
  currency_id: string;
368
368
  livemode: boolean;
369
369
  period_start: number;
@@ -660,17 +660,19 @@ export async function ensureRechargeInvoice(
660
660
  currency_id: string;
661
661
  metadata?: any;
662
662
  payment_settings?: any;
663
+ livemode?: boolean;
663
664
  },
664
- subscription: Subscription,
665
+ subscription: Subscription | null,
665
666
  paymentMethod: PaymentMethod,
666
667
  customer: Customer
667
668
  ) {
668
669
  try {
670
+ const { livemode = true } = invoiceProps;
669
671
  const { invoice } = await createInvoiceWithItems({
670
672
  customer,
671
673
  subscription,
672
674
  currency_id: invoiceProps.currency_id,
673
- livemode: subscription.livemode,
675
+ livemode: subscription?.livemode ?? livemode,
674
676
  period_start: dayjs().unix(),
675
677
  period_end: dayjs().unix(),
676
678
  status: 'paid',
@@ -99,6 +99,9 @@ export default {
99
99
  result.push({
100
100
  step,
101
101
  claim: claims?.[0],
102
+ stepRequest: {
103
+ headers: request?.headers,
104
+ },
102
105
  });
103
106
 
104
107
  // 判断是否为最后一步
@@ -164,12 +167,17 @@ export default {
164
167
 
165
168
  if (paymentMethod.type === 'arcblock') {
166
169
  await prepareTxExecution();
170
+ const requestArray = result
171
+ .map((item: { stepRequest?: Request }) => item.stepRequest)
172
+ .filter(Boolean) as Request[];
173
+ const requestSource = requestArray.length > 0 ? requestArray : request;
174
+
167
175
  const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
168
176
  userDid,
169
177
  userPk,
170
178
  claimsList,
171
179
  paymentMethod,
172
- request,
180
+ requestSource,
173
181
  subscription?.id,
174
182
  paymentCurrency.contract
175
183
  );
@@ -113,6 +113,9 @@ export default {
113
113
  result.push({
114
114
  step,
115
115
  claim: claims?.[0],
116
+ stepRequest: {
117
+ headers: request?.headers,
118
+ },
116
119
  });
117
120
 
118
121
  // 判断是否为最后一步
@@ -171,12 +174,17 @@ export default {
171
174
  if (paymentMethod.type === 'arcblock') {
172
175
  await prepareTxExecution();
173
176
 
177
+ const requestArray = result
178
+ .map((item: { stepRequest?: Request }) => item.stepRequest)
179
+ .filter(Boolean) as Request[];
180
+ const requestSource = requestArray.length > 0 ? requestArray : request;
181
+
174
182
  const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
175
183
  userDid,
176
184
  userPk,
177
185
  claimsList,
178
186
  paymentMethod,
179
- request,
187
+ requestSource,
180
188
  subscription?.id,
181
189
  paymentCurrency?.contract
182
190
  );
@@ -23,16 +23,18 @@ export default {
23
23
  authPrincipal: async ({ extraParams }: CallbackArgs) => {
24
24
  const { paymentMethod } = await ensureSubscriptionForCollectBatch(
25
25
  extraParams.subscriptionId,
26
- extraParams.currencyId
26
+ extraParams.currencyId,
27
+ extraParams.customerId
27
28
  );
28
29
  return getAuthPrincipalClaim(paymentMethod, 'pay');
29
30
  },
30
31
  },
31
32
  onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
32
- const { subscriptionId, currencyId } = extraParams;
33
+ const { subscriptionId, currencyId, customerId } = extraParams;
33
34
  const { amount, invoices, paymentCurrency, paymentMethod } = await ensureSubscriptionForCollectBatch(
34
35
  subscriptionId,
35
- currencyId
36
+ currencyId,
37
+ customerId
36
38
  );
37
39
 
38
40
  if (paymentMethod.type === 'arcblock') {
@@ -83,8 +85,8 @@ export default {
83
85
  throw new Error(`Payment method ${paymentMethod.type} not supported`);
84
86
  },
85
87
  onAuth: async ({ request, userDid, claims, extraParams }: CallbackArgs) => {
86
- const { subscriptionId, currencyId } = extraParams;
87
- const { invoices, paymentMethod } = await ensureSubscriptionForCollectBatch(subscriptionId, currencyId);
88
+ const { subscriptionId, currencyId, customerId } = extraParams;
89
+ const { invoices, paymentMethod } = await ensureSubscriptionForCollectBatch(subscriptionId, currencyId, customerId);
88
90
 
89
91
  const afterTxExecution = async (paymentDetails: any) => {
90
92
  const paymentIntents = await PaymentIntent.findAll({
@@ -0,0 +1,124 @@
1
+ import type { Transaction } from '@ocap/client';
2
+ import { fromAddress } from '@ocap/wallet';
3
+ import { fromTokenToUnit } from '@ocap/util';
4
+ import type { CallbackArgs } from '../../libs/auth';
5
+ import { getGasPayerExtra } from '../../libs/payment';
6
+ import { getTxMetadata } from '../../libs/util';
7
+ import { ensureAccountRecharge, getAuthPrincipalClaim } from './shared';
8
+ import logger from '../../libs/logger';
9
+ import { ensureRechargeInvoice } from '../../libs/invoice';
10
+
11
+ export default {
12
+ action: 'recharge-account',
13
+ authPrincipal: false,
14
+ claims: {
15
+ authPrincipal: async ({ extraParams }: CallbackArgs) => {
16
+ const { paymentMethod } = await ensureAccountRecharge(extraParams.customerDid, extraParams.currencyId);
17
+ return getAuthPrincipalClaim(paymentMethod, 'recharge-account');
18
+ },
19
+ },
20
+ onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
21
+ const { customerDid, currencyId } = extraParams;
22
+ let { amount } = extraParams;
23
+ const { paymentMethod, paymentCurrency, customer } = await ensureAccountRecharge(customerDid, currencyId);
24
+ amount = fromTokenToUnit(amount, paymentCurrency.decimal).toString();
25
+ if (paymentMethod.type === 'arcblock') {
26
+ const tokens = [{ address: paymentCurrency.contract as string, value: amount }];
27
+ // @ts-ignore
28
+ const itx: TransferV3Tx = {
29
+ outputs: [{ owner: customerDid, tokens, assets: [] }],
30
+ data: getTxMetadata({
31
+ rechargeCustomerId: customer.id,
32
+ customerDid,
33
+ currencyId: paymentCurrency.id,
34
+ amount,
35
+ action: 'recharge-account',
36
+ }),
37
+ };
38
+
39
+ const claims: { [key: string]: object } = {
40
+ prepareTx: {
41
+ type: 'TransferV3Tx',
42
+ description: `Recharge account for ${customerDid}`,
43
+ partialTx: { from: userDid, pk: userPk, itx },
44
+ requirement: { tokens },
45
+ chainInfo: {
46
+ host: paymentMethod.settings?.arcblock?.api_host as string,
47
+ id: paymentMethod.settings?.arcblock?.chain_id as string,
48
+ },
49
+ },
50
+ };
51
+
52
+ return claims;
53
+ }
54
+
55
+ throw new Error(`Payment method ${paymentMethod.type} not supported`);
56
+ },
57
+ onAuth: async ({ request, userDid, claims, extraParams }: CallbackArgs) => {
58
+ const { customerDid, currencyId } = extraParams;
59
+ const { paymentMethod, paymentCurrency, customer } = await ensureAccountRecharge(customerDid, currencyId);
60
+ let { amount } = extraParams;
61
+ amount = fromTokenToUnit(amount, paymentCurrency.decimal).toString();
62
+
63
+ const afterTxExecution = async (paymentDetails: any) => {
64
+ await ensureRechargeInvoice(
65
+ {
66
+ total: amount,
67
+ description: 'Add funds for account',
68
+ currency_id: paymentCurrency.id,
69
+ metadata: {
70
+ payment_details: {
71
+ [paymentMethod.type]: paymentDetails,
72
+ receiverAddress: customerDid,
73
+ },
74
+ },
75
+ livemode: paymentCurrency?.livemode,
76
+ payment_settings: {
77
+ payment_method_types: [paymentMethod.type],
78
+ payment_method_options: {
79
+ [paymentMethod.type]: {
80
+ payer: userDid,
81
+ },
82
+ },
83
+ },
84
+ },
85
+ null,
86
+ paymentMethod,
87
+ customer!
88
+ );
89
+ };
90
+ if (paymentMethod.type === 'arcblock') {
91
+ try {
92
+ const client = paymentMethod.getOcapClient();
93
+ const claim = claims.find((x) => x.type === 'prepareTx');
94
+
95
+ const tx: Partial<Transaction> = client.decodeTx(claim.finalTx);
96
+ if (claim.delegator && claim.from) {
97
+ tx.delegator = claim.delegator;
98
+ tx.from = claim.from;
99
+ }
100
+
101
+ // @ts-ignore
102
+ const { buffer } = await client.encodeTransferV3Tx({ tx });
103
+ const txHash = await client.sendTransferV3Tx(
104
+ // @ts-ignore
105
+ { tx, wallet: fromAddress(userDid) },
106
+ getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
107
+ );
108
+
109
+ await afterTxExecution({
110
+ tx_hash: txHash,
111
+ payer: userDid,
112
+ type: 'transfer',
113
+ });
114
+ return { hash: txHash };
115
+ } catch (err) {
116
+ console.error(err);
117
+ logger.error('Failed to finalize recharge on arcblock', { receiverAddress: customerDid, error: err });
118
+ throw err;
119
+ }
120
+ }
121
+
122
+ throw new Error(`Payment method ${paymentMethod.type} not supported`);
123
+ },
124
+ };
@@ -132,6 +132,9 @@ export default {
132
132
  result.push({
133
133
  step,
134
134
  claim: claims?.[0],
135
+ stepRequest: {
136
+ headers: request?.headers,
137
+ },
135
138
  });
136
139
 
137
140
  // 判断是否为最后一步
@@ -199,13 +202,17 @@ export default {
199
202
  if (paymentMethod.type === 'arcblock') {
200
203
  try {
201
204
  await prepareTxExecution();
205
+ const requestArray = result
206
+ .map((item: { stepRequest?: Request }) => item.stepRequest)
207
+ .filter(Boolean) as Request[];
208
+ const requestSource = requestArray.length > 0 ? requestArray : request;
202
209
 
203
210
  const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
204
211
  userDid,
205
212
  userPk,
206
213
  claimsList,
207
214
  paymentMethod,
208
- request,
215
+ requestSource,
209
216
  subscription?.id,
210
217
  paymentCurrency?.contract
211
218
  );
@@ -6,7 +6,7 @@ import type { Transaction } from '@ocap/client';
6
6
  import { BN, fromTokenToUnit, toBase58 } from '@ocap/util';
7
7
  import { fromPublicKey } from '@ocap/wallet';
8
8
  import type { Request } from 'express';
9
- import isEmpty from 'lodash/isEmpty';
9
+ import { isEmpty } from 'lodash';
10
10
 
11
11
  import { estimateMaxGasForTx, hasStakedForGas } from '../../integrations/arcblock/stake';
12
12
  import { encodeApproveItx } from '../../integrations/ethereum/token';
@@ -481,6 +481,25 @@ export async function ensureSubscriptionRecharge(subscriptionId: string) {
481
481
  };
482
482
  }
483
483
 
484
+ export async function ensureAccountRecharge(customerId: string, currencyId: string) {
485
+ const customer = await Customer.findByPkOrDid(customerId);
486
+ if (!customer) {
487
+ throw new Error(`Customer ${customerId} not found`);
488
+ }
489
+ const paymentCurrency = await PaymentCurrency.findByPk(currencyId);
490
+ if (!paymentCurrency) {
491
+ throw new Error(`Currency ${currencyId} not found`);
492
+ }
493
+ const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
494
+ if (!paymentMethod) {
495
+ throw new Error(`Payment method ${paymentCurrency.payment_method_id} not found`);
496
+ }
497
+ return {
498
+ paymentCurrency: paymentCurrency as PaymentCurrency,
499
+ paymentMethod: paymentMethod as PaymentMethod,
500
+ customer,
501
+ };
502
+ }
484
503
  export function getAuthPrincipalClaim(method: PaymentMethod, action: string) {
485
504
  let chainInfo = { type: 'none', id: 'none', host: 'none' };
486
505
  if (method.type === 'arcblock') {
@@ -934,10 +953,16 @@ export async function ensureChangePaymentContext(subscriptionId: string) {
934
953
  };
935
954
  }
936
955
 
937
- export async function ensureSubscriptionForCollectBatch(subscriptionId: string, currencyId: string) {
938
- const subscription = await Subscription.findByPk(subscriptionId);
939
- if (!subscription) {
940
- throw new Error(`Subscription ${subscriptionId} not found when prepare batch collect`);
956
+ export async function ensureSubscriptionForCollectBatch(
957
+ subscriptionId?: string,
958
+ currencyId?: string,
959
+ customerId?: string
960
+ ) {
961
+ if (!currencyId) {
962
+ throw new Error('Currency ID must be provided');
963
+ }
964
+ if (!subscriptionId && !customerId) {
965
+ throw new Error('Either subscriptionId or customerId must be provided');
941
966
  }
942
967
 
943
968
  const paymentCurrency = await PaymentCurrency.findByPk(currencyId);
@@ -945,16 +970,36 @@ export async function ensureSubscriptionForCollectBatch(subscriptionId: string,
945
970
  throw new Error(`PaymentCurrency ${currencyId} not found when prepare batch collect`);
946
971
  }
947
972
 
973
+ let subscription;
974
+ let searchCustomerId = customerId;
975
+
976
+ if (subscriptionId) {
977
+ subscription = await Subscription.findByPk(subscriptionId);
978
+ if (!subscription) {
979
+ throw new Error(`Subscription ${subscriptionId} not found when prepare batch collect`);
980
+ }
981
+ searchCustomerId = subscription.customer_id;
982
+ } else if (customerId) {
983
+ const customer = await Customer.findByPkOrDid(customerId);
984
+ if (!customer) {
985
+ throw new Error(`Customer ${customerId} not found when prepare batch collect`);
986
+ }
987
+ searchCustomerId = customer.id;
988
+ }
989
+
948
990
  const [summary, detail] = await Invoice.getUncollectibleAmount({
949
- subscriptionId: subscription.id,
950
- customerId: subscription.customer_id,
991
+ ...(subscriptionId ? { subscriptionId } : {}),
992
+ customerId: searchCustomerId,
951
993
  currencyId,
952
994
  });
953
995
  if (isEmpty(summary) || !summary[currencyId] || summary[currencyId] === '0') {
954
- throw new Error(`No uncollectible invoice found for subscription ${subscriptionId} to batch collect`);
996
+ throw new Error('No uncollectible invoice found to batch collect');
955
997
  }
956
998
 
957
999
  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
1000
+ if (!paymentMethod) {
1001
+ throw new Error(`Payment method not found for currency ${currencyId}`);
1002
+ }
958
1003
 
959
1004
  return {
960
1005
  subscription,
@@ -1005,7 +1050,7 @@ export async function executeOcapTransactions(
1005
1050
  userPk: string,
1006
1051
  claims: any[],
1007
1052
  paymentMethod: PaymentMethod,
1008
- request: Request,
1053
+ requestSource: Request | Request[] | any[],
1009
1054
  subscriptionId?: string,
1010
1055
  paymentCurrencyContract?: string,
1011
1056
  nonce?: string
@@ -1022,47 +1067,64 @@ export async function executeOcapTransactions(
1022
1067
  const stakingAmount =
1023
1068
  staking?.requirement?.tokens?.find((x: any) => x.address === paymentCurrencyContract)?.value || '0';
1024
1069
 
1025
- try {
1026
- const [delegationTxHash, stakingTxHash] = await Promise.all(
1027
- transactions.map(async ([claim, type]) => {
1028
- if (!claim) {
1029
- return '';
1030
- }
1031
-
1032
- const tx: Partial<Transaction> = client.decodeTx(claim.finalTx || claim.origin);
1033
- if (claim.sig) {
1034
- tx.signature = claim.sig;
1035
- }
1036
-
1037
- // @ts-ignore
1038
- const { buffer } = await client[`encode${type}Tx`]({ tx });
1039
- // @ts-ignore
1040
- const txHash = await client[`send${type}Tx`](
1041
- // @ts-ignore
1042
- { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
1043
- getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
1044
- );
1045
-
1046
- return txHash;
1047
- })
1048
- );
1070
+ try {
1071
+ const getHeaders = (index: number): Record<string, string> => {
1072
+ if (Array.isArray(requestSource)) {
1073
+ if (requestSource.length === 0) {
1074
+ return {};
1075
+ }
1076
+ const headerIndex = index < requestSource.length ? index : 0;
1077
+ const req = requestSource[headerIndex];
1078
+ return req ? client.pickGasPayerHeaders(req) : {};
1079
+ }
1049
1080
 
1050
- return {
1051
- tx_hash: delegationTxHash,
1052
- payer: userDid,
1053
- type: 'delegate',
1054
- staking: {
1055
- tx_hash: stakingTxHash,
1056
- address: await getCustomerStakeAddress(userDid, nonce || subscriptionId || ''),
1057
- },
1058
- stakingAmount,
1059
- };
1060
- } catch (err) {
1061
- logger.error('executeOcapTransactions failed', err);
1062
- throw err;
1063
- }
1064
- }
1081
+ if (requestSource && typeof requestSource === 'object') {
1082
+ return client.pickGasPayerHeaders(requestSource);
1083
+ }
1084
+
1085
+ return {};
1086
+ };
1065
1087
 
1088
+ const [delegationTxHash, stakingTxHash] = await Promise.all(
1089
+ transactions.map(async ([claim, type], index) => {
1090
+ if (!claim) {
1091
+ return '';
1092
+ }
1093
+
1094
+ const tx: Partial<Transaction> = client.decodeTx(claim.finalTx || claim.origin);
1095
+ if (claim.sig) {
1096
+ tx.signature = claim.sig;
1097
+ }
1098
+
1099
+ // @ts-ignore
1100
+ const { buffer } = await client[`encode${type}Tx`]({ tx });
1101
+ const gasPayerHeaders = getHeaders(index);
1102
+ // @ts-ignore
1103
+ const txHash = await client[`send${type}Tx`](
1104
+ // @ts-ignore
1105
+ { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
1106
+ getGasPayerExtra(buffer, gasPayerHeaders)
1107
+ );
1108
+
1109
+ return txHash;
1110
+ })
1111
+ );
1112
+
1113
+ return {
1114
+ tx_hash: delegationTxHash,
1115
+ payer: userDid,
1116
+ type: 'delegate',
1117
+ staking: {
1118
+ tx_hash: stakingTxHash,
1119
+ address: await getCustomerStakeAddress(userDid, nonce || subscriptionId || ''),
1120
+ },
1121
+ stakingAmount,
1122
+ };
1123
+ } catch (err) {
1124
+ logger.error('executeOcapTransactions failed', err);
1125
+ throw err;
1126
+ }
1127
+ }
1066
1128
 
1067
1129
  export async function updateStripeSubscriptionAfterChangePayment(setupIntent: SetupIntent, subscription: Subscription) {
1068
1130
  const { from_method: fromMethodId, to_method: toMethodId } = setupIntent.metadata || {};
@@ -129,6 +129,9 @@ export default {
129
129
  result.push({
130
130
  step,
131
131
  claim: claims?.[0],
132
+ stepRequest: {
133
+ headers: request?.headers,
134
+ },
132
135
  });
133
136
  const staking = result.find((x: any) => x.claim?.type === 'prepareTx' && x.claim?.meta?.purpose === 'staking');
134
137
  const isFinalStep = (paymentMethod.type === 'arcblock' && staking) || paymentMethod.type !== 'arcblock';
@@ -183,12 +186,19 @@ export default {
183
186
  if (invoice) {
184
187
  await invoice.update({ payment_settings: paymentSettings });
185
188
  }
189
+
190
+ const requestArray = result
191
+ .map((item: { stepRequest?: Request }) => item.stepRequest)
192
+ .filter(Boolean) as Request[];
193
+
194
+ const requestSource = requestArray.length > 0 ? requestArray : request;
195
+
186
196
  const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
187
197
  userDid,
188
198
  userPk,
189
199
  claimsList,
190
200
  paymentMethod,
191
- request,
201
+ requestSource,
192
202
  subscription?.id,
193
203
  paymentCurrency?.contract
194
204
  );