payment-kit 1.23.5 → 1.23.7

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.
@@ -359,6 +359,22 @@ export async function getTokenContext(paymentCurrency: TPaymentCurrency) {
359
359
  };
360
360
  }
361
361
 
362
+ /**
363
+ * Get the latest DID address for a customer, handling DID migration.
364
+ */
365
+ export async function getAccountState(paymentCurrency: TPaymentCurrency, did?: string) {
366
+ if (!did) return null;
367
+
368
+ try {
369
+ const { client } = await getTokenContext(paymentCurrency);
370
+ const { state } = await client.getAccountState({ address: did, traceMigration: true });
371
+ return state;
372
+ } catch (error) {
373
+ logger.error('Failed to get account state', { did, error });
374
+ return null;
375
+ }
376
+ }
377
+
362
378
  export async function mintToken({
363
379
  paymentCurrency,
364
380
  amount,
@@ -19,7 +19,7 @@ import {
19
19
  } from '../store/models';
20
20
  import { events } from '../libs/event';
21
21
  import { calculateExpiresAt, createCreditGrant } from '../libs/credit-grant';
22
- import { mintToken, transferTokenFromCustomer } from '../integrations/arcblock/token';
22
+ import { getAccountState, mintToken, transferTokenFromCustomer } from '../integrations/arcblock/token';
23
23
  import logger from '../libs/logger';
24
24
 
25
25
  type CreditGrantJob =
@@ -66,17 +66,19 @@ async function activateGrant(creditGrant: CreditGrant) {
66
66
 
67
67
  // Verify customer
68
68
  const customer = await Customer.findByPk(creditGrant.customer_id);
69
- if (!customer?.did) {
69
+ if (!customer) {
70
70
  throw new Error(`Customer DID not found for credit grant ${creditGrant.id}`);
71
71
  }
72
72
 
73
+ const customerState = await getAccountState(paymentCurrency, customer.did);
74
+
73
75
  // Step 1: Mint token on-chain
74
76
  // TODO: check if already minted
75
77
  const mintAmount = fromUnitToToken(creditGrant.amount, paymentCurrency.decimal);
76
78
  const hash = await mintToken({
77
79
  paymentCurrency,
78
80
  amount: mintAmount,
79
- receiver: customer.did,
81
+ receiver: customerState?.address || customer.did,
80
82
  creditGrantId: creditGrant.id,
81
83
  });
82
84
 
@@ -172,11 +174,12 @@ export async function expireGrant(creditGrant: CreditGrant) {
172
174
 
173
175
  // Transfer remaining tokens to system wallet (same as consumption flow)
174
176
  const chainDetail = creditGrant.chain_detail || {};
177
+ const customerState = await getAccountState(paymentCurrency, customer.did);
175
178
 
176
179
  try {
177
180
  const hash = await transferTokenFromCustomer({
178
181
  paymentCurrency,
179
- customerDid: customer.did,
182
+ customerDid: customerState?.address || customer.did,
180
183
  amount: remainingAmount.toString(),
181
184
  data: {
182
185
  reason: 'credit_expired',
@@ -8,7 +8,7 @@
8
8
  import { BN } from '@ocap/util';
9
9
  import logger from '../libs/logger';
10
10
  import createQueue from '../libs/queue';
11
- import { getCustomerTokenBalance } from '../integrations/arcblock/token';
11
+ import { getAccountState, getCustomerTokenBalance } from '../integrations/arcblock/token';
12
12
  import { CreditGrant, CreditTransaction, Customer, PaymentCurrency } from '../store/models';
13
13
  import { events } from '../libs/event';
14
14
 
@@ -98,21 +98,23 @@ async function handleReconciliation(job: ReconciliationJob) {
98
98
  });
99
99
 
100
100
  try {
101
- const customer = await Customer.findByPk(customerId);
102
- if (!customer) {
103
- logger.warn('Customer not found for reconciliation', { customerId });
104
- return;
105
- }
106
-
107
101
  const currency = await PaymentCurrency.findByPk(currencyId);
108
102
  if (!currency || !currency.isOnChainCredit()) {
109
103
  logger.warn('Currency not found or not on-chain credit', { currencyId });
110
104
  return;
111
105
  }
112
106
 
107
+ const customer = await Customer.findByPk(customerId);
108
+ if (!customer?.did) {
109
+ logger.warn('Customer not found for reconciliation', { customerId });
110
+ return;
111
+ }
112
+ const customerState = await getAccountState(currency, customer.did);
113
+ const customerDid = customerState?.address || customer.did;
114
+
113
115
  // Get balance info (considering pending transfers)
114
116
  const balanceInfo = await getBalanceInfo(customerId, currencyId);
115
- const chainBalance = await getCustomerTokenBalance(customer.did, currency);
117
+ const chainBalance = await getCustomerTokenBalance(customerDid, currency);
116
118
 
117
119
  const difference = new BN(chainBalance).sub(new BN(balanceInfo.expectedChainBalance));
118
120
  const isMatched = difference.eq(new BN(0));
@@ -131,7 +133,7 @@ async function handleReconciliation(job: ReconciliationJob) {
131
133
  } else {
132
134
  logger.error('Credit balance mismatch detected', {
133
135
  customerId,
134
- customerDid: customer.did,
136
+ customerDid,
135
137
  currencyId,
136
138
  creditGrantId,
137
139
  trigger,
@@ -145,7 +147,7 @@ async function handleReconciliation(job: ReconciliationJob) {
145
147
  // Emit event for monitoring/alerting
146
148
  events.emit('customer.credit.reconciliation.mismatch', {
147
149
  customerId,
148
- customerDid: customer.did,
150
+ customerDid,
149
151
  currencyId,
150
152
  creditGrantId,
151
153
  trigger,
@@ -17,7 +17,7 @@ import { Refund } from '../store/models/refund';
17
17
  import { Subscription } from '../store/models/subscription';
18
18
  import { CreditGrant } from '../store/models/credit-grant';
19
19
  import { Invoice } from '../store/models/invoice';
20
- import { burnToken } from '../integrations/arcblock/token';
20
+ import { burnToken, getAccountState } from '../integrations/arcblock/token';
21
21
  import type { EVMChainType, PaymentError } from '../store/models/types';
22
22
  import { EVM_CHAIN_TYPES } from '../libs/constants';
23
23
 
@@ -587,12 +587,16 @@ export async function handleOnchainCreditRefund(refund: Refund): Promise<string
587
587
 
588
588
  // Only burn if user has remaining tokens
589
589
  if (actualBurnAmount.gt(new BN(0))) {
590
+ // Get the latest DID for burn (handles DID migration via on-chain state)
591
+ // eslint-disable-next-line no-await-in-loop
592
+ const customerState = await getAccountState(grantCurrency, customer.did);
593
+
590
594
  try {
591
595
  // eslint-disable-next-line no-await-in-loop
592
596
  burnHash = await burnToken({
593
597
  paymentCurrency: grantCurrency,
594
598
  amount: fromUnitToToken(actualBurnAmount.toString(), grantCurrency.decimal),
595
- sender: customer.did,
599
+ sender: customerState?.address || customer.did,
596
600
  data: {
597
601
  reason: 'credit_grant_refund',
598
602
  creditGrantId: creditGrant.id,
@@ -1,7 +1,7 @@
1
1
  import { BN } from '@ocap/util';
2
2
  import logger from '../libs/logger';
3
3
  import createQueue from '../libs/queue';
4
- import { transferTokenFromCustomer, getCustomerTokenBalance } from '../integrations/arcblock/token';
4
+ import { getAccountState, transferTokenFromCustomer, getCustomerTokenBalance } from '../integrations/arcblock/token';
5
5
  import { CreditTransaction, CreditGrant, Customer, MeterEvent, PaymentCurrency, Subscription } from '../store/models';
6
6
 
7
7
  type TokenTransferJob = {
@@ -101,6 +101,10 @@ export async function handleTokenTransfer(job: TokenTransferJob): Promise<void>
101
101
 
102
102
  const { creditTransaction, creditGrant, customer, paymentCurrency, meterEvent } = validation;
103
103
 
104
+ // Get the latest DID for transfer (handles DID migration via on-chain state)
105
+ const customerState = await getAccountState(paymentCurrency, customer.did);
106
+ const customerDid = customerState?.address || customer.did;
107
+
104
108
  let txHash: string | null = null;
105
109
  // Record partial transfer details when chain balance is insufficient
106
110
  let transferResult: { expected: string; actual: string } | null = null;
@@ -109,7 +113,7 @@ export async function handleTokenTransfer(job: TokenTransferJob): Promise<void>
109
113
  // Attempt token transfer
110
114
  txHash = await transferTokenFromCustomer({
111
115
  paymentCurrency,
112
- customerDid: customer.did,
116
+ customerDid,
113
117
  amount: job.amount,
114
118
  data: {
115
119
  reason: 'credit_consumption',
@@ -147,7 +151,7 @@ export async function handleTokenTransfer(job: TokenTransferJob): Promise<void>
147
151
  });
148
152
 
149
153
  // Get actual chain balance
150
- const chainBalance = await getCustomerTokenBalance(customer.did, paymentCurrency);
154
+ const chainBalance = await getCustomerTokenBalance(customerDid, paymentCurrency);
151
155
  const chainBalanceBN = new BN(chainBalance);
152
156
  const expectBalanceBN = new BN(job.amount);
153
157
  const chainDebt = expectBalanceBN.sub(chainBalanceBN);
@@ -174,7 +178,7 @@ export async function handleTokenTransfer(job: TokenTransferJob): Promise<void>
174
178
  if (chainBalanceBN.gt(new BN(0))) {
175
179
  txHash = await transferTokenFromCustomer({
176
180
  paymentCurrency,
177
- customerDid: customer.did,
181
+ customerDid,
178
182
  amount: transferAmount.toString(),
179
183
  data: {
180
184
  reason: 'credit_consumption',
@@ -185,7 +189,7 @@ export async function handleTokenTransfer(job: TokenTransferJob): Promise<void>
185
189
 
186
190
  logger.warn('Partial token transfer completed - chain balance insufficient', {
187
191
  creditTransactionId: creditTransaction.id,
188
- customerDid: customer.did,
192
+ customerDid,
189
193
  currencyId: paymentCurrency.id,
190
194
  actualBalance: chainBalanceBN.toString(),
191
195
  expectedAmount: expectBalanceBN.toString(),
@@ -195,7 +199,7 @@ export async function handleTokenTransfer(job: TokenTransferJob): Promise<void>
195
199
  } else {
196
200
  logger.error('Zero chain balance - no tokens transferred', {
197
201
  creditTransactionId: creditTransaction.id,
198
- customerDid: customer.did,
202
+ customerDid,
199
203
  currencyId: paymentCurrency.id,
200
204
  expectedAmount: expectBalanceBN.toString(),
201
205
  });
@@ -19,6 +19,7 @@ import { checkDepositVaultAmount } from '../libs/payment';
19
19
  import { getTokenSummaryByDid } from '../integrations/arcblock/stake';
20
20
  import { MetadataSchema } from '../libs/api';
21
21
  import { getRechargePaymentUrl } from '../libs/currency';
22
+ import { checkCurrencySupportRecurring } from '../libs/product';
22
23
 
23
24
  const router = Router();
24
25
 
@@ -577,4 +578,29 @@ router.put('/:id/recharge-config', auth, async (req, res) => {
577
578
  });
578
579
  });
579
580
 
581
+ // check if currencies support recurring subscriptions
582
+ router.post('/check-recurring-support', async (req, res) => {
583
+ try {
584
+ const { currency_ids: currencyIds } = req.body;
585
+ if (!Array.isArray(currencyIds)) {
586
+ return res.status(400).json({ error: 'currency_ids must be an array' });
587
+ }
588
+ if (currencyIds.length === 0) {
589
+ return res.status(400).json({ error: 'currency_ids cannot be empty' });
590
+ }
591
+ const { notSupportCurrencies, validate } = await checkCurrencySupportRecurring(currencyIds, true);
592
+ return res.json({
593
+ supported: validate,
594
+ unsupported_currencies: notSupportCurrencies.map((c) => ({
595
+ id: c.id,
596
+ name: c.name,
597
+ symbol: c.symbol,
598
+ })),
599
+ });
600
+ } catch (err) {
601
+ logger.error('check recurring support error', err);
602
+ return res.status(400).json({ error: err.message });
603
+ }
604
+ });
605
+
580
606
  export default router;
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.23.5
17
+ version: 1.23.7
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.23.5",
3
+ "version": "1.23.7",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
@@ -44,7 +44,7 @@
44
44
  ]
45
45
  },
46
46
  "dependencies": {
47
- "@abtnode/cron": "^1.17.5",
47
+ "@abtnode/cron": "^1.17.6",
48
48
  "@arcblock/did": "^1.27.15",
49
49
  "@arcblock/did-connect-react": "^3.2.19",
50
50
  "@arcblock/did-connect-storage-nedb": "^1.8.0",
@@ -56,15 +56,15 @@
56
56
  "@arcblock/vc": "^1.27.15",
57
57
  "@blocklet/did-space-js": "^1.2.11",
58
58
  "@blocklet/error": "^0.3.5",
59
- "@blocklet/js-sdk": "^1.17.5",
60
- "@blocklet/logger": "^1.17.5",
61
- "@blocklet/payment-broker-client": "1.23.5",
62
- "@blocklet/payment-react": "1.23.5",
63
- "@blocklet/payment-vendor": "1.23.5",
64
- "@blocklet/sdk": "^1.17.5",
59
+ "@blocklet/js-sdk": "^1.17.6",
60
+ "@blocklet/logger": "^1.17.6",
61
+ "@blocklet/payment-broker-client": "1.23.7",
62
+ "@blocklet/payment-react": "1.23.7",
63
+ "@blocklet/payment-vendor": "1.23.7",
64
+ "@blocklet/sdk": "^1.17.6",
65
65
  "@blocklet/ui-react": "^3.2.19",
66
- "@blocklet/uploader": "^0.3.16",
67
- "@blocklet/xss": "^0.3.14",
66
+ "@blocklet/uploader": "^0.3.17",
67
+ "@blocklet/xss": "^0.3.15",
68
68
  "@mui/icons-material": "^7.1.2",
69
69
  "@mui/lab": "7.0.0-beta.14",
70
70
  "@mui/material": "^7.1.2",
@@ -128,9 +128,9 @@
128
128
  "web3": "^4.16.0"
129
129
  },
130
130
  "devDependencies": {
131
- "@abtnode/types": "^1.17.5",
131
+ "@abtnode/types": "^1.17.6",
132
132
  "@arcblock/eslint-config-ts": "^0.3.3",
133
- "@blocklet/payment-types": "1.23.5",
133
+ "@blocklet/payment-types": "1.23.7",
134
134
  "@types/cookie-parser": "^1.4.9",
135
135
  "@types/cors": "^2.8.19",
136
136
  "@types/debug": "^4.1.12",
@@ -177,5 +177,5 @@
177
177
  "parser": "typescript"
178
178
  }
179
179
  },
180
- "gitHead": "486456ed522207cc4efa54cd14b1f65c6433d179"
180
+ "gitHead": "74ae6bd8f41bf89e0d404af26c788431b4ca4779"
181
181
  }
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
- import { formatBNStr, formatToDate, Table, usePaymentContext } from '@blocklet/payment-react';
3
+ import { formatBNStr, formatCreditAmount, formatToDate, Table, usePaymentContext } from '@blocklet/payment-react';
4
4
  import type { TCreditGrantExpanded } from '@blocklet/payment-types';
5
5
  import { Box, Chip, Divider, styled, Typography } from '@mui/material';
6
6
  import { useNavigate } from 'react-router-dom';
@@ -88,8 +88,10 @@ export default function RelatedCreditGrants({ grants, showDivider = true, mode =
88
88
  return (
89
89
  <Box onClick={() => handleShowGrantDetail(grant)}>
90
90
  <Typography variant="body2">
91
- {formatBNStr(grant.remaining_amount, grant.paymentCurrency?.decimal || 0)}{' '}
92
- {grant.paymentCurrency?.symbol}
91
+ {formatCreditAmount(
92
+ formatBNStr(grant.remaining_amount, grant.paymentCurrency?.decimal || 0),
93
+ grant.paymentCurrency?.symbol || ''
94
+ )}
93
95
  </Typography>
94
96
  </Box>
95
97
  );
@@ -1,6 +1,12 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
- import { formatAmount, Table, getPriceUintAmountByCurrency, formatNumber } from '@blocklet/payment-react';
3
+ import {
4
+ formatAmount,
5
+ Table,
6
+ getPriceUintAmountByCurrency,
7
+ formatNumber,
8
+ formatCreditForCheckout,
9
+ } from '@blocklet/payment-react';
4
10
  import type { TInvoiceExpanded, TInvoiceItem } from '@blocklet/payment-types';
5
11
  import { InfoOutlined } from '@mui/icons-material';
6
12
  import { Box, Stack, Tooltip, Typography } from '@mui/material';
@@ -77,7 +83,7 @@ export function getInvoiceRows(invoice: TInvoiceExpanded, t: (key: string) => st
77
83
  if (creditInfo?.amount) {
78
84
  credits = {
79
85
  total: creditInfo.amount,
80
- currency: creditInfo.currency?.name || 'Credits',
86
+ currency: creditInfo.currency?.symbol || 'Credits',
81
87
  };
82
88
  }
83
89
 
@@ -245,8 +251,7 @@ export default function InvoiceTable({ invoice, simple = false, emptyNodeText =
245
251
  },
246
252
  }}>
247
253
  {t('customer.invoice.creditsInfo', {
248
- amount: formatNumber(item.credits.total),
249
- currency: item.credits.currency,
254
+ amount: formatCreditForCheckout(formatNumber(item.credits.total), item.credits.currency, locale),
250
255
  })}
251
256
  </Typography>
252
257
  )}
@@ -1874,7 +1874,7 @@ export default flat({
1874
1874
  invoice: {
1875
1875
  relatedInvoice: 'Related Invoice',
1876
1876
  donation: 'Donation',
1877
- creditsInfo: 'Total {amount} {currency} included',
1877
+ creditsInfo: 'Total {amount} included',
1878
1878
  appliedDiscounts: 'Applied Discounts',
1879
1879
  },
1880
1880
  payout: {
@@ -1818,7 +1818,7 @@ export default flat({
1818
1818
  invoice: {
1819
1819
  relatedInvoice: '关联账单',
1820
1820
  donation: '打赏记录',
1821
- creditsInfo: '总共包含 {amount} {currency}',
1821
+ creditsInfo: '总共包含 {amount}',
1822
1822
  appliedDiscounts: '已应用优惠',
1823
1823
  },
1824
1824
  payout: {